@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.
- package/package.json +8 -7
- package/src/__e2e__/children_as_child_nodes.js +23 -0
- package/src/__e2e__/common.js +70 -0
- package/src/__e2e__/controlled_expanded.js +89 -0
- package/src/__e2e__/displaying_loading_error.js +45 -0
- package/src/__e2e__/expanded.js +42 -0
- package/src/__e2e__/force_reload.js +66 -0
- package/src/__e2e__/get-organisation-unit-data.js +119 -0
- package/src/__e2e__/highlight.js +23 -0
- package/src/__e2e__/loading_state.js +37 -0
- package/src/__e2e__/multi_selection.js +24 -0
- package/src/__e2e__/namespace.js +1 -0
- package/src/__e2e__/no_selection.js +32 -0
- package/src/__e2e__/path_based_filtering.js +49 -0
- package/src/__e2e__/single_selection.js +46 -0
- package/src/__e2e__/sub_unit_as_root.js +28 -0
- package/src/__e2e__/tree_api.js +55 -0
- package/src/__stories__/collapsed.js +11 -0
- package/src/__stories__/custom-expanded-imperative-open.js +181 -0
- package/src/__stories__/custom-node-label.js +19 -0
- package/src/__stories__/development-stories.js +86 -0
- package/src/__stories__/expanded.js +12 -0
- package/src/__stories__/filtered-root.js +15 -0
- package/src/__stories__/filtered.js +13 -0
- package/src/__stories__/force-reload-all.js +46 -0
- package/src/__stories__/force-reload-one-unit.js +36 -0
- package/src/__stories__/highlighted.js +13 -0
- package/src/__stories__/indeterminate.js +13 -0
- package/src/__stories__/loading-error-grandchild.js +39 -0
- package/src/__stories__/loading.js +27 -0
- package/src/__stories__/multiple-roots.js +20 -0
- package/src/__stories__/no-selection.js +16 -0
- package/src/__stories__/replace-roots.js +28 -0
- package/src/__stories__/root-error.js +36 -0
- package/src/__stories__/root-loading.js +34 -0
- package/src/__stories__/rtl.js +14 -0
- package/src/__stories__/selected-multiple.js +18 -0
- package/src/__stories__/shared.js +192 -0
- package/src/__stories__/single-selection.js +16 -0
- package/src/features/children_as_child_nodes/index.js +29 -0
- package/src/features/children_as_child_nodes.feature +6 -0
- package/src/features/controlled_expanded/index.js +86 -0
- package/src/features/controlled_expanded.feature +11 -0
- package/src/features/displaying_loading_error/index.js +46 -0
- package/src/features/displaying_loading_error.feature +24 -0
- package/src/features/expanded/index.js +87 -0
- package/src/features/expanded.feature +27 -0
- package/src/features/force_reload/index.js +36 -0
- package/src/features/force_reload.feature +7 -0
- package/src/features/highlight/index.js +9 -0
- package/src/features/highlight.feature +5 -0
- package/src/features/loading_state/index.js +26 -0
- package/src/features/loading_state.feature +7 -0
- package/src/features/multi_selection/index.js +94 -0
- package/src/features/multi_selection.feature +31 -0
- package/src/features/no_selection/index.js +41 -0
- package/src/features/no_selection.feature +13 -0
- package/src/features/path_based_filtering/index.js +97 -0
- package/src/features/path_based_filtering.feature +24 -0
- package/src/features/single_selection/index.js +41 -0
- package/src/features/single_selection.feature +20 -0
- package/src/features/sub_unit_as_root/index.js +83 -0
- package/src/features/sub_unit_as_root.feature +34 -0
- package/src/features/tree_api/index.js +121 -0
- package/src/features/tree_api.feature +37 -0
- package/src/get-all-expanded-paths/get-all-expanded-paths.js +32 -0
- package/src/get-all-expanded-paths/get-all-expanded-paths.test.js +22 -0
- package/src/get-all-expanded-paths/index.js +1 -0
- package/src/helpers/index.js +3 -0
- package/src/helpers/is-path-included.js +15 -0
- package/src/helpers/left-trim-to-root-id.js +3 -0
- package/src/helpers/sort-node-children-alphabetically.js +5 -0
- package/src/index.js +6 -0
- package/src/locales/ar/translations.json +5 -0
- package/src/locales/cs/translations.json +5 -0
- package/src/locales/en/translations.json +5 -0
- package/src/locales/es/translations.json +5 -0
- package/src/locales/es_419/translations.json +5 -0
- package/src/locales/fr/translations.json +5 -0
- package/src/locales/index.js +50 -0
- package/src/locales/lo/translations.json +5 -0
- package/src/locales/nb/translations.json +5 -0
- package/src/locales/nl/translations.json +5 -0
- package/src/locales/pt/translations.json +5 -0
- package/src/locales/ru/translations.json +5 -0
- package/src/locales/uk/translations.json +5 -0
- package/src/locales/uz_Latn/translations.json +5 -0
- package/src/locales/uz_UZ_Cyrl/translations.json +5 -0
- package/src/locales/uz_UZ_Latn/translations.json +5 -0
- package/src/locales/vi/translations.json +5 -0
- package/src/locales/zh/translations.json +5 -0
- package/src/locales/zh_CN/translations.json +5 -0
- package/src/organisation-unit-node/compute-child-nodes.js +27 -0
- package/src/organisation-unit-node/compute-child-nodes.test.js +85 -0
- package/src/organisation-unit-node/error-message.js +23 -0
- package/src/organisation-unit-node/has-descendant-selected-paths.js +15 -0
- package/src/organisation-unit-node/has-descendant-selected-paths.test.js +30 -0
- package/src/organisation-unit-node/index.js +1 -0
- package/src/organisation-unit-node/label/disabled-selection-label.js +26 -0
- package/src/organisation-unit-node/label/icon-empty.js +31 -0
- package/src/organisation-unit-node/label/icon-folder-closed.js +38 -0
- package/src/organisation-unit-node/label/icon-folder-open.js +49 -0
- package/src/organisation-unit-node/label/icon-single.js +41 -0
- package/src/organisation-unit-node/label/icon.js +35 -0
- package/src/organisation-unit-node/label/iconized-checkbox.js +67 -0
- package/src/organisation-unit-node/label/index.js +1 -0
- package/src/organisation-unit-node/label/label-container.js +36 -0
- package/src/organisation-unit-node/label/label.js +146 -0
- package/src/organisation-unit-node/label/single-selection-label.js +60 -0
- package/src/organisation-unit-node/loading-spinner.js +22 -0
- package/src/organisation-unit-node/organisation-unit-node-children.js +123 -0
- package/src/organisation-unit-node/organisation-unit-node.js +190 -0
- package/src/organisation-unit-node/use-open-state.js +37 -0
- package/src/organisation-unit-node/use-open-state.test.js +111 -0
- package/src/organisation-unit-node/use-org-children.js +63 -0
- package/src/organisation-unit-node/use-org-children.test.js +314 -0
- package/src/organisation-unit-node/use-org-data/index.js +1 -0
- package/src/organisation-unit-node/use-org-data/use-org-data.js +40 -0
- package/src/organisation-unit-node/use-org-data/use-org-data.test.js +137 -0
- package/src/organisation-unit-tree/default-render-node-label/default-render-node-label.js +1 -0
- package/src/organisation-unit-tree/default-render-node-label/index.js +1 -0
- package/src/organisation-unit-tree/filter-root-ids.js +9 -0
- package/src/organisation-unit-tree/index.js +3 -0
- package/src/organisation-unit-tree/organisation-unit-tree-root-error.js +20 -0
- package/src/organisation-unit-tree/organisation-unit-tree-root-loading.js +22 -0
- package/src/organisation-unit-tree/organisation-unit-tree.js +253 -0
- package/src/organisation-unit-tree/organisation-unit-tree.test.js +77 -0
- package/src/organisation-unit-tree/use-expanded/create-expand-handlers.js +45 -0
- package/src/organisation-unit-tree/use-expanded/create-expand-handlers.test.js +54 -0
- package/src/organisation-unit-tree/use-expanded/index.js +1 -0
- package/src/organisation-unit-tree/use-expanded/use-expanded.js +42 -0
- package/src/organisation-unit-tree/use-expanded/use-expanded.test.js +73 -0
- package/src/organisation-unit-tree/use-force-reload.js +22 -0
- package/src/organisation-unit-tree/use-force-reload.test.js +43 -0
- package/src/organisation-unit-tree/use-root-org-data/index.js +1 -0
- package/src/organisation-unit-tree/use-root-org-data/patch-missing-display-name.js +20 -0
- package/src/organisation-unit-tree/use-root-org-data/patch-missing-display-name.test.js +24 -0
- package/src/organisation-unit-tree/use-root-org-data/use-root-org-data.js +60 -0
- package/src/organisation-unit-tree/use-root-org-data/use-root-org-unit.test.js +184 -0
- package/src/organisation-unit-tree.e2e.stories.js +28 -0
- package/src/organisation-unit-tree.prod.stories.js +70 -0
- 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
|
+
})
|