Skip to content

Commit

Permalink
Make the UI Editor work via portals
Browse files Browse the repository at this point in the history
  • Loading branch information
brandones committed Jan 5, 2021
1 parent 38b6db9 commit 9f723d7
Show file tree
Hide file tree
Showing 23 changed files with 495 additions and 205 deletions.
31 changes: 15 additions & 16 deletions packages/esm-extensions/src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ function createNewExtensionSlotInstance(): ExtensionSlotInstance {
idOrder: [],
removedIds: [],
registered: 1,
domElement: null,
};
}

Expand Down Expand Up @@ -48,6 +49,7 @@ export const registerExtension: (
name,
load,
moduleName,
instances: {},
};
}
);
Expand Down Expand Up @@ -204,10 +206,12 @@ function getUpdatedExtensionSlotInfoForUnregistration(
*
* @param moduleName The name of the module that contains the extension slot
* @param actualExtensionSlotName The extension slot name that is actually used
* @param domElement The HTML element of the extension slot
*/
export function registerExtensionSlot(
moduleName: string,
actualExtensionSlotName: string
actualExtensionSlotName: string,
domElement: HTMLElement
) {
updateExtensionStore(async (state) => {
const slotName =
Expand All @@ -224,7 +228,16 @@ export function registerExtensionSlot(
...state,
slots: {
...state.slots,
[slotName]: updatedSlot,
[slotName]: {
...updatedSlot,
instances: {
...updatedSlot.instances,
[moduleName]: {
...updatedSlot.instances[moduleName],
domElement,
},
},
},
},
};
});
Expand Down Expand Up @@ -268,20 +281,6 @@ export function getExtensionSlotsForModule(moduleName: string) {
);
}

const uiEditorSettingKey = "openmrs:isUIEditorEnabled";

export function getIsUIEditorEnabled(): boolean {
try {
return JSON.parse(localStorage.getItem(uiEditorSettingKey) ?? "false");
} catch {
return false;
}
}

export function setIsUIEditorEnabled(enabled: boolean) {
localStorage.setItem(uiEditorSettingKey, JSON.stringify(enabled));
}

/**
* @internal
* Just for testing.
Expand Down
17 changes: 17 additions & 0 deletions packages/esm-extensions/src/render.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { mountRootParcel } from "single-spa";
import { cloneDeep, set } from "lodash-es";
import { getExtensionRegistration } from "./extensions";
import { getActualRouteProps } from "./route";
import { updateExtensionStore } from "./store";

export interface Lifecycle {
bootstrap(): void;
Expand Down Expand Up @@ -57,6 +59,21 @@ export function renderExtension(
domElement,
})
);

updateExtensionStore((state) =>
set(
cloneDeep(state),
[
"extensions",
extensionName,
"instances",
extensionSlotModuleName,
actualExtensionSlotName,
"domElement",
],
domElement
)
);
} else {
throw Error(
`Couldn't find extension '${extensionName}' to attach to '${actualExtensionSlotName}'`
Expand Down
18 changes: 17 additions & 1 deletion packages/esm-extensions/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,21 @@ export interface ExtensionRegistration extends ExtensionDefinition {
moduleName: string;
}

export interface ExtensionInfo extends ExtensionRegistration {
/**
* The instances where the extension has been rendered using `renderExtension`,
* indexed by slotModuleName and actualExtensionSlotName
*/
instances: Record<string, Record<string, ExtensionInstance>>;
}

export interface ExtensionInstance {
domElement: HTMLElement;
}

export interface ExtensionStore {
slots: Record<string, ExtensionSlotInfo>;
extensions: Record<string, ExtensionRegistration>;
extensions: Record<string, ExtensionInfo>;
}

export interface ExtensionSlotInstance {
Expand Down Expand Up @@ -39,6 +51,10 @@ export interface ExtensionSlotInstance {
* The number of active registrations on the instance.
*/
registered: number;
/**
* The dom element at which the slot is mounted
*/
domElement: HTMLElement | null;
}

export interface ExtensionSlotInfo {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,59 +1,28 @@
import createStore, { Store } from "unistore";

export function openmrsFetch() {
return new Promise(() => {});
}

interface StoreEntity {
value: Store<any>;
active: boolean;
}

const availableStores: Record<string, StoreEntity> = {};

export function createGlobalStore<TState>(
name: string,
initialState: TState
): Store<TState> {
const available = availableStores[name];

if (available) {
if (available.active) {
console.error(
"Cannot override an existing store. Make sure that stores are only created once."
);
} else {
available.value.setState(initialState, true);
}

available.active = true;
return available.value;
} else {
const store = createStore(initialState);

availableStores[name] = {
value: store,
active: true,
};

return store;
}
let state;

function makeStore(state) {
return {
getState: () => state,
setState: (val) => {
state = { ...state, ...val };
},
subscribe: (updateFcn) => {
updateFcn(state);
return () => {};
},
unsubscribe: () => {},
};
}

export function getGlobalStore<TState = any>(
name: string,
fallbackState?: TState
): Store<TState> {
const available = availableStores[name];
export const createGlobalStore = jest.fn().mockImplementation((n, value) => {
state = value;
return makeStore(state);
});

if (!available) {
const store = createStore(fallbackState);
availableStores[name] = {
value: store,
active: false,
};
return store;
}

return available.value;
}
export const getGlobalStore = jest
.fn()
.mockImplementation(() => makeStore(state));
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
export const getIsUIEditorEnabled = (): boolean => true;

export const setIsUIEditorEnabled = (boolean): void => {};

let state = { slots: {}, extensions: {} };
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { merge } from "lodash-es";
import { Store } from "unistore";
import React from "react";

export const ExtensionContext = React.createContext({
Expand All @@ -15,3 +17,12 @@ export const openmrsRootDecorator = jest
export const UserHasAccess = jest.fn().mockImplementation((props: any) => {
return props.children;
});

export const createUseStore = (store: Store<any>) => (reducer, actions) => {
const state = store.getState();
return merge(
actions,
typeof reducer === "string" ? { [reducer]: state[reducer] } : {},
...(Array.isArray(reducer) ? reducer.map((r) => ({ [r]: state[r] })) : [{}])
);
};
4 changes: 2 additions & 2 deletions packages/esm-implementer-tools-app/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ module.exports = {
"\\.(css)$": "identity-obj-proxy",
"@openmrs/esm-api": "<rootDir>/__mocks__/openmrs-esm-api.mock.tsx",
"@openmrs/esm-config": "<rootDir>/__mocks__/openmrs-esm-config.mock.tsx",
"@openmrs/esm-react-utils":
"<rootDir>/__mocks__/openmrs-esm-react-utils.mock.tsx",
"@openmrs/esm-extensions":
"<rootDir>/__mocks__/openmrs-esm-extensions.mock.tsx",
"@openmrs/esm-styleguide":
"<rootDir>/__mocks__/openmrs-esm-styleguide.mock.tsx",
"@openmrs/esm-react-utils":
"<rootDir>/__mocks__/openmrs-esm-react-utils.mock.tsx",
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,29 @@ import { Button, Column, Grid, Row, Toggle } from "carbon-components-react";
import { Download16, TrashCan16 } from "@carbon/icons-react";
import styles from "./configuration.styles.css";
import { ConfigTree } from "./config-tree.component";
import {
getIsUIEditorEnabled,
setIsUIEditorEnabled,
} from "@openmrs/esm-extensions";
import { getStore, ImplementerToolsStore, useStore } from "../store";
import { Description } from "./description.component";

export default function Configuration(props: ConfigurationProps) {
export type ConfigurationProps = {
setHasAlert(value: boolean): void;
};

const actions = {
toggleIsUIEditorEnabled({ isUIEditorEnabled }: ImplementerToolsStore) {
return { isUIEditorEnabled: !isUIEditorEnabled };
},
};

export function Configuration({ setHasAlert }: ConfigurationProps) {
const { isUIEditorEnabled, toggleIsUIEditorEnabled } = useStore(
["isUIEditorEnabled"],
actions
);
const [config, setConfig] = useState({});
const [isDevConfigActive, setIsDevConfigActive] = useState(
getAreDevDefaultsOn()
);
const [isUIEditorActive, setIsUIEditorActive] = useState(
getIsUIEditorEnabled()
);
const store = getStore();
const tempConfig = getTemporaryConfig();
const tempConfigObjUrl = new Blob(
[JSON.stringify(tempConfig, undefined, 2)],
Expand All @@ -41,6 +50,7 @@ export default function Configuration(props: ConfigurationProps) {
useEffect(updateConfig, []);

return (
<<<<<<< HEAD
<>
<div className={styles.tools}>
<Grid style={{ margin: "0.25rem", padding: 0 }}>
Expand Down Expand Up @@ -107,9 +117,53 @@ export default function Configuration(props: ConfigurationProps) {
</div>
</div>
</>
=======
<>
<div className={styles.tools}>
<Toggle
id="devConfigSwitch"
labelText="Dev Config"
onToggle={() => {
setAreDevDefaultsOn(!isDevConfigActive);
setIsDevConfigActive(!isDevConfigActive);
}}
toggled={isDevConfigActive}
/>
<Toggle
id={"uiEditorSwitch"}
labelText="UI Editor"
toggled={isUIEditorEnabled}
onToggle={toggleIsUIEditorEnabled}
/>
<Button
small
kind="secondary"
onClick={() => {
clearTemporaryConfig();
updateConfig();
}}
>
Clear Temporary Config
</Button>
<Button small kind="secondary" renderIcon={Download16}>
<a
className={styles.downloadLink}
download="temporary_config.json"
href={window.URL.createObjectURL(tempConfigObjUrl)}
>
Download Temporary Config
</a>
</Button>
</div>
<div className={styles.mainContent}>
<div className={styles.configTreePane}>
<ConfigTree config={config} />
</div>
<div className={styles.descriptionPane}>
<Description />
</div>
</div>
</>
>>>>>>> c4338a5... Make the UI Editor work via portals
);
}

type ConfigurationProps = {
setHasAlert(value: boolean): void;
};
Loading

0 comments on commit 9f723d7

Please sign in to comment.