Skip to content

Commit

Permalink
MF-363 Application should update live in response to temporary config…
Browse files Browse the repository at this point in the history
  • Loading branch information
brandones authored Oct 19, 2020
1 parent 902cb05 commit 23fc6a9
Show file tree
Hide file tree
Showing 7 changed files with 178 additions and 59 deletions.
5 changes: 5 additions & 0 deletions packages/esm-config/src/module-config/module-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as R from "ramda";
import { invalidateConfigCache } from "../react-hook/config-cache";

// The input configs
type ProvidedConfig = {
Expand Down Expand Up @@ -105,6 +106,7 @@ export async function getDevtoolsConfig(): Promise<object> {
*/
export function setAreDevDefaultsOn(value: boolean): void {
localStorage.setItem("openmrsConfigAreDevDefaultsOn", JSON.stringify(value));
invalidateConfigCache();
}

/**
Expand Down Expand Up @@ -133,6 +135,7 @@ export function setTemporaryConfigValue(path: string[], value: any): void {
"openmrsTemporaryConfig",
JSON.stringify(_temporaryConfig)
);
invalidateConfigCache();
}

/**
Expand All @@ -145,6 +148,7 @@ export function unsetTemporaryConfigValue(path: string[]): void {
"openmrsTemporaryConfig",
JSON.stringify(_temporaryConfig)
);
invalidateConfigCache();
}

/**
Expand All @@ -153,6 +157,7 @@ export function unsetTemporaryConfigValue(path: string[]): void {
export function clearTemporaryConfig(): void {
_temporaryConfig = {};
localStorage.removeItem("openmrsTemporaryConfig");
invalidateConfigCache();
}

/**
Expand Down
23 changes: 23 additions & 0 deletions packages/esm-config/src/react-hook/config-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Subject } from "rxjs";
import { useState, useCallback } from "react";

export const configCache = {};
export const configCacheNotifier = new Subject<null>();

export function invalidateConfigCache() {
configCacheNotifier.next();
}

export function clearConfigCache() {
for (let member in configCache) {
delete configCache[member];
}
}

export function useForceUpdate() {
const [, setTick] = useState(0);
const update = useCallback(() => {
setTick((tick) => tick + 1);
}, []);
return update;
}
51 changes: 36 additions & 15 deletions packages/esm-config/src/react-hook/use-config.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import React from "react";
import { ModuleNameContext } from "@openmrs/esm-context";
import { render, cleanup, waitFor } from "@testing-library/react";
import { useConfig, clearConfig } from "./use-config";
import { clearAll, defineConfigSchema } from "../module-config/module-config";
import { render, cleanup, screen, waitFor } from "@testing-library/react";
import { useConfig } from "./use-config";
import {
clearAll,
defineConfigSchema,
setTemporaryConfigValue,
} from "../module-config/module-config";
import { clearConfigCache } from "./config-cache";

describe(`useConfig`, () => {
afterEach(clearAll);
afterEach(cleanup);
afterEach(clearConfig);
afterEach(clearConfigCache);

it(`can return config as a react hook`, async () => {
defineConfigSchema("foo-module", {
Expand All @@ -16,17 +21,15 @@ describe(`useConfig`, () => {
},
});

const { getByText } = render(
render(
<React.Suspense fallback={<div>Suspense!</div>}>
<ModuleNameContext.Provider value="foo-module">
<RenderConfig configKey="thing" />
</ModuleNameContext.Provider>
</React.Suspense>
);

await waitFor(() => {
expect(getByText("The first thing")).toBeTruthy();
});
expect(screen.findByText("The first thing")).toBeTruthy();
});

it(`can handle multiple calls to useConfig from different modules`, async () => {
Expand All @@ -42,31 +45,49 @@ describe(`useConfig`, () => {
},
});

let wrapper = render(
render(
<React.Suspense fallback={<div>Suspense!</div>}>
<ModuleNameContext.Provider value="foo-module">
<RenderConfig configKey="thing" />
</ModuleNameContext.Provider>
</React.Suspense>
);

await waitFor(() => {
expect(wrapper.getByText("foo thing")).toBeTruthy();
});
expect(screen.findByText("foo thing")).toBeTruthy();

cleanup();

wrapper = render(
render(
<React.Suspense fallback={<div>Suspense!</div>}>
<ModuleNameContext.Provider value="bar-module">
<RenderConfig configKey="thing" />
</ModuleNameContext.Provider>
</React.Suspense>
);

await waitFor(() => {
expect(wrapper.getByText("bar thing")).toBeTruthy();
expect(screen.findByText("bar thing")).toBeTruthy();
});

it("updates with a new value when the temporary config is updated", async () => {
defineConfigSchema("foo-module", {
thing: {
default: "The first thing",
},
});

render(
<React.Suspense fallback={<div>Suspense!</div>}>
<ModuleNameContext.Provider value="foo-module">
<RenderConfig configKey="thing" />
</ModuleNameContext.Provider>
</React.Suspense>
);

expect(screen.findByText("The first thing")).toBeTruthy();

setTemporaryConfigValue(["foo-module", "thing"], "A new thing");

expect(screen.findByText("A new thing")).toBeTruthy();
});
});

Expand Down
47 changes: 31 additions & 16 deletions packages/esm-config/src/react-hook/use-config.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,54 @@
import React from "react";
import { useContext, useEffect } from "react";
import { ModuleNameContext } from "@openmrs/esm-context";
import * as Config from "../module-config/module-config";
import {
configCache,
configCacheNotifier,
useForceUpdate,
} from "./config-cache";

let config = {};
let error;

/**
* Use this React Hook to obtain your module's configuration.
*/
export function useConfig() {
const moduleName = React.useContext(ModuleNameContext);
const moduleName = useContext(ModuleNameContext);
const forceUpdate = useForceUpdate();

if (!moduleName) {
throw Error(
"ModuleNameContext has not been provided. This should come from openmrs-react-root-decorator"
);
}

function getConfigAndSetCache(moduleName: string) {
return Config.getConfig(moduleName)
.then((res) => {
configCache[moduleName] = res;
forceUpdate();
})
.catch((err) => {
error = err;
});
}

useEffect(() => {
const sub = configCacheNotifier.subscribe(() => {
getConfigAndSetCache(moduleName);
});
return () => sub.unsubscribe();
}, [moduleName]);

if (error) {
// Suspense will just keep calling useConfig if the thrown promise rejects.
// So we check ahead of time and avoid creating a new promise.
throw error;
}
if (!config[moduleName]) {
if (!configCache[moduleName]) {
// React will prevent the client component from rendering until the promise resolves
throw Config.getConfig(moduleName)
.then((res) => {
config[moduleName] = res;
})
.catch((err) => {
error = err;
});
throw getConfigAndSetCache(moduleName);
} else {
return config[moduleName];
return configCache[moduleName];
}
}

export function clearConfig() {
config = {};
}
65 changes: 52 additions & 13 deletions packages/esm-config/src/react-hook/use-extension-config.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import {
clearAll,
defineConfigSchema,
provide,
getExtensionConfig,
setTemporaryConfigValue,
} from "../module-config/module-config";
import { useExtensionConfig, clearConfig } from "./use-extension-config";
import { useExtensionConfig } from "./use-extension-config";
import { clearConfigCache } from "./config-cache";

describe(`useExtensionConfig`, () => {
afterEach(clearAll);
afterEach(cleanup);
afterEach(clearConfig);
afterEach(clearConfigCache);

it(`can return extension config as a react hook`, async () => {
defineConfigSchema("ext-module", {
Expand All @@ -37,9 +38,7 @@ describe(`useExtensionConfig`, () => {
</React.Suspense>
);

await waitFor(() => {
expect(screen.getByText("The first thing")).toBeTruthy();
});
expect(screen.findByText("The first thing")).toBeTruthy();
});

it(`can handle multiple extensions`, async () => {
Expand Down Expand Up @@ -80,10 +79,8 @@ describe(`useExtensionConfig`, () => {
</React.Suspense>
);

await waitFor(() => {
expect(screen.getByText("foo thing")).toBeTruthy();
});
expect(screen.getByText("bar thing")).toBeTruthy();
expect(await screen.findByText("foo thing")).toBeTruthy();
expect(screen.findByText("bar thing")).toBeTruthy();
});

it("can handle multiple extension slots", async () => {
Expand Down Expand Up @@ -132,10 +129,52 @@ describe(`useExtensionConfig`, () => {
</React.Suspense>
);

await waitFor(() => {
expect(screen.getByText("foo thing")).toBeTruthy();
expect(await screen.findByText("foo thing")).toBeTruthy();
expect(screen.findByText("another thing")).toBeTruthy();
});

it("updates with a new value when the temporary config is updated", async () => {
defineConfigSchema("ext-module", {
thing: {
default: "The first thing",
},
});
expect(screen.getByText("another thing")).toBeTruthy();

render(
<React.Suspense fallback={<div>Suspense!</div>}>
<ModuleNameContext.Provider value="slot-module">
<ExtensionContext.Provider
value={{
extensionModuleName: "ext-module",
extensionSlotName: "fooSlot",
extensionId: "barExt#id1",
}}
>
<RenderConfig configKey="thing" />
</ExtensionContext.Provider>
</ModuleNameContext.Provider>
</React.Suspense>
);

expect(await screen.findByText("The first thing")).toBeTruthy();

setTemporaryConfigValue(["ext-module", "thing"], "A new thing");

expect(await screen.findByText("A new thing")).toBeTruthy();

setTemporaryConfigValue(
[
"slot-module",
"extensions",
"fooSlot",
"configure",
"barExt#id1",
"thing",
],
"Yet another thing"
);

expect(await screen.findByText("Yet another thing")).toBeTruthy();
});
});

Expand Down
Loading

0 comments on commit 23fc6a9

Please sign in to comment.