@dhis2-ui/organisation-unit-tree 10.16.2 → 10.16.3

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,60 @@
1
+ import { useDataQuery } from '@dhis2/app-runtime'
2
+ import { useMemo } from 'react'
3
+ import { patchMissingDisplayName } from './patch-missing-display-name.js'
4
+
5
+ export const createRootQuery = (ids, displayProperty) =>
6
+ ids.reduce(
7
+ (query, id) => ({
8
+ ...query,
9
+ [id]: {
10
+ id,
11
+ resource: `organisationUnits`,
12
+ params: ({ isUserDataViewFallback }) => ({
13
+ isUserDataViewFallback,
14
+ fields: [
15
+ displayProperty === 'displayName'
16
+ ? 'displayName'
17
+ : `${displayProperty}~rename(displayName)`,
18
+ 'path',
19
+ 'id',
20
+ ],
21
+ }),
22
+ },
23
+ }),
24
+ {}
25
+ )
26
+
27
+ /**
28
+ * @param {string[]} ids
29
+ * @param {Object} [options]
30
+ * @param {boolean} [options.withChildren]
31
+ * @param {boolean} [options.isUserDataViewFallback]
32
+ * @param {'displayName'|'displayShortName'} [options.displayProperty]
33
+ * @returns {Object}
34
+ */
35
+ export const useRootOrgData = (
36
+ ids,
37
+ { isUserDataViewFallback, displayProperty = 'displayName' } = {}
38
+ ) => {
39
+ const query = useMemo(
40
+ () => createRootQuery(ids, displayProperty),
41
+ [ids, displayProperty]
42
+ )
43
+ const variables = { isUserDataViewFallback }
44
+ const rootOrgUnits = useDataQuery(query, {
45
+ variables,
46
+ })
47
+ const { called, loading, error, data, refetch } = rootOrgUnits
48
+
49
+ const patchedData = useMemo(() => {
50
+ return data ? patchMissingDisplayName(data) : data
51
+ }, [data])
52
+
53
+ return {
54
+ called,
55
+ loading,
56
+ error: error || null,
57
+ data: patchedData || null,
58
+ refetch,
59
+ }
60
+ }
@@ -0,0 +1,184 @@
1
+ import { CustomDataProvider } from '@dhis2/app-runtime'
2
+ import { renderHook, waitFor } from '@testing-library/react'
3
+ import React from 'react'
4
+ import { useRootOrgData } from './use-root-org-data.js'
5
+
6
+ describe('OrganisationUnitTree - useRootOrgData', () => {
7
+ // @TODO: Figure out why this is necessary at all...
8
+ const origError = console.error
9
+ jest.spyOn(console, 'error').mockImplementation((...args) => {
10
+ const [err] = args
11
+
12
+ if (!err.toString().match(/^Warning: An update to/)) {
13
+ origError(...args)
14
+ }
15
+ })
16
+
17
+ // @TODO: This is kind of necessary; no idea if we can get rid of this
18
+ const origWarn = console.warn
19
+ const dynamicQueryWarningMsg =
20
+ "The query should be static, don't create it within the render loop!"
21
+ jest.spyOn(console, 'warn').mockImplementation((...args) => {
22
+ const [err] = args
23
+
24
+ if (!err.toString().match(dynamicQueryWarningMsg)) {
25
+ origWarn(...args)
26
+ }
27
+ })
28
+
29
+ afterAll(() => {
30
+ console.error.mockRestore()
31
+ console.warn.mockRestore()
32
+ })
33
+
34
+ const dataProviderData = {
35
+ organisationUnits: jest.fn(() => {
36
+ return {
37
+ id: 'A0000000000',
38
+ path: '/A0000000000',
39
+ displayName: 'Org Unit 1',
40
+ }
41
+ }),
42
+ }
43
+
44
+ const wrapper = ({ children }) => (
45
+ <CustomDataProvider data={dataProviderData}>
46
+ {children}
47
+ </CustomDataProvider>
48
+ )
49
+
50
+ it('should respond with `loading: false`, `error: null` and `data: null` initially', () => {
51
+ const { result } = renderHook(() => useRootOrgData(['A0000000000']), {
52
+ wrapper,
53
+ })
54
+
55
+ expect(result.current).toEqual(
56
+ expect.objectContaining({
57
+ loading: true,
58
+ error: null,
59
+ data: null,
60
+ })
61
+ )
62
+ expect(result.current.refetch).toBeInstanceOf(Function)
63
+ })
64
+
65
+ it('should provide the org unit data', async () => {
66
+ const { result } = renderHook(() => useRootOrgData(['A0000000000']), {
67
+ wrapper,
68
+ })
69
+
70
+ await waitFor(() => {})
71
+
72
+ expect(result.current).toEqual(
73
+ expect.objectContaining({
74
+ loading: false,
75
+ error: null,
76
+ data: {
77
+ A0000000000: {
78
+ id: 'A0000000000',
79
+ path: '/A0000000000',
80
+ displayName: 'Org Unit 1',
81
+ },
82
+ },
83
+ })
84
+ )
85
+ })
86
+
87
+ it('should provide the error', async () => {
88
+ const errorWrapper = ({ children }) => (
89
+ <CustomDataProvider
90
+ data={{
91
+ organisationUnits: async () => {
92
+ throw new Error('Error message')
93
+ },
94
+ }}
95
+ >
96
+ {children}
97
+ </CustomDataProvider>
98
+ )
99
+
100
+ const { result } = renderHook(() => useRootOrgData(['A0000000000']), {
101
+ wrapper: errorWrapper,
102
+ })
103
+
104
+ await waitFor(() => {
105
+ expect(result.current).toEqual(
106
+ expect.objectContaining({
107
+ loading: false,
108
+ error: new Error('Error message'),
109
+ data: null,
110
+ })
111
+ )
112
+ })
113
+ })
114
+
115
+ it('should send the "isUserDataViewFallback" parameter with value "undefined"', async () => {
116
+ renderHook(() => useRootOrgData(['A0000000000']), { wrapper })
117
+
118
+ await waitFor(() => {})
119
+
120
+ expect(dataProviderData.organisationUnits).toHaveBeenCalledWith(
121
+ 'read',
122
+ expect.objectContaining({
123
+ params: expect.objectContaining({
124
+ isUserDataViewFallback: undefined,
125
+ }),
126
+ }),
127
+ expect.objectContaining({}) // contains the `signal`
128
+ )
129
+ })
130
+
131
+ it('should send the "isUserDataViewFallback" parameter with value "true"', async () => {
132
+ const options = { isUserDataViewFallback: true }
133
+ renderHook(() => useRootOrgData(['A0000000000'], options), { wrapper })
134
+
135
+ await waitFor(() => {})
136
+
137
+ expect(dataProviderData.organisationUnits).toHaveBeenCalledWith(
138
+ 'read',
139
+ expect.objectContaining({
140
+ params: expect.objectContaining({
141
+ isUserDataViewFallback: true,
142
+ }),
143
+ }),
144
+ expect.objectContaining({}) // contains the `signal`
145
+ )
146
+ })
147
+
148
+ it('should patch the display name if it is missing', async () => {
149
+ const dataProviderDataWithoutDisplayName = {
150
+ organisationUnits: jest.fn(() => {
151
+ return {
152
+ id: 'A0000000000',
153
+ path: '/A0000000000',
154
+ }
155
+ }),
156
+ }
157
+
158
+ const wrapperWithoutDisplayName = ({ children }) => (
159
+ <CustomDataProvider data={dataProviderDataWithoutDisplayName}>
160
+ {children}
161
+ </CustomDataProvider>
162
+ )
163
+
164
+ const { result } = renderHook(() => useRootOrgData(['A0000000000']), {
165
+ wrapper: wrapperWithoutDisplayName,
166
+ })
167
+
168
+ await waitFor(() => {
169
+ expect(result.current).toEqual(
170
+ expect.objectContaining({
171
+ loading: false,
172
+ error: null,
173
+ data: {
174
+ A0000000000: {
175
+ id: 'A0000000000',
176
+ path: '/A0000000000',
177
+ displayName: '',
178
+ },
179
+ },
180
+ })
181
+ )
182
+ })
183
+ })
184
+ })
@@ -0,0 +1,28 @@
1
+ export default { title: 'OrganisationUnitTree' }
2
+
3
+ export { ClosedWithChildren } from './__e2e__/children_as_child_nodes.js'
4
+ export { Controlled, MissingProps } from './__e2e__/controlled_expanded.js'
5
+ export {
6
+ A0000000001LoadingError,
7
+ A0000000001LoadingErrorAutoexpand,
8
+ } from './__e2e__/displaying_loading_error.js'
9
+ export {
10
+ NoInitiallyExpandedPaths,
11
+ InitiallyExpandedPaths,
12
+ WithRootMainAndRootSubOrgUnit,
13
+ } from './__e2e__/expanded.js'
14
+ export { ForceReloading } from './__e2e__/force_reload.js'
15
+ export { RootHighlighted } from './__e2e__/highlight.js'
16
+ export { A0000000001Loading } from './__e2e__/loading_state.js'
17
+ export { MultipleSelection } from './__e2e__/multi_selection.js'
18
+ export {
19
+ NoSelectionClosed,
20
+ NoSelectionRootOpened,
21
+ } from './__e2e__/no_selection.js'
22
+ export {
23
+ FilteredBy3LevelPath,
24
+ FilteredBy3LevelPathAnd2LevelPath,
25
+ } from './__e2e__/path_based_filtering.js'
26
+ export { SingleSelection } from './__e2e__/single_selection.js'
27
+ export { MultipleRootSubAndOneMainOrgUnit } from './__e2e__/sub_unit_as_root.js'
28
+ export { Events } from './__e2e__/tree_api.js'
@@ -0,0 +1,70 @@
1
+ import { CustomDataProvider } from '@dhis2/app-runtime'
2
+ import React from 'react'
3
+ import { customData } from './__stories__/shared.js'
4
+ import { OrganisationUnitTree } from './index.js'
5
+
6
+ const subtitle =
7
+ 'Display, manipulate and select organization units displayed in a hierarchical tree'
8
+
9
+ const description = `
10
+ This is a complex, controlled component. It needs access to an App Runtime data provider to fetch org unit data.
11
+
12
+ Several props require arrays of org. unit paths (referred to as \`orgUnitPathPropType[]\` in the table below). Take a look at the \`initiallyExpanded\` and \`filter\` props in the example to see an example of the paths format.
13
+
14
+ Example:
15
+
16
+ \`\`\`js
17
+ import { OrganisationUnitTree } from '@dhis2/ui'
18
+
19
+ const orgUnitTree = (
20
+ <OrganisationUnitTree
21
+ name="Root org unit"
22
+ roots="A0000000000"
23
+ onChange={onChange}
24
+ onExpand={onExpand}
25
+ onCollapse={onCollapse}
26
+ onChildrenLoaded={onChildrenLoaded}
27
+ // Notice the format of the org unit paths
28
+ initiallyExpanded={['/A0000000000/A0000000001']}
29
+ filter={['/A0000000000/A0000000001/A0000000003']}
30
+ />
31
+ )
32
+ \`\`\`
33
+
34
+ `
35
+
36
+ export default {
37
+ title: 'Organisation Unit Tree',
38
+ component: OrganisationUnitTree,
39
+ decorators: [
40
+ (fn) => (
41
+ <CustomDataProvider data={customData}>{fn()}</CustomDataProvider>
42
+ ),
43
+ ],
44
+ parameters: {
45
+ componentSubtitle: subtitle,
46
+ docs: { description: { component: description } },
47
+ },
48
+ }
49
+
50
+ export { Collapsed } from './__stories__/collapsed.js'
51
+ export { Expanded } from './__stories__/expanded.js'
52
+ export { CustomExpandedImperativeOpen } from './__stories__/custom-expanded-imperative-open.js'
53
+ export { MultipleRoots } from './__stories__/multiple-roots.js'
54
+ export { CustomNodeLabel } from './__stories__/custom-node-label.js'
55
+ export { FilteredRoot } from './__stories__/filtered-root.js'
56
+ export { Filtered } from './__stories__/filtered.js'
57
+ export { SelectedMultiple } from './__stories__/selected-multiple.js'
58
+ export { Indeterminate } from './__stories__/indeterminate.js'
59
+ export { SingleSelection } from './__stories__/single-selection.js'
60
+ export { NoSelection } from './__stories__/no-selection.js'
61
+ export { Highlighted } from './__stories__/highlighted.js'
62
+ export { ForceReloadAll } from './__stories__/force-reload-all.js'
63
+ export { ForceReloadOneUnit } from './__stories__/force-reload-one-unit.js'
64
+ export { ReplaceRoots } from './__stories__/replace-roots.js'
65
+ export { Loading } from './__stories__/loading.js'
66
+ export { RootLoading } from './__stories__/root-loading.js'
67
+ export { RootError } from './__stories__/root-error.js'
68
+ export { LoadingErrorGrandchild } from './__stories__/loading-error-grandchild.js'
69
+ export { RTL } from './__stories__/rtl.js'
70
+ export * from './__stories__/development-stories.js'
@@ -0,0 +1,33 @@
1
+ /* eslint-disable max-params */
2
+
3
+ const UNIT_ID_PATTERN = '[a-zA-Z][a-zA-Z0-9]{10}'
4
+ const orgUnitIdRegExp = new RegExp(`^${UNIT_ID_PATTERN}$`)
5
+ const orgUnitPathRegExp = new RegExp(`(/${UNIT_ID_PATTERN})+`)
6
+
7
+ export const orgUnitPathPropType = (
8
+ propValue,
9
+ key,
10
+ compName,
11
+ location,
12
+ propFullName
13
+ ) => {
14
+ if (!orgUnitPathRegExp.test(propValue[key])) {
15
+ return new Error(
16
+ `Invalid org unit path \`${propValue[key]}\` supplied to \`${compName}.${propFullName}\``
17
+ )
18
+ }
19
+ }
20
+
21
+ export const orgUnitIdPropType = (
22
+ propValue,
23
+ key,
24
+ compName,
25
+ location,
26
+ propFullName
27
+ ) => {
28
+ if (!orgUnitIdRegExp.test(propValue[key])) {
29
+ return new Error(
30
+ `Invalid org unit id \`${propValue[key]}\` supplied to \`${compName}.${propFullName}\``
31
+ )
32
+ }
33
+ }