@openmrs/esm-react-utils 10.0.1-pre.5263 → 10.0.1-pre.5299

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/Extension.d.ts.map +1 -1
  3. package/dist/Extension.js +86 -44
  4. package/dist/index.d.ts +2 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2 -0
  7. package/dist/useAssignedExtensionIds.d.ts +1 -1
  8. package/dist/useAssignedExtensionIds.d.ts.map +1 -1
  9. package/dist/useAssignedExtensionIds.js +7 -14
  10. package/dist/useAssignedExtensions.d.ts +13 -1
  11. package/dist/useAssignedExtensions.d.ts.map +1 -1
  12. package/dist/useAssignedExtensions.js +31 -4
  13. package/dist/useCandidateExtensions.d.ts +11 -0
  14. package/dist/useCandidateExtensions.d.ts.map +1 -0
  15. package/dist/useCandidateExtensions.js +12 -0
  16. package/dist/useExtensionSlot.d.ts.map +1 -1
  17. package/dist/useExtensionSlot.js +8 -13
  18. package/dist/useExtensionSlotStore.d.ts +0 -1
  19. package/dist/useExtensionSlotStore.d.ts.map +1 -1
  20. package/dist/useExtensionSlotStore.js +19 -2
  21. package/dist/useFhirPagination.d.ts +1 -1
  22. package/dist/useOpenmrsPagination.d.ts +2 -2
  23. package/dist/useOpenmrsPagination.d.ts.map +1 -1
  24. package/dist/useOpenmrsPagination.js +66 -15
  25. package/dist/usePagination.d.ts +5 -0
  26. package/dist/usePagination.d.ts.map +1 -1
  27. package/dist/usePagination.js +23 -21
  28. package/dist/useShallowStableValue.d.ts +13 -0
  29. package/dist/useShallowStableValue.d.ts.map +1 -0
  30. package/dist/useShallowStableValue.js +19 -0
  31. package/dist/useStore.d.ts.map +1 -1
  32. package/dist/useStore.js +25 -11
  33. package/package.json +23 -23
  34. package/src/Extension.tsx +110 -54
  35. package/src/extensions.test.tsx +112 -0
  36. package/src/index.ts +2 -0
  37. package/src/useAssignedExtensionIds.ts +5 -15
  38. package/src/useAssignedExtensions.ts +30 -3
  39. package/src/useCandidateExtensions.ts +15 -0
  40. package/src/useExtensionSlot.ts +7 -17
  41. package/src/useExtensionSlotStore.ts +23 -3
  42. package/src/useOpenmrsPagination.test.ts +297 -1
  43. package/src/useOpenmrsPagination.ts +72 -10
  44. package/src/usePagination.test.tsx +315 -0
  45. package/src/usePagination.ts +27 -15
  46. package/src/useShallowStableValue.test.ts +69 -0
  47. package/src/useShallowStableValue.ts +24 -0
  48. package/src/useStore.test.ts +39 -0
  49. package/src/useStore.ts +17 -10
@@ -1,3 +1,3 @@
1
- [0] Successfully compiled: 53 files with swc (130.4ms)
1
+ [0] Successfully compiled: 55 files with swc (217.11ms)
2
2
  [0] swc --strip-leading-paths src -d dist exited with code 0
3
3
  [1] tsc --project tsconfig.build.json exited with code 0
@@ -1 +1 @@
1
- {"version":3,"file":"Extension.d.ts","sourceRoot":"","sources":["../src/Extension.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAqD,MAAM,OAAO,CAAC;AAI1E,MAAM,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,GAAG;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CA+E9C,CAAC"}
1
+ {"version":3,"file":"Extension.d.ts","sourceRoot":"","sources":["../src/Extension.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAqD,MAAM,OAAO,CAAC;AAI1E,MAAM,MAAM,cAAc,GAAG,KAAK,CAAC,cAAc,CAAC,cAAc,CAAC,GAAG;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAuI9C,CAAC"}
package/dist/Extension.js CHANGED
@@ -13,58 +13,100 @@ import { ComponentContext } from "./index.js";
13
13
  const { extension } = useContext(ComponentContext);
14
14
  const parcel = useRef(null);
15
15
  const updatePromise = useRef(Promise.resolve());
16
+ const isUnmounted = useRef(false);
17
+ const isRendering = useRef(false);
18
+ const latestState = useRef(state);
19
+ // Takes the parcel to tear down rather than reading `parcel.current`, which can be reassigned
20
+ // between scheduling this and running it.
21
+ const unmountParcel = useCallback((target)=>{
22
+ if (!target) {
23
+ return;
24
+ }
25
+ const unmountWhenMounted = ()=>{
26
+ if (target.getStatus() === 'MOUNTED') {
27
+ target.unmount();
28
+ }
29
+ };
30
+ switch(target.getStatus()){
31
+ case 'MOUNTED':
32
+ target.unmount();
33
+ break;
34
+ case 'UPDATING':
35
+ // The rejection is reported by whoever owns `updatePromise`; here it only matters that a
36
+ // failed update still runs the teardown, or the parcel is left broken and mounted.
37
+ updatePromise.current?.then(unmountWhenMounted, unmountWhenMounted);
38
+ break;
39
+ default:
40
+ // Any other status: the parcel either hasn't finished coming up or is already gone.
41
+ // `mountPromise` has settled or will, and `unmountWhenMounted` re-checks the status.
42
+ target.mountPromise.then(unmountWhenMounted, ()=>{});
43
+ }
44
+ }, []);
45
+ const applyUpdate = useCallback((target, nextState)=>{
46
+ if (!target.update || target.getStatus() === 'UNMOUNTING') {
47
+ return;
48
+ }
49
+ // Every later update chains onto this promise, so it has to settle even when an update fails.
50
+ updatePromise.current = Promise.all([
51
+ target.mountPromise,
52
+ updatePromise.current
53
+ ]).then(()=>{
54
+ if (parcel.current?.getStatus() === 'MOUNTED' && parcel.current.update) {
55
+ return parcel.current.update({
56
+ ...nextState
57
+ });
58
+ }
59
+ }).catch((err)=>{
60
+ // A parcel torn down while its update was in flight rejects, and that race is expected.
61
+ // We use the status to distinguish reject reasons as the message isn't reliable
62
+ const status = parcel.current?.getStatus();
63
+ if (status !== 'UNMOUNTING' && status !== 'NOT_MOUNTED' && status !== 'UNLOADING') {
64
+ console.error(`The extension '${extension?.extensionId}' failed to update`, err);
65
+ }
66
+ });
67
+ }, []);
16
68
  const ref = useCallback((node)=>{
17
- if (extension?.extensionSlotName && extension.extensionSlotModuleName && extension.extensionSlotModuleName && !parcel.current) {
18
- renderExtension(node, extension.extensionSlotName, extension.extensionSlotModuleName, extension.extensionId, undefined, state).then((newParcel)=>{
19
- parcel.current = newParcel;
20
- });
69
+ // React detaches a ref by calling it with null. If we render something in response to this,
70
+ // React ignores it and we're left tracking a parcel with no visible DOM, so skip conditions
71
+ // we can't handle.
72
+ if (!node || parcel.current || isRendering.current || !extension?.extensionSlotName || !extension.extensionSlotModuleName) {
73
+ return;
21
74
  }
75
+ isRendering.current = true;
76
+ renderExtension(node, extension.extensionSlotName, extension.extensionSlotModuleName, extension.extensionId, undefined, state).then((newParcel)=>{
77
+ isRendering.current = false;
78
+ parcel.current = newParcel;
79
+ // Loading an extension's bundle can outlast the component that asked for it: the cleanup
80
+ // effect has already run and saw no parcel, so teardown has to happen here instead, or the
81
+ // parcel mounts into a detached node and its rendering record outlives the page.
82
+ if (isUnmounted.current) {
83
+ unmountParcel(newParcel);
84
+ return;
85
+ }
86
+ // Ensure we update the state of the parcel to the latest version we've received
87
+ if (newParcel && latestState.current !== state) {
88
+ applyUpdate(newParcel, latestState.current);
89
+ }
90
+ }, // Cleared so a later reattach can try again; `renderExtension` reports its own failures.
91
+ ()=>{
92
+ isRendering.current = false;
93
+ });
22
94
  }, []);
23
95
  useEffect(()=>{
96
+ // Reset on every run, not just the first: StrictMode mounts, tears down and mounts again, so
97
+ // a flag only ever set by the cleanup would still read "unmounted" for the live component.
98
+ isUnmounted.current = false;
24
99
  return ()=>{
25
- if (parcel && parcel.current) {
26
- const status = parcel.current.getStatus();
27
- switch(status){
28
- case 'MOUNTING':
29
- parcel.current.mountPromise.then(()=>{
30
- if (parcel.current?.getStatus() === 'MOUNTED') {
31
- parcel.current.unmount();
32
- }
33
- });
34
- break;
35
- case 'MOUNTED':
36
- parcel.current.unmount();
37
- break;
38
- case 'UPDATING':
39
- if (updatePromise.current) {
40
- updatePromise.current.then(()=>{
41
- if (parcel.current?.getStatus() === 'MOUNTED') {
42
- parcel.current.unmount();
43
- }
44
- });
45
- }
46
- }
47
- }
100
+ isUnmounted.current = true;
101
+ unmountParcel(parcel.current);
48
102
  };
49
103
  }, []);
50
104
  useEffect(()=>{
51
- if (parcel.current && parcel.current.update && parcel.current.getStatus() !== 'UNMOUNTING') {
52
- Promise.all([
53
- parcel.current.mountPromise,
54
- updatePromise.current
55
- ]).then(()=>{
56
- if (parcel?.current?.getStatus() === 'MOUNTED' && parcel.current.update) {
57
- updatePromise.current = parcel.current.update({
58
- ...state
59
- }).catch((err)=>{
60
- // if we were trying to update but the component was unmounted
61
- // while this was happening, ignore the error
62
- if (!(err instanceof Error) || !err.message.includes('minified message #32') || parcel.current?.getStatus() === 'MOUNTED') {
63
- throw err;
64
- }
65
- });
66
- }
67
- });
105
+ // Recorded before the parcel is checked, so that a state that arrives before there is a parcel
106
+ // to receive it is still the one applied once there is.
107
+ latestState.current = state;
108
+ if (parcel.current) {
109
+ applyUpdate(parcel.current, state);
68
110
  }
69
111
  }, [
70
112
  state
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export * from './useAbortController';
10
10
  export * from './useAppContext';
11
11
  export * from './useAssignedExtensionIds';
12
12
  export * from './useAssignedExtensions';
13
+ export * from './useCandidateExtensions';
14
+ export * from './useShallowStableValue';
13
15
  export * from './useAttachments';
14
16
  export * from './useBodyScrollLock';
15
17
  export * from './useConfig';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,KAAK,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAC/F,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,6BAA6B,CAAC;AAC5C,cAAc,kBAAkB,CAAC;AACjC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,iBAAiB,CAAC;AAChC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,yBAAyB,CAAC;AACxC,cAAc,0BAA0B,CAAC;AACzC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,aAAa,CAAC;AAC5B,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,mBAAmB,CAAC;AAClC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,kBAAkB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,KAAK,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAC/F,cAAc,iBAAiB,CAAC;AAChC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,gCAAgC,CAAC;AAC/C,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -10,6 +10,8 @@ export * from "./useAbortController.js";
10
10
  export * from "./useAppContext.js";
11
11
  export * from "./useAssignedExtensionIds.js";
12
12
  export * from "./useAssignedExtensions.js";
13
+ export * from "./useCandidateExtensions.js";
14
+ export * from "./useShallowStableValue.js";
13
15
  export * from "./useAttachments.js";
14
16
  export * from "./useBodyScrollLock.js";
15
17
  export * from "./useConfig.js";
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Gets the assigned extension ids for a given extension slot name.
3
- * Does not consider if offline or online.
3
+ *
4
4
  * @param slotName The name of the slot to get the assigned IDs for.
5
5
  *
6
6
  * @deprecated Use `useAssignedExtensions`
@@ -1 +1 @@
1
- {"version":3,"file":"useAssignedExtensionIds.d.ts","sourceRoot":"","sources":["../src/useAssignedExtensionIds.ts"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,YAavD"}
1
+ {"version":3,"file":"useAssignedExtensionIds.d.ts","sourceRoot":"","sources":["../src/useAssignedExtensionIds.ts"],"names":[],"mappings":"AAIA;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,YAIvD"}
@@ -1,21 +1,14 @@
1
- /** @module @category Extension */ import { useEffect, useState } from "react";
2
- import { getExtensionStore } from "@openmrs/esm-extensions";
3
- import { isEqual } from "lodash-es";
1
+ /** @module @category Extension */ import { useMemo } from "react";
2
+ import { useAssignedExtensions } from "./useAssignedExtensions.js";
4
3
  /**
5
4
  * Gets the assigned extension ids for a given extension slot name.
6
- * Does not consider if offline or online.
5
+ *
7
6
  * @param slotName The name of the slot to get the assigned IDs for.
8
7
  *
9
8
  * @deprecated Use `useAssignedExtensions`
10
9
  */ export function useAssignedExtensionIds(slotName) {
11
- const [ids, setIds] = useState([]);
12
- useEffect(()=>{
13
- return getExtensionStore().subscribe((state)=>{
14
- const newIds = state.slots[slotName]?.assignedExtensions.map((e)=>e.id) ?? [];
15
- if (!isEqual(newIds, ids)) {
16
- setIds(newIds);
17
- }
18
- });
19
- }, []);
20
- return ids;
10
+ const assignedExtensions = useAssignedExtensions(slotName);
11
+ return useMemo(()=>assignedExtensions.map((extension)=>extension.id), [
12
+ assignedExtensions
13
+ ]);
21
14
  }
@@ -1,6 +1,18 @@
1
+ import { type ExtensionSlotCustomState } from '@openmrs/esm-extensions';
1
2
  /**
2
3
  * Gets the assigned extensions for a given extension slot name.
4
+ *
5
+ * The reactive form of `getAssignedExtensions`, and it answers the same thing: display conditions
6
+ * are always applied, so the result is what should actually be displayed. Pass `state` whenever you
7
+ * know it: the same slot can be rendered in several places at once with different state, so
8
+ * conditions are resolved against the state of this rendering. Omitting it resolves them against
9
+ * the session alone, hiding any extension whose condition depends on state.
10
+ *
11
+ * The returned array is a copy, so sorting or filtering it in place can't corrupt the extension
12
+ * store. Its reference is stable for as long as the slot's extensions, the state and the session are.
13
+ *
3
14
  * @param slotName The name of the slot to get the assigned extensions for.
15
+ * @param state The state of this rendering of the slot.
4
16
  */
5
- export declare function useAssignedExtensions(slotName: string): import("@openmrs/esm-extensions").AssignedExtension[];
17
+ export declare function useAssignedExtensions(slotName: string, state?: ExtensionSlotCustomState): import("@openmrs/esm-extensions").AssignedExtension[];
6
18
  //# sourceMappingURL=useAssignedExtensions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useAssignedExtensions.d.ts","sourceRoot":"","sources":["../src/useAssignedExtensions.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,yDAGrD"}
1
+ {"version":3,"file":"useAssignedExtensions.d.ts","sourceRoot":"","sources":["../src/useAssignedExtensions.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,wBAAwB,EAAyB,MAAM,yBAAyB,CAAC;AAO/F;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,wBAAwB,yDAYvF"}
@@ -1,8 +1,35 @@
1
- /** @module @category Extension */ import { useExtensionSlotStore } from "./useExtensionSlotStore.js";
1
+ /** @module @category Extension */ import { useMemo } from "react";
2
+ import { sessionStore } from "@openmrs/esm-api";
3
+ import { getAssignedExtensions } from "@openmrs/esm-extensions";
4
+ import { useShallowStableValue } from "./useShallowStableValue.js";
5
+ import { useExtensionSlotStore } from "./useExtensionSlotStore.js";
6
+ import { useStore } from "./useStore.js";
7
+ const selectSession = (state)=>state.session;
2
8
  /**
3
9
  * Gets the assigned extensions for a given extension slot name.
10
+ *
11
+ * The reactive form of `getAssignedExtensions`, and it answers the same thing: display conditions
12
+ * are always applied, so the result is what should actually be displayed. Pass `state` whenever you
13
+ * know it: the same slot can be rendered in several places at once with different state, so
14
+ * conditions are resolved against the state of this rendering. Omitting it resolves them against
15
+ * the session alone, hiding any extension whose condition depends on state.
16
+ *
17
+ * The returned array is a copy, so sorting or filtering it in place can't corrupt the extension
18
+ * store. Its reference is stable for as long as the slot's extensions, the state and the session are.
19
+ *
4
20
  * @param slotName The name of the slot to get the assigned extensions for.
5
- */ export function useAssignedExtensions(slotName) {
6
- const slotStore = useExtensionSlotStore(slotName);
7
- return slotStore?.assignedExtensions ?? [];
21
+ * @param state The state of this rendering of the slot.
22
+ */ export function useAssignedExtensions(slotName, state) {
23
+ // Subscribes so that this re-runs when the slot's extensions change; `getAssignedExtensions` reads
24
+ // the same store, so the value below is what it is about to return.
25
+ const { candidateExtensions } = useExtensionSlotStore(slotName);
26
+ // Display conditions may refer to `session`, so they have to be re-evaluated when it changes.
27
+ const session = useStore(sessionStore, selectSession);
28
+ const stableState = useShallowStableValue(state);
29
+ return useMemo(()=>getAssignedExtensions(slotName, stableState), [
30
+ slotName,
31
+ stableState,
32
+ session,
33
+ candidateExtensions
34
+ ]);
8
35
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The reactive form of `getCandidateExtensions`: everything assigned to a slot, with no display
3
+ * condition evaluated. Intended for tools that present a slot's configuration rather than render it.
4
+ *
5
+ * Use {@link useAssignedExtensions} to decide what to display.
6
+ *
7
+ * @param slotName The name of the slot to get the candidate extensions for.
8
+ * @internal
9
+ */
10
+ export declare function useCandidateExtensions(slotName: string): import("@openmrs/esm-extensions").AssignedExtension[];
11
+ //# sourceMappingURL=useCandidateExtensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCandidateExtensions.d.ts","sourceRoot":"","sources":["../src/useCandidateExtensions.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,yDAEtD"}
@@ -0,0 +1,12 @@
1
+ /** @module @category Extension */ import { useExtensionSlotStore } from "./useExtensionSlotStore.js";
2
+ /**
3
+ * The reactive form of `getCandidateExtensions`: everything assigned to a slot, with no display
4
+ * condition evaluated. Intended for tools that present a slot's configuration rather than render it.
5
+ *
6
+ * Use {@link useAssignedExtensions} to decide what to display.
7
+ *
8
+ * @param slotName The name of the slot to get the candidate extensions for.
9
+ * @internal
10
+ */ export function useCandidateExtensions(slotName) {
11
+ return useExtensionSlotStore(slotName).candidateExtensions;
12
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"useExtensionSlot.d.ts","sourceRoot":"","sources":["../src/useExtensionSlot.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,wBAAwB,EAG9B,MAAM,yBAAyB,CAAC;AAIjC,gBAAgB;AAChB,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,wBAAwB;;;;EA0BlF"}
1
+ {"version":3,"file":"useExtensionSlot.d.ts","sourceRoot":"","sources":["../src/useExtensionSlot.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,wBAAwB,EAAyB,MAAM,yBAAyB,CAAC;AAI/F,gBAAgB;AAChB,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,wBAAwB;;;;EAoBlF"}
@@ -1,26 +1,21 @@
1
- import { useContext, useEffect, useRef } from "react";
2
- import { registerExtensionSlot, updateExtensionSlotState } from "@openmrs/esm-extensions";
1
+ import { useContext, useEffect } from "react";
2
+ import { registerExtensionSlot } from "@openmrs/esm-extensions";
3
3
  import { ComponentContext } from "./ComponentContext.js";
4
4
  import { useAssignedExtensions } from "./useAssignedExtensions.js";
5
5
  /** @internal */ export function useExtensionSlot(slotName, state) {
6
6
  const { moduleName } = useContext(ComponentContext);
7
- const isInitialRender = useRef(true);
8
7
  if (!moduleName) {
9
8
  throw Error('ComponentContext has not been provided. This should come from @openmrs/esm-react-utils.');
10
9
  }
11
10
  useEffect(()=>{
12
- registerExtensionSlot(moduleName, slotName, state);
13
- isInitialRender.current = false;
14
- }, []);
15
- useEffect(()=>{
16
- if (!isInitialRender.current) {
17
- updateExtensionSlotState(slotName, state);
18
- }
11
+ registerExtensionSlot(moduleName, slotName);
19
12
  }, [
20
- slotName,
21
- state
13
+ moduleName,
14
+ slotName
22
15
  ]);
23
- const extensions = useAssignedExtensions(slotName);
16
+ // `state` must be local to the rendering rather than written to the store as state is
17
+ // render-specific
18
+ const extensions = useAssignedExtensions(slotName, state);
24
19
  return {
25
20
  extensions,
26
21
  extensionSlotName: slotName,
@@ -1,4 +1,3 @@
1
- /** @module @category Extension */
2
1
  import { type ExtensionSlotState } from '@openmrs/esm-extensions';
3
2
  export declare const useExtensionSlotStore: (slot: string) => ExtensionSlotState;
4
3
  //# sourceMappingURL=useExtensionSlotStore.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useExtensionSlotStore.d.ts","sourceRoot":"","sources":["../src/useExtensionSlotStore.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAClC,OAAO,EAAE,KAAK,kBAAkB,EAA0C,MAAM,yBAAyB,CAAC;AAG1G,eAAO,MAAM,qBAAqB,GAAI,MAAM,MAAM,uBACiD,CAAC"}
1
+ {"version":3,"file":"useExtensionSlotStore.d.ts","sourceRoot":"","sources":["../src/useExtensionSlotStore.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,kBAAkB,EAGxB,MAAM,yBAAyB,CAAC;AAcjC,eAAO,MAAM,qBAAqB,GAAI,MAAM,MAAM,uBAIjD,CAAC"}
@@ -1,3 +1,20 @@
1
- /** @module @category Extension */ import { getExtensionStore } from "@openmrs/esm-extensions";
1
+ /** @module @category Extension */ import { useCallback } from "react";
2
+ import { getExtensionStore } from "@openmrs/esm-extensions";
2
3
  import { useStore } from "./useStore.js";
3
- export const useExtensionSlotStore = (slot)=>useStore(getExtensionStore(), (state)=>state.slots?.[slot]);
4
+ /**
5
+ * Stands in for a slot that has not been registered or attached to. Shared rather than built per
6
+ * call, so the snapshot stays reference-stable across renders — and frozen because that sharing
7
+ * means an in-place sort or push would otherwise corrupt every unregistered slot in the page.
8
+ */ const emptyCandidates = [];
9
+ Object.freeze(emptyCandidates);
10
+ const emptySlotState = {
11
+ candidateExtensions: emptyCandidates
12
+ };
13
+ Object.freeze(emptySlotState);
14
+ export const useExtensionSlotStore = (slot)=>{
15
+ // Memoized so that `useStore`'s snapshot function is stable across renders.
16
+ const select = useCallback((state)=>state.slots?.[slot] ?? emptySlotState, [
17
+ slot
18
+ ]);
19
+ return useStore(getExtensionStore(), select);
20
+ };
@@ -28,7 +28,7 @@ export declare function useFhirPagination<T extends fhir.ResourceBase>(url: stri
28
28
  totalPages: number;
29
29
  totalCount: number;
30
30
  currentPage: number;
31
- currentPageSize: import("react").MutableRefObject<number>;
31
+ currentPageSize: number;
32
32
  paginated: boolean;
33
33
  showNextButton: boolean;
34
34
  showPreviousButton: boolean;
@@ -61,7 +61,7 @@ export declare function useOpenmrsPagination<T>(url: string | URL, pageSize: num
61
61
  totalPages: number;
62
62
  totalCount: number;
63
63
  currentPage: number;
64
- currentPageSize: import("react").MutableRefObject<number>;
64
+ currentPageSize: number;
65
65
  paginated: boolean;
66
66
  showNextButton: boolean;
67
67
  showPreviousButton: boolean;
@@ -87,7 +87,7 @@ export declare function useServerPagination<T, R>(url: string | URL, pageSize: n
87
87
  totalPages: number;
88
88
  totalCount: number;
89
89
  currentPage: number;
90
- currentPageSize: import("react").MutableRefObject<number>;
90
+ currentPageSize: number;
91
91
  paginated: boolean;
92
92
  showNextButton: boolean;
93
93
  showPreviousButton: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"useOpenmrsPagination.d.ts","sourceRoot":"","sources":["../src/useOpenmrsPagination.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,KAAK,aAAa,EAAyB,MAAM,kBAAkB,CAAC;AAE7E,OAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAGpD,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B,CAAC,CAAC;IAC3C;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD;;OAEG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,0BAA0B,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAM;;;;;;;;;;;;;iBAgF5D,MAAM;;;EA7EhB;AAED,KAAK,+BAA+B,CAAC,CAAC,IAAI,wBAAwB,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC;AACnG,eAAO,MAAM,+BAA+B,EAAE,+BAA+B,CAAC,GAAG,CA6BhF,CAAC;AAEF,MAAM,WAAW,wBAAwB,CAAC,CAAC,EAAE,CAAC;IAC5C,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzF,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;IACvD,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,CAAC;IACnD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,CAAC;IACxD,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CAC5D;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EACtC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,QAAQ,EAAE,MAAM,EAChB,wBAAwB,EAAE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,EACxD,OAAO,GAAE,0BAA0B,CAAC,CAAC,CAAM;;;;;;;;;;;;;iBA+BlC,MAAM;;;EA+BhB"}
1
+ {"version":3,"file":"useOpenmrsPagination.d.ts","sourceRoot":"","sources":["../src/useOpenmrsPagination.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,KAAK,aAAa,EAAyB,MAAM,kBAAkB,CAAC;AAE7E,OAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,KAAK,CAAC;AAGpD,MAAM,WAAW,wBAAwB,CAAC,CAAC;IACzC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAClB,KAAK,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpD,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,0BAA0B,CAAC,CAAC;IAC3C;;OAEG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;IAErD;;OAEG;IACH,SAAS,CAAC,EAAE,gBAAgB,CAAC;CAC9B;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAAC,CAAC,EACpC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,0BAA0B,CAAC,wBAAwB,CAAC,CAAC,CAAC,CAAM;;;;;;;;;;;;;iBAyG5D,MAAM;;;EAtGhB;AAED,KAAK,+BAA+B,CAAC,CAAC,IAAI,wBAAwB,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC,CAAC,CAAC,CAAC;AACnG,eAAO,MAAM,+BAA+B,EAAE,+BAA+B,CAAC,GAAG,CA6BhF,CAAC;AAEF,MAAM,WAAW,wBAAwB,CAAC,CAAC,EAAE,CAAC;IAC5C,eAAe,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IACzF,UAAU,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;IACvD,aAAa,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,CAAC;IACnD,kBAAkB,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,MAAM,CAAC;IACxD,OAAO,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;CAC5D;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,CAAC,EACtC,GAAG,EAAE,MAAM,GAAG,GAAG,EACjB,QAAQ,EAAE,MAAM,EAChB,wBAAwB,EAAE,wBAAwB,CAAC,CAAC,EAAE,CAAC,CAAC,EACxD,OAAO,GAAE,0BAA0B,CAAC,CAAC,CAAM;;;;;;;;;;;;;iBAwDlC,MAAM;;;EAoEhB"}
@@ -1,5 +1,5 @@
1
1
  /** @module @category UI */ import { makeUrl, openmrsFetch } from "@openmrs/esm-api";
2
- import { useCallback, useMemo, useRef, useState } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import useSWR from "swr";
4
4
  import useSWRImmutable from "swr/immutable";
5
5
  /**
@@ -65,12 +65,31 @@ export function useServerPagination(url, pageSize, serverPaginationHandlers, opt
65
65
  const { getPaginatedUrl, getTotalCount, getCurrentPageSize, getData } = serverPaginationHandlers;
66
66
  const { immutable, swrConfig } = options;
67
67
  const fetcher = options.fetcher ?? openmrsFetch;
68
- const [currentPage, setCurrentPage] = useState(1); // 1-indexing instead of 0-indexing, to keep consistency with `usePagination`
68
+ // 1-indexing instead of 0-indexing, to keep consistency with `usePagination`
69
+ const [currentPage, setCurrentPage] = useState(1);
69
70
  // Cache the totalCount and currentPageSize so we don't lose them
70
71
  // as we wait for next page's result to load while navigating to a different page.
71
72
  // This can be used to prevent jarring UI changes while loading
72
73
  const totalCount = useRef(Number.NaN);
73
- const currentPageSize = useRef(Number.NaN); // this value is usually same as pageSize, except when currentPage is last page.
74
+ // this value is usually same as pageSize, except when currentPage is last page.
75
+ const currentPageSize = useRef(Number.NaN);
76
+ // tracked as a ref so we don't need to make the current page a dependency of `goTo()`, etc.
77
+ const currentPageRef = useRef(currentPage);
78
+ // use to differentiate "endpoint doesn't return a total" from "we haven't run a request yet"
79
+ const hasResponded = useRef(false);
80
+ const warnedMissingTotal = useRef(false);
81
+ // if the URL we're querying changes, reset to page 1
82
+ const urlKey = url?.toString() ?? '';
83
+ const [lastUrlKey, setLastUrlKey] = useState(urlKey);
84
+ if (urlKey !== lastUrlKey) {
85
+ setLastUrlKey(urlKey);
86
+ setCurrentPage(1);
87
+ currentPageRef.current = 1;
88
+ totalCount.current = Number.NaN;
89
+ currentPageSize.current = Number.NaN;
90
+ hasResponded.current = false;
91
+ warnedMissingTotal.current = false;
92
+ }
74
93
  const limit = pageSize;
75
94
  const startIndex = (currentPage - 1) * pageSize;
76
95
  const urlUrl = useMemo(()=>{
@@ -85,39 +104,71 @@ export function useServerPagination(url, pageSize, serverPaginationHandlers, opt
85
104
  if (data?.data) {
86
105
  totalCount.current = getTotalCount(data?.data);
87
106
  currentPageSize.current = getCurrentPageSize(data?.data);
107
+ hasResponded.current = true;
88
108
  }
89
109
  const totalPages = Math.ceil(totalCount.current / pageSize);
110
+ // The bound is read from `totalCount` at call time rather than captured from the creating render,
111
+ // which is what keeps this callback referentially stable, apart from a change in `pageSize`.
90
112
  const goTo = useCallback((page)=>{
91
- if (0 < page && page <= totalPages) {
113
+ if (!Number.isInteger(page) || page < 1) {
114
+ console.warn(`useServerPagination: ignoring goTo(${page}); page must be a positive integer.`);
115
+ return;
116
+ }
117
+ // page count is not known prior to running any query when `bound` will be `NaN`, so in this
118
+ // case we assume the requested page is fine; after results return, things will settle properly
119
+ // clamped
120
+ const bound = Math.ceil(totalCount.current / pageSize);
121
+ if (!hasResponded.current || Number.isNaN(bound) || page <= bound) {
122
+ currentPageRef.current = page;
92
123
  setCurrentPage(page);
93
124
  } else {
94
125
  console.warn('Invalid attempt to go to out of bounds page: ' + page);
95
126
  }
96
127
  }, [
97
- url,
128
+ pageSize
129
+ ]);
130
+ // A page accepted before the total was known may turn out not to exist, so put the user on the last
131
+ // real page once the response settles it. Costs an extra request, but only for a page that was out of
132
+ // range to begin with; without it the hook sits on an empty page with no way back.
133
+ useEffect(()=>{
134
+ if (!hasResponded.current) {
135
+ return;
136
+ }
137
+ if (Number.isNaN(totalCount.current)) {
138
+ if (!warnedMissingTotal.current) {
139
+ warnedMissingTotal.current = true;
140
+ console.warn(`useServerPagination: ${urlKey} returned no total count, so out-of-range pages cannot be detected.`);
141
+ }
142
+ return;
143
+ }
144
+ const lastPage = Math.max(1, Math.ceil(totalCount.current / pageSize));
145
+ if (currentPage > lastPage) {
146
+ console.warn(`Invalid attempt to go to out of bounds page: ${currentPage}; returning to page ${lastPage}.`);
147
+ currentPageRef.current = lastPage;
148
+ setCurrentPage(lastPage);
149
+ }
150
+ }, [
98
151
  currentPage,
99
- totalPages
152
+ pageSize,
153
+ urlKey,
154
+ data
100
155
  ]);
101
156
  const goToNext = useCallback(()=>{
102
- goTo(currentPage + 1);
157
+ goTo(currentPageRef.current + 1);
103
158
  }, [
104
- url,
105
- currentPage,
106
- totalPages
159
+ goTo
107
160
  ]);
108
161
  const goToPrevious = useCallback(()=>{
109
- goTo(currentPage - 1);
162
+ goTo(currentPageRef.current - 1);
110
163
  }, [
111
- url,
112
- currentPage,
113
- totalPages
164
+ goTo
114
165
  ]);
115
166
  return {
116
167
  data: getData(data?.data),
117
168
  totalPages,
118
169
  totalCount: totalCount.current,
119
170
  currentPage,
120
- currentPageSize,
171
+ currentPageSize: currentPageSize.current,
121
172
  paginated: totalPages > 1,
122
173
  showNextButton: currentPage < totalPages,
123
174
  showPreviousButton: currentPage > 1,
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Use this hook to paginate data that already exists on the client side.
3
3
  * Note that if the data is obtained from server-side, the caller must handle server-side pagination manually.
4
+ *
5
+ * `goTo`, `goToNext` and `goToPrevious` keep the same identity for the life of the component, so they are
6
+ * safe to list in a dependency array. `currentPage` is bounded by the data, so it can change on its own
7
+ * when the data shrinks; a component reacting to `currentPage` should expect that.
8
+ *
4
9
  * @see `useServerPagination` for hook that automatically manages server-side pagination.
5
10
  * @see `useServerInfinite` for hook to get all data loaded onto the client-side
6
11
  * @param data
@@ -1 +1 @@
1
- {"version":3,"file":"usePagination.d.ts","sourceRoot":"","sources":["../src/usePagination.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,GAAE,KAAK,CAAC,CAAC,CAAM,EAAE,cAAc,SAAwB;;;;;;;iBAiBjF,MAAM;;;EAiChB"}
1
+ {"version":3,"file":"usePagination.d.ts","sourceRoot":"","sources":["../src/usePagination.ts"],"names":[],"mappings":"AAKA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,IAAI,GAAE,KAAK,CAAC,CAAC,CAAM,EAAE,cAAc,SAAwB;;;;;;;iBAwB1D,MAAM;;;EAiCvC"}