@dhis2-ui/organisation-unit-tree 10.16.2 → 10.16.3-alpha.1

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 (142) hide show
  1. package/package.json +8 -7
  2. package/src/__e2e__/children_as_child_nodes.js +23 -0
  3. package/src/__e2e__/common.js +70 -0
  4. package/src/__e2e__/controlled_expanded.js +89 -0
  5. package/src/__e2e__/displaying_loading_error.js +45 -0
  6. package/src/__e2e__/expanded.js +42 -0
  7. package/src/__e2e__/force_reload.js +66 -0
  8. package/src/__e2e__/get-organisation-unit-data.js +119 -0
  9. package/src/__e2e__/highlight.js +23 -0
  10. package/src/__e2e__/loading_state.js +37 -0
  11. package/src/__e2e__/multi_selection.js +24 -0
  12. package/src/__e2e__/namespace.js +1 -0
  13. package/src/__e2e__/no_selection.js +32 -0
  14. package/src/__e2e__/path_based_filtering.js +49 -0
  15. package/src/__e2e__/single_selection.js +46 -0
  16. package/src/__e2e__/sub_unit_as_root.js +28 -0
  17. package/src/__e2e__/tree_api.js +55 -0
  18. package/src/__stories__/collapsed.js +11 -0
  19. package/src/__stories__/custom-expanded-imperative-open.js +181 -0
  20. package/src/__stories__/custom-node-label.js +19 -0
  21. package/src/__stories__/development-stories.js +86 -0
  22. package/src/__stories__/expanded.js +12 -0
  23. package/src/__stories__/filtered-root.js +15 -0
  24. package/src/__stories__/filtered.js +13 -0
  25. package/src/__stories__/force-reload-all.js +46 -0
  26. package/src/__stories__/force-reload-one-unit.js +36 -0
  27. package/src/__stories__/highlighted.js +13 -0
  28. package/src/__stories__/indeterminate.js +13 -0
  29. package/src/__stories__/loading-error-grandchild.js +39 -0
  30. package/src/__stories__/loading.js +27 -0
  31. package/src/__stories__/multiple-roots.js +20 -0
  32. package/src/__stories__/no-selection.js +16 -0
  33. package/src/__stories__/replace-roots.js +28 -0
  34. package/src/__stories__/root-error.js +36 -0
  35. package/src/__stories__/root-loading.js +34 -0
  36. package/src/__stories__/rtl.js +14 -0
  37. package/src/__stories__/selected-multiple.js +18 -0
  38. package/src/__stories__/shared.js +192 -0
  39. package/src/__stories__/single-selection.js +16 -0
  40. package/src/features/children_as_child_nodes/index.js +29 -0
  41. package/src/features/children_as_child_nodes.feature +6 -0
  42. package/src/features/controlled_expanded/index.js +86 -0
  43. package/src/features/controlled_expanded.feature +11 -0
  44. package/src/features/displaying_loading_error/index.js +46 -0
  45. package/src/features/displaying_loading_error.feature +24 -0
  46. package/src/features/expanded/index.js +87 -0
  47. package/src/features/expanded.feature +27 -0
  48. package/src/features/force_reload/index.js +36 -0
  49. package/src/features/force_reload.feature +7 -0
  50. package/src/features/highlight/index.js +9 -0
  51. package/src/features/highlight.feature +5 -0
  52. package/src/features/loading_state/index.js +26 -0
  53. package/src/features/loading_state.feature +7 -0
  54. package/src/features/multi_selection/index.js +94 -0
  55. package/src/features/multi_selection.feature +31 -0
  56. package/src/features/no_selection/index.js +41 -0
  57. package/src/features/no_selection.feature +13 -0
  58. package/src/features/path_based_filtering/index.js +97 -0
  59. package/src/features/path_based_filtering.feature +24 -0
  60. package/src/features/single_selection/index.js +41 -0
  61. package/src/features/single_selection.feature +20 -0
  62. package/src/features/sub_unit_as_root/index.js +83 -0
  63. package/src/features/sub_unit_as_root.feature +34 -0
  64. package/src/features/tree_api/index.js +121 -0
  65. package/src/features/tree_api.feature +37 -0
  66. package/src/get-all-expanded-paths/get-all-expanded-paths.js +32 -0
  67. package/src/get-all-expanded-paths/get-all-expanded-paths.test.js +22 -0
  68. package/src/get-all-expanded-paths/index.js +1 -0
  69. package/src/helpers/index.js +3 -0
  70. package/src/helpers/is-path-included.js +15 -0
  71. package/src/helpers/left-trim-to-root-id.js +3 -0
  72. package/src/helpers/sort-node-children-alphabetically.js +5 -0
  73. package/src/index.js +6 -0
  74. package/src/locales/ar/translations.json +5 -0
  75. package/src/locales/cs/translations.json +5 -0
  76. package/src/locales/en/translations.json +5 -0
  77. package/src/locales/es/translations.json +5 -0
  78. package/src/locales/es_419/translations.json +5 -0
  79. package/src/locales/fr/translations.json +5 -0
  80. package/src/locales/index.js +50 -0
  81. package/src/locales/lo/translations.json +5 -0
  82. package/src/locales/nb/translations.json +5 -0
  83. package/src/locales/nl/translations.json +5 -0
  84. package/src/locales/pt/translations.json +5 -0
  85. package/src/locales/ru/translations.json +5 -0
  86. package/src/locales/uk/translations.json +5 -0
  87. package/src/locales/uz_Latn/translations.json +5 -0
  88. package/src/locales/uz_UZ_Cyrl/translations.json +5 -0
  89. package/src/locales/uz_UZ_Latn/translations.json +5 -0
  90. package/src/locales/vi/translations.json +5 -0
  91. package/src/locales/zh/translations.json +5 -0
  92. package/src/locales/zh_CN/translations.json +5 -0
  93. package/src/organisation-unit-node/compute-child-nodes.js +27 -0
  94. package/src/organisation-unit-node/compute-child-nodes.test.js +85 -0
  95. package/src/organisation-unit-node/error-message.js +23 -0
  96. package/src/organisation-unit-node/has-descendant-selected-paths.js +15 -0
  97. package/src/organisation-unit-node/has-descendant-selected-paths.test.js +30 -0
  98. package/src/organisation-unit-node/index.js +1 -0
  99. package/src/organisation-unit-node/label/disabled-selection-label.js +26 -0
  100. package/src/organisation-unit-node/label/icon-empty.js +31 -0
  101. package/src/organisation-unit-node/label/icon-folder-closed.js +38 -0
  102. package/src/organisation-unit-node/label/icon-folder-open.js +49 -0
  103. package/src/organisation-unit-node/label/icon-single.js +41 -0
  104. package/src/organisation-unit-node/label/icon.js +35 -0
  105. package/src/organisation-unit-node/label/iconized-checkbox.js +67 -0
  106. package/src/organisation-unit-node/label/index.js +1 -0
  107. package/src/organisation-unit-node/label/label-container.js +36 -0
  108. package/src/organisation-unit-node/label/label.js +146 -0
  109. package/src/organisation-unit-node/label/single-selection-label.js +60 -0
  110. package/src/organisation-unit-node/loading-spinner.js +22 -0
  111. package/src/organisation-unit-node/organisation-unit-node-children.js +123 -0
  112. package/src/organisation-unit-node/organisation-unit-node.js +190 -0
  113. package/src/organisation-unit-node/use-open-state.js +37 -0
  114. package/src/organisation-unit-node/use-open-state.test.js +111 -0
  115. package/src/organisation-unit-node/use-org-children.js +63 -0
  116. package/src/organisation-unit-node/use-org-children.test.js +314 -0
  117. package/src/organisation-unit-node/use-org-data/index.js +1 -0
  118. package/src/organisation-unit-node/use-org-data/use-org-data.js +40 -0
  119. package/src/organisation-unit-node/use-org-data/use-org-data.test.js +137 -0
  120. package/src/organisation-unit-tree/default-render-node-label/default-render-node-label.js +1 -0
  121. package/src/organisation-unit-tree/default-render-node-label/index.js +1 -0
  122. package/src/organisation-unit-tree/filter-root-ids.js +9 -0
  123. package/src/organisation-unit-tree/index.js +3 -0
  124. package/src/organisation-unit-tree/organisation-unit-tree-root-error.js +20 -0
  125. package/src/organisation-unit-tree/organisation-unit-tree-root-loading.js +22 -0
  126. package/src/organisation-unit-tree/organisation-unit-tree.js +253 -0
  127. package/src/organisation-unit-tree/organisation-unit-tree.test.js +77 -0
  128. package/src/organisation-unit-tree/use-expanded/create-expand-handlers.js +45 -0
  129. package/src/organisation-unit-tree/use-expanded/create-expand-handlers.test.js +54 -0
  130. package/src/organisation-unit-tree/use-expanded/index.js +1 -0
  131. package/src/organisation-unit-tree/use-expanded/use-expanded.js +42 -0
  132. package/src/organisation-unit-tree/use-expanded/use-expanded.test.js +73 -0
  133. package/src/organisation-unit-tree/use-force-reload.js +22 -0
  134. package/src/organisation-unit-tree/use-force-reload.test.js +43 -0
  135. package/src/organisation-unit-tree/use-root-org-data/index.js +1 -0
  136. package/src/organisation-unit-tree/use-root-org-data/patch-missing-display-name.js +20 -0
  137. package/src/organisation-unit-tree/use-root-org-data/patch-missing-display-name.test.js +24 -0
  138. package/src/organisation-unit-tree/use-root-org-data/use-root-org-data.js +60 -0
  139. package/src/organisation-unit-tree/use-root-org-data/use-root-org-unit.test.js +184 -0
  140. package/src/organisation-unit-tree.e2e.stories.js +28 -0
  141. package/src/organisation-unit-tree.prod.stories.js +70 -0
  142. package/src/prop-types.js +33 -0
@@ -0,0 +1,253 @@
1
+ import { requiredIf } from '@dhis2/prop-types'
2
+ import PropTypes from 'prop-types'
3
+ import React, { useEffect, useState } from 'react'
4
+ import { OrganisationUnitNode } from '../organisation-unit-node/index.js'
5
+ import { orgUnitPathPropType } from '../prop-types.js'
6
+ import { defaultRenderNodeLabel } from './default-render-node-label/index.js'
7
+ import { filterRootIds } from './filter-root-ids.js'
8
+ import { OrganisationUnitTreeRootError } from './organisation-unit-tree-root-error.js'
9
+ import { OrganisationUnitTreeRootLoading } from './organisation-unit-tree-root-loading.js'
10
+ import { useExpanded } from './use-expanded/index.js'
11
+ import { useForceReload } from './use-force-reload.js'
12
+ import { useRootOrgData } from './use-root-org-data/index.js'
13
+
14
+ // A stable object to reference
15
+ const staticArray = []
16
+ const OrganisationUnitTree = ({
17
+ onChange,
18
+ roots,
19
+
20
+ autoExpandLoadingError,
21
+ dataTest = 'dhis2-uiwidgets-orgunittree',
22
+ disableSelection,
23
+ displayProperty = 'displayName',
24
+ forceReload,
25
+ highlighted = staticArray,
26
+ isUserDataViewFallback,
27
+ initiallyExpanded = staticArray,
28
+ filter = staticArray,
29
+ renderNodeLabel = defaultRenderNodeLabel,
30
+ selected = staticArray,
31
+ singleSelection,
32
+ suppressAlphabeticalSorting,
33
+
34
+ expanded: expandedControlled,
35
+ handleExpand: handleExpandControlled,
36
+ handleCollapse: handleCollapseControlled,
37
+
38
+ onExpand,
39
+ onCollapse,
40
+ onChildrenLoaded,
41
+ }) => {
42
+ const rootIds = filterRootIds(
43
+ filter,
44
+ Array.isArray(roots) ? roots : [roots]
45
+ )
46
+ const reloadId = useForceReload(forceReload)
47
+ const [prevReloadId, setPrevReloadId] = useState(reloadId)
48
+ const { called, loading, error, data, refetch } = useRootOrgData(rootIds, {
49
+ isUserDataViewFallback,
50
+ suppressAlphabeticalSorting,
51
+ displayProperty,
52
+ })
53
+
54
+ const { expanded, handleExpand, handleCollapse } = useExpanded({
55
+ initiallyExpanded,
56
+ onExpand,
57
+ onCollapse,
58
+ expandedControlled,
59
+ handleExpandControlled,
60
+ handleCollapseControlled,
61
+ })
62
+
63
+ useEffect(() => {
64
+ // do not refetch on initial render
65
+ if (refetch && reloadId > 0 && reloadId !== prevReloadId) {
66
+ refetch()
67
+ setPrevReloadId(reloadId)
68
+ }
69
+
70
+ return () =>
71
+ console.warn(
72
+ '@TODO: Why does this component unmount after a force reload?'
73
+ )
74
+ }, [reloadId, prevReloadId, refetch])
75
+
76
+ const isLoading = !called || loading
77
+
78
+ return (
79
+ <div data-test={dataTest}>
80
+ {isLoading && <OrganisationUnitTreeRootLoading />}
81
+ {error && <OrganisationUnitTreeRootError error={error} />}
82
+ {!error &&
83
+ !isLoading &&
84
+ rootIds.map((rootId) => {
85
+ const rootNode = data[rootId]
86
+
87
+ return (
88
+ <OrganisationUnitNode
89
+ key={rootNode.path}
90
+ rootId={rootId}
91
+ autoExpandLoadingError={autoExpandLoadingError}
92
+ dataTest={dataTest}
93
+ disableSelection={disableSelection}
94
+ displayName={rootNode.displayName}
95
+ displayProperty={displayProperty}
96
+ expanded={expanded}
97
+ highlighted={highlighted}
98
+ id={rootId}
99
+ isUserDataViewFallback={isUserDataViewFallback}
100
+ filter={filter}
101
+ path={rootNode.path}
102
+ renderNodeLabel={renderNodeLabel}
103
+ selected={selected}
104
+ singleSelection={singleSelection}
105
+ suppressAlphabeticalSorting={
106
+ suppressAlphabeticalSorting
107
+ }
108
+ onChange={onChange}
109
+ onChildrenLoaded={onChildrenLoaded}
110
+ onCollapse={handleCollapse}
111
+ onExpand={handleExpand}
112
+ />
113
+ )
114
+ })}
115
+ </div>
116
+ )
117
+ }
118
+
119
+ OrganisationUnitTree.propTypes = {
120
+ /** Root org unit ID(s) */
121
+ roots: PropTypes.oneOfType([
122
+ PropTypes.string,
123
+ PropTypes.arrayOf(PropTypes.string),
124
+ ]).isRequired,
125
+
126
+ /** Will be called with the following object:
127
+ * `{ id: string, displayName: string, path: string, checked: boolean, selected: string[] }` */
128
+ onChange: PropTypes.func.isRequired,
129
+
130
+ /** When set, the error when loading children fails will be shown automatically */
131
+ autoExpandLoadingError: PropTypes.bool,
132
+
133
+ dataTest: PropTypes.string,
134
+
135
+ /** When set to true, no unit can be selected */
136
+ disableSelection: PropTypes.bool,
137
+
138
+ /**
139
+ * Which field to render as the org unit label. Defaults to `'displayName'`.
140
+ * Set to `'displayShortName'` to honour the `keyAnalysisDisplayProperty`
141
+ * system/user setting. The query renames the chosen field back to
142
+ * `displayName` internally, so consumer-facing data shape is unchanged.
143
+ */
144
+ displayProperty: PropTypes.oneOf(['displayName', 'displayShortName']),
145
+
146
+ expanded: requiredIf(
147
+ (props) => !!props.handleExpand || !!props.handleCollapse,
148
+ PropTypes.arrayOf(PropTypes.string)
149
+ ),
150
+
151
+ /**
152
+ * All organisation units with a path that includes the provided paths will be shown.
153
+ * All others will not be rendered. When not provided, all org units will be shown.
154
+ */
155
+ filter: PropTypes.arrayOf(orgUnitPathPropType),
156
+
157
+ /** When true, everything will be reloaded. In order to load it again after reloading, `forceReload` has to be set to `false` and then to `true` again */
158
+ forceReload: PropTypes.bool,
159
+
160
+ handleCollapse: requiredIf(
161
+ (props) => !!props.expanded || !!props.handleExpand,
162
+ PropTypes.func
163
+ ),
164
+
165
+ handleExpand: requiredIf(
166
+ (props) => !!props.expanded || !!props.handleCollapse,
167
+ PropTypes.func
168
+ ),
169
+
170
+ /**
171
+ * All units provided to "highlighted" as path will be visually
172
+ * highlighted.
173
+ * Note:
174
+ * The d2-ui component used two props for this:
175
+ * * searchResults
176
+ * * highlightSearchResults
177
+ */
178
+ highlighted: PropTypes.arrayOf(orgUnitPathPropType),
179
+
180
+ /**
181
+ * An array of OU paths that will be expanded automatically
182
+ * as soon as they are encountered.
183
+ * The path of an OU is the UIDs of the OU
184
+ * and all its parent OUs separated by slashes (/)
185
+ * Note: This replaces "openFirstLevel" as that's redundant
186
+ */
187
+ initiallyExpanded: PropTypes.arrayOf(orgUnitPathPropType),
188
+
189
+ /** When provided, the 'isUserDataViewFallback' option will be sent when requesting the org units */
190
+ isUserDataViewFallback: PropTypes.bool,
191
+
192
+ /** Renders the actual node component for each leaf, can be used to
193
+ * customize the node. The default function just returns the node's
194
+ * displayName
195
+ *
196
+ * Shape of the object passed to the callback:
197
+ * ```
198
+ * {
199
+ * label: string,
200
+ * node: {
201
+ * displayName: string,
202
+ * id: string,
203
+ * // Only provided once `loading` is false
204
+ * path?: string,
205
+ * // Only provided once `loading` is false
206
+ * children?: Array.<{
207
+ * id: string,
208
+ * path: string,
209
+ * displayName: string
210
+ * }>
211
+ * },
212
+ * loading: boolean,
213
+ * error: string,
214
+ * open: boolean,
215
+ * selected: string[],
216
+ * singleSelection: boolean,
217
+ * disableSelection: boolean,
218
+ * }
219
+ * ``` */
220
+ renderNodeLabel: PropTypes.func,
221
+
222
+ /** An array of paths of selected OUs. The path of an OU is the UIDs of the OU and all its parent OUs separated by slashes (`/`) */
223
+ selected: PropTypes.arrayOf(orgUnitPathPropType),
224
+
225
+ /** When set, no checkboxes will be displayed and only the first selected path in `selected` will be highlighted */
226
+ singleSelection: PropTypes.bool,
227
+
228
+ /** Turns off alphabetical sorting of units */
229
+ suppressAlphabeticalSorting: PropTypes.bool,
230
+
231
+ /** Called with the children's data that was loaded */
232
+ onChildrenLoaded: PropTypes.func,
233
+
234
+ /** Called with `{ path: string }` with the path of the parent of the level closed */
235
+ onCollapse: PropTypes.func,
236
+
237
+ /** Called with `{ path: string }` with the path of the parent of the level opened */
238
+ onExpand: PropTypes.func,
239
+
240
+ /**
241
+ * All units with ids (not paths!) provided
242
+ * to "idsThatShouldBeReloaded" will be reloaded
243
+ * In order to reload an id twice, the array must be changed
244
+ * while keeping the id to reload in the array
245
+ *
246
+ * NOTE: This is currently not working due to a limitation
247
+ * of the data engine (we can't force specific resource to reload,
248
+ * we'd have to reload the sibling nodes currently as well)
249
+ */
250
+ //idsThatShouldBeReloaded: propTypes.arrayOf(orgUnitIdPropType),
251
+ }
252
+
253
+ export { OrganisationUnitTree }
@@ -0,0 +1,77 @@
1
+ import { CustomDataProvider } from '@dhis2/app-runtime'
2
+ import { shallow } from 'enzyme'
3
+ import React from 'react'
4
+ import { OrganisationUnitTree } from './organisation-unit-tree.js'
5
+
6
+ describe('OrganisationUnitTree', () => {
7
+ const origError = console.error.bind(console)
8
+ const errorMock = jest.fn()
9
+
10
+ beforeEach(() => {
11
+ console.error = errorMock
12
+ })
13
+
14
+ afterEach(() => {
15
+ console.error = origError
16
+ errorMock.mockClear()
17
+ })
18
+
19
+ describe('Controlled expanded props', () => {
20
+ describe('Missing props', () => {
21
+ it('should throw a prop-types error when "handleCollapse" is missing', () => {
22
+ shallow(
23
+ <CustomDataProvider data={{}}>
24
+ <OrganisationUnitTree
25
+ roots="/A001"
26
+ expanded={[]}
27
+ onChange={() => {}}
28
+ handleExpand={() => {}}
29
+ />
30
+ </CustomDataProvider>
31
+ )
32
+
33
+ expect(errorMock).toHaveBeenCalledTimes(1)
34
+ expect(errorMock.mock.calls[0][2]).toMatch(
35
+ /Invalid prop `handleCollapse` supplied to `OrganisationUnitTree`/,
36
+ {}
37
+ )
38
+ })
39
+
40
+ it('should throw a prop-types error when "handleExpand" is missing', () => {
41
+ shallow(
42
+ <CustomDataProvider data={{}}>
43
+ <OrganisationUnitTree
44
+ roots="/A001"
45
+ expanded={[]}
46
+ onChange={() => {}}
47
+ handleCollapse={() => {}}
48
+ />
49
+ </CustomDataProvider>
50
+ )
51
+
52
+ expect(errorMock).toHaveBeenCalledTimes(1)
53
+ expect(errorMock.mock.calls[0][2]).toMatch(
54
+ /Invalid prop `handleExpand` supplied to `OrganisationUnitTree`/
55
+ )
56
+ })
57
+
58
+ it('should throw a prop-types error when "expanded" is missing', () => {
59
+ shallow(
60
+ <CustomDataProvider data={{}}>
61
+ <OrganisationUnitTree
62
+ roots="/A001"
63
+ onChange={() => {}}
64
+ handleCollapse={() => {}}
65
+ handleExpand={() => {}}
66
+ />
67
+ </CustomDataProvider>
68
+ )
69
+
70
+ expect(errorMock).toHaveBeenCalledTimes(1)
71
+ expect(errorMock.mock.calls[0][2]).toMatch(
72
+ 'Invalid prop `expanded` supplied to `OrganisationUnitTree`, this prop is conditionally required but has value `undefined`. The condition that made this prop required is: `props => !!props.handleExpand || !!props.handleCollapse`.'
73
+ )
74
+ })
75
+ })
76
+ })
77
+ })
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @param {Object} args
3
+ * @param {string[]} args.expanded
4
+ * @param {Function} args.setExpanded
5
+ * @param {Function} [args.onExpand]
6
+ * @param {Function} [args.onCollapse]
7
+ * @returns {{ handleExpand: Function, handleCollapse: Function }}
8
+ */
9
+ export const createExpandHandlers = ({
10
+ expanded,
11
+ setExpanded,
12
+ onExpand,
13
+ onCollapse,
14
+ }) => {
15
+ const handleExpand = ({ path, ...rest }) => {
16
+ if (!expanded.includes(path)) {
17
+ setExpanded([...expanded, path])
18
+
19
+ if (onExpand) {
20
+ onExpand({ path, ...rest })
21
+ }
22
+ }
23
+ }
24
+
25
+ const handleCollapse = ({ path, ...rest }) => {
26
+ const pathIndex = expanded.indexOf(path)
27
+ if (pathIndex !== -1) {
28
+ const updatedExpanded =
29
+ pathIndex === 0
30
+ ? expanded.slice(1)
31
+ : [
32
+ ...expanded.slice(0, pathIndex),
33
+ ...expanded.slice(pathIndex + 1),
34
+ ]
35
+
36
+ setExpanded(updatedExpanded)
37
+
38
+ if (onCollapse) {
39
+ onCollapse({ path, ...rest })
40
+ }
41
+ }
42
+ }
43
+
44
+ return { handleExpand, handleCollapse }
45
+ }
@@ -0,0 +1,54 @@
1
+ import { createExpandHandlers } from './create-expand-handlers.js'
2
+
3
+ describe('OrganisationUnitTree - useExpanded - createExpandHandlers', () => {
4
+ const initiallyExpanded = ['/foo/bar/baz', '/foobar/barbaz/bazfoo']
5
+ const onExpand = jest.fn()
6
+ const onCollapse = jest.fn()
7
+
8
+ afterEach(() => {
9
+ onExpand.mockClear()
10
+ onCollapse.mockClear()
11
+ })
12
+
13
+ it('should add a path to the expanded paths when calling handleExpand when not present yet', () => {
14
+ const setExpanded = jest.fn()
15
+ const expanded = initiallyExpanded
16
+ const { handleExpand } = createExpandHandlers({ expanded, setExpanded })
17
+ handleExpand({ path: '/a/new/path' })
18
+
19
+ expect(setExpanded).toHaveBeenCalledWith([...expanded, '/a/new/path'])
20
+ })
21
+
22
+ it('should not add a path to the expanded paths when calling handleExpand when already present', () => {
23
+ const setExpanded = jest.fn()
24
+ const expanded = [...initiallyExpanded, '/a/new/path']
25
+ const { handleExpand } = createExpandHandlers({ expanded, setExpanded })
26
+ handleExpand({ path: '/a/new/path' })
27
+
28
+ expect(setExpanded).toHaveBeenCalledTimes(0)
29
+ })
30
+
31
+ it('should remove a path to the expanded paths when calling handleExpand when path is expanded', () => {
32
+ const setExpanded = jest.fn()
33
+ const expanded = [...initiallyExpanded, '/a/new/path']
34
+ const { handleCollapse } = createExpandHandlers({
35
+ expanded,
36
+ setExpanded,
37
+ })
38
+ handleCollapse({ path: '/a/new/path' })
39
+
40
+ expect(setExpanded).toHaveBeenCalledWith(initiallyExpanded)
41
+ })
42
+
43
+ it('should not remove a path to the expanded paths when calling handleExpand when path is not expanded', () => {
44
+ const setExpanded = jest.fn()
45
+ const expanded = initiallyExpanded
46
+ const { handleCollapse } = createExpandHandlers({
47
+ expanded,
48
+ setExpanded,
49
+ })
50
+ handleCollapse({ path: '/a/new/path' })
51
+
52
+ expect(setExpanded).toHaveBeenCalledTimes(0)
53
+ })
54
+ })
@@ -0,0 +1 @@
1
+ export { useExpanded } from './use-expanded.js'
@@ -0,0 +1,42 @@
1
+ import { useState } from 'react'
2
+ import { getAllExpandedPaths } from '../../get-all-expanded-paths/index.js'
3
+ import { createExpandHandlers } from './create-expand-handlers.js'
4
+
5
+ /**
6
+ * @param {string[]} initiallyExpanded
7
+ * @param {Function} [onExpand]
8
+ * @param {Function} [onCollapse]
9
+ * @returns {{ expanded: string[], handleExpand: Function, handleCollapse: Function }}
10
+ */
11
+ export const useExpanded = ({
12
+ initiallyExpanded,
13
+ onExpand,
14
+ onCollapse,
15
+ expandedControlled,
16
+ handleExpandControlled,
17
+ handleCollapseControlled,
18
+ }) => {
19
+ const isControlled = !!expandedControlled
20
+ const allInitiallyExpandedPaths = isControlled
21
+ ? []
22
+ : getAllExpandedPaths(initiallyExpanded)
23
+
24
+ const [expanded, setExpanded] = useState(allInitiallyExpandedPaths)
25
+
26
+ if (isControlled) {
27
+ return {
28
+ expanded: expandedControlled,
29
+ handleExpand: handleExpandControlled,
30
+ handleCollapse: handleCollapseControlled,
31
+ }
32
+ }
33
+
34
+ const { handleExpand, handleCollapse } = createExpandHandlers({
35
+ expanded,
36
+ setExpanded,
37
+ onExpand,
38
+ onCollapse,
39
+ })
40
+
41
+ return { expanded, handleExpand, handleCollapse }
42
+ }
@@ -0,0 +1,73 @@
1
+ import { useState } from 'react'
2
+ import { getAllExpandedPaths } from '../../get-all-expanded-paths/index.js'
3
+ import { createExpandHandlers } from './create-expand-handlers.js'
4
+ import { useExpanded } from './use-expanded.js'
5
+
6
+ jest.mock('react', () => ({
7
+ useState: jest.fn((initialValue) => [initialValue, () => null]),
8
+ }))
9
+
10
+ jest.mock('../../get-all-expanded-paths/index.js', () => ({
11
+ getAllExpandedPaths: jest.fn((input) => input),
12
+ }))
13
+
14
+ jest.mock('./create-expand-handlers.js', () => ({
15
+ createExpandHandlers: jest.fn(() => ({
16
+ handleCollapse: () => null,
17
+ handleExpand: () => null,
18
+ })),
19
+ }))
20
+
21
+ describe('OrganisationUnitTree - useExpanded hook', () => {
22
+ const onExpand = jest.fn()
23
+ const onCollapse = jest.fn()
24
+
25
+ it('should use the getAllExpandedPaths helper to determine the initial state', () => {
26
+ getAllExpandedPaths.mockImplementationOnce((input) => [
27
+ ...input,
28
+ '/foo/bar/baz',
29
+ ])
30
+
31
+ const expected = ['/foo', '/foo/bar', '/foo/bar/baz']
32
+ const { expanded: actual } = useExpanded({
33
+ initiallyExpanded: ['/foo', '/foo/bar'],
34
+ onExpand,
35
+ onCollapse,
36
+ })
37
+
38
+ expect(actual).toEqual(expected)
39
+ })
40
+
41
+ it('should pass the setExpanded function from seState to createExpandHandlers', () => {
42
+ const setExpanded = jest.fn()
43
+ useState.mockImplementationOnce(() => [[], setExpanded])
44
+
45
+ useExpanded({
46
+ initiallyExpanded: [],
47
+ onExpand,
48
+ onCollapse,
49
+ })
50
+ expect(createExpandHandlers).toHaveBeenCalledWith(
51
+ expect.objectContaining({ setExpanded })
52
+ )
53
+ })
54
+
55
+ it('should return the controlled values if all of them are being provided', () => {
56
+ const expandedControlled = ['/foo']
57
+ const handleExpandControlled = jest.fn()
58
+ const handleCollapseControlled = jest.fn()
59
+
60
+ const { expanded, handleExpand, handleCollapse } = useExpanded({
61
+ initiallyExpanded: [],
62
+ onExpand,
63
+ onCollapse,
64
+ expandedControlled,
65
+ handleExpandControlled,
66
+ handleCollapseControlled,
67
+ })
68
+
69
+ expect(expanded).toBe(expandedControlled)
70
+ expect(handleExpand).toBe(handleExpandControlled)
71
+ expect(handleCollapse).toBe(handleCollapseControlled)
72
+ })
73
+ })
@@ -0,0 +1,22 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ /**
4
+ * This will create a new reloadId everytime "forceReload" changes to true,
5
+ * which can be used as the "key" prop on the org unit tree.
6
+ * When that id changes, the whole tree rerenders
7
+ * and therefore triggers all "useDataQuery"s to
8
+ * run the query again
9
+ *
10
+ * @param {bool} forceReload
11
+ * @returns {Int}
12
+ */
13
+ export const useForceReload = (forceReload) => {
14
+ const [reloadId, setReloadId] = useState(0)
15
+
16
+ useEffect(() => {
17
+ const nextReloadId = reloadId + 1
18
+ forceReload === true && setReloadId(nextReloadId)
19
+ }, [forceReload])
20
+
21
+ return reloadId
22
+ }
@@ -0,0 +1,43 @@
1
+ import { useEffect, useState } from 'react'
2
+ import { useForceReload } from './use-force-reload.js'
3
+
4
+ jest.mock('react', () => ({
5
+ useEffect: jest.fn((callback) => callback()),
6
+ useState: jest.fn((iV) => [iV, () => null]),
7
+ }))
8
+
9
+ describe('OrganisationUnitTree - useForceReload', () => {
10
+ afterEach(() => {
11
+ useEffect.mockClear()
12
+ })
13
+
14
+ it('should return an reloadIf of 0 when forceReload is false', () => {
15
+ const expected = 0
16
+ const actual = useForceReload(false)
17
+
18
+ expect(actual).toBe(expected)
19
+ })
20
+
21
+ it('should increase the default reloadId when forceReload is true', () => {
22
+ useState.mockImplementationOnce((initialValue) => [
23
+ initialValue,
24
+ setReloadId,
25
+ ])
26
+
27
+ const setReloadId = jest.fn()
28
+ const expected = 1
29
+
30
+ useForceReload(true)
31
+
32
+ expect(setReloadId).toHaveBeenCalledWith(expected)
33
+ })
34
+
35
+ it('should pass forceReload as only item as the second argument to useEffect', () => {
36
+ const forceReload = true
37
+ useForceReload(forceReload)
38
+
39
+ const useEffectDependencies = useEffect.mock.calls[0][1]
40
+
41
+ expect(useEffectDependencies).toEqual([forceReload])
42
+ })
43
+ })
@@ -0,0 +1 @@
1
+ export { useRootOrgData } from './use-root-org-data.js'
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Note by JGS: I can't recall why this is necessary,
3
+ * but it's there.. So I guess it's better to leave it in for now
4
+ * and investigate why this is necessary in the first place!
5
+ * Maybe we can omit this completely and remove the state from
6
+ * the useRootOrgData hook entirely
7
+ * @TODO: Investigate if this could be removed
8
+ *
9
+ * @param {Object[]} nodes
10
+ * @returns {}
11
+ */
12
+ export const patchMissingDisplayName = (nodes) => {
13
+ const nodeEntries = Object.entries(nodes)
14
+ const nodesWithDisplayName = nodeEntries.map(([id, node]) => {
15
+ const displayName = node.displayName || ''
16
+ return [id, { ...node, displayName }]
17
+ })
18
+
19
+ return Object.fromEntries(nodesWithDisplayName)
20
+ }
@@ -0,0 +1,24 @@
1
+ import { patchMissingDisplayName } from './patch-missing-display-name.js'
2
+
3
+ describe('patchMissingDisplayName', () => {
4
+ it('should add an empty string as displayName if the prop is falsy', () => {
5
+ const nodes = {
6
+ id1: { noDisplayName: 'foo' },
7
+ id2: { displayName: null },
8
+ id3: { displayName: false },
9
+ id4: { displayName: undefined },
10
+ id5: { displayName: 'Should stay the same' },
11
+ }
12
+
13
+ const actual = patchMissingDisplayName(nodes)
14
+ const expected = {
15
+ id1: { noDisplayName: 'foo', displayName: '' },
16
+ id2: { displayName: '' },
17
+ id3: { displayName: '' },
18
+ id4: { displayName: '' },
19
+ id5: { displayName: 'Should stay the same' },
20
+ }
21
+
22
+ expect(actual).toEqual(expected)
23
+ })
24
+ })