Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the UI Editor work through portals #53

Merged
merged 2 commits into from
Jan 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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