Skip to content

Commit

Permalink
Make the UI Editor work through portals (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
brandones authored Jan 15, 2021
1 parent 38b6db9 commit 879042c
Show file tree
Hide file tree
Showing 28 changed files with 427 additions and 231 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,4 +1,6 @@
import { Store } from "unistore";
import React from "react";
import { extensionStore } from "@openmrs/esm-extensions";

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

export const createUseStore = (store: Store<any>) => (actions) => {
const state = store.getState();
return { ...state, ...actions };
};

export const useExtensionStore = (actions) => {
const state = extensionStore.getState();
return { ...state, ...actions };
};
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",
},
};
3 changes: 2 additions & 1 deletion packages/esm-implementer-tools-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"test": "jest --config jest.config.js --passWithNoTests",
"build": "webpack --mode=production",
"typescript": "tsc",
"lint": "eslint src --ext ts,tsx"
"lint": "eslint src --ext ts,tsx",
"format": "prettier --write src/**"
},
"keywords": [
"openmrs",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { openmrsFetch } from "@openmrs/esm-api";
import * as semver from "semver";
import { difference } from "lodash-es";
import difference from "lodash-es/difference";

const installedBackendModules: Array<Record<string, string>> = [];
const modulesWithMissingBackendModules: MissingBackendModules[] = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,26 @@ 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(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 Down Expand Up @@ -60,11 +66,8 @@ export default function Configuration(props: ConfigurationProps) {
<Toggle
id={"uiEditorSwitch"}
labelText="UI Editor"
toggled={isUIEditorActive}
onToggle={() => {
setIsUIEditorActive(!isUIEditorActive);
setIsUIEditorEnabled(!isUIEditorActive);
}}
toggled={isUIEditorEnabled}
onToggle={toggleIsUIEditorEnabled}
/>
</Column>
<Column sm={1} md={2} className={styles.actionButton}>
Expand Down Expand Up @@ -109,7 +112,3 @@ export default function Configuration(props: ConfigurationProps) {
</>
);
}

type ConfigurationProps = {
setHasAlert(value: boolean): void;
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import {
setTemporaryConfigValue,
Type,
} from "@openmrs/esm-config";
import { Provider } from "unistore/react";
import Configuration from "./configuration.component";
import { Configuration } from "./configuration.component";
import { getStore } from "../store";
import {
performConceptSearch,
Expand Down Expand Up @@ -109,11 +108,7 @@ describe(`<Configuration />`, () => {
});

function renderConfiguration() {
render(
<Provider store={getStore()}>
<Configuration setHasAlert={() => {}} />
</Provider>
);
render(<Configuration setHasAlert={() => {}} />);
}

it(`renders without dying`, async () => {
Expand Down
Loading

0 comments on commit 879042c

Please sign in to comment.