@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,190 @@
1
+ import { Node } from '@dhis2-ui/node'
2
+ import PropTypes from 'prop-types'
3
+ import React from 'react'
4
+ import { leftTrimToRootId } from '../helpers/index.js'
5
+ import i18n from '../locales/index.js'
6
+ import { orgUnitPathPropType } from '../prop-types.js'
7
+ import { ErrorMessage } from './error-message.js'
8
+ import { hasDescendantSelectedPaths } from './has-descendant-selected-paths.js'
9
+ import { Label } from './label/index.js'
10
+ import { LoadingSpinner } from './loading-spinner.js'
11
+ import { OrganisationUnitNodeChildren } from './organisation-unit-node-children.js'
12
+ import { useOpenState } from './use-open-state.js'
13
+ import { useOrgData } from './use-org-data/index.js'
14
+
15
+ export const OrganisationUnitNode = ({
16
+ autoExpandLoadingError,
17
+ dataTest,
18
+ disableSelection,
19
+ displayName,
20
+ displayProperty,
21
+ expanded,
22
+ highlighted,
23
+ id,
24
+ isUserDataViewFallback,
25
+ path,
26
+ renderNodeLabel,
27
+ rootId,
28
+ selected,
29
+ singleSelection,
30
+ filter,
31
+ suppressAlphabeticalSorting,
32
+ onChange,
33
+ onChildrenLoaded,
34
+ onCollapse,
35
+ onExpand,
36
+ }) => {
37
+ const orgData = useOrgData(id, {
38
+ isUserDataViewFallback,
39
+ displayName,
40
+ })
41
+
42
+ const strippedPath = leftTrimToRootId(path, rootId)
43
+ const node = {
44
+ // guarantee that displayName and id are avaiable before data loaded
45
+ displayName,
46
+ id,
47
+ ...(orgData.data || {}),
48
+ // do not override strippedPath with path from loaded data
49
+ path: strippedPath,
50
+ }
51
+ const hasChildren = !!node.children && node.children > 0
52
+
53
+ const hasSelectedDescendants = hasDescendantSelectedPaths(
54
+ strippedPath,
55
+ selected,
56
+ rootId
57
+ )
58
+ const isHighlighted = highlighted.includes(path)
59
+ const { open, onToggleOpen } = useOpenState({
60
+ autoExpandLoadingError,
61
+ errorMessage: orgData.error && orgData.error.toString(),
62
+ path: strippedPath,
63
+ expanded,
64
+ onExpand,
65
+ onCollapse,
66
+ })
67
+
68
+ const isSelected = !!selected.find((curPath) =>
69
+ curPath.match(new RegExp(`${strippedPath}$`))
70
+ )
71
+
72
+ const labelContent = renderNodeLabel({
73
+ disableSelection,
74
+ hasChildren,
75
+ hasSelectedDescendants,
76
+ loading: orgData.loading,
77
+ error: orgData.error,
78
+ selected,
79
+ open,
80
+ path,
81
+ singleSelection,
82
+ node,
83
+ label: displayName,
84
+ checked: isSelected,
85
+ highlighted: isHighlighted,
86
+ })
87
+
88
+ const label = (
89
+ <Label
90
+ node={node}
91
+ fullPath={path}
92
+ open={open}
93
+ loading={orgData.loading}
94
+ checked={isSelected}
95
+ rootId={rootId}
96
+ onChange={onChange}
97
+ dataTest={`${dataTest}-label`}
98
+ selected={selected}
99
+ hasChildren={hasChildren}
100
+ highlighted={isHighlighted}
101
+ onToggleOpen={onToggleOpen}
102
+ disableSelection={disableSelection}
103
+ singleSelection={singleSelection}
104
+ hasSelectedDescendants={hasSelectedDescendants}
105
+ >
106
+ {labelContent}
107
+ </Label>
108
+ )
109
+
110
+ /**
111
+ * No children means no arrow, therefore we have to provide something.
112
+ * While "loading" is true, "hasChildren" is false
113
+ * There are some possible children variants as content of this node:
114
+ *
115
+ * 1. Nothing; There are no children
116
+ * 2. Placeholder: There are children, but the Node is closed (show arrow)
117
+ * 3. Error: There are children and loading information somehow failed
118
+ * 4. Child nodes: There are children and the node is open
119
+ */
120
+ const showPlaceholder = hasChildren && !open && !orgData.error
121
+ const showChildNodes = hasChildren && open && !orgData.error
122
+
123
+ return (
124
+ <Node
125
+ dataTest={`${dataTest}-node`}
126
+ open={open}
127
+ onOpen={onToggleOpen}
128
+ onClose={onToggleOpen}
129
+ component={label}
130
+ icon={orgData.loading && <LoadingSpinner />}
131
+ >
132
+ {orgData.error && (
133
+ <ErrorMessage dataTest={dataTest}>
134
+ {i18n.t('Could not load children')}
135
+ </ErrorMessage>
136
+ )}
137
+ {showPlaceholder && <span data-test={`${dataTest}-placeholder`} />}
138
+ {showChildNodes && (
139
+ <OrganisationUnitNodeChildren
140
+ // Prevent cirular imports
141
+ OrganisationUnitNode={OrganisationUnitNode}
142
+ node={node}
143
+ autoExpandLoadingError={autoExpandLoadingError}
144
+ dataTest={dataTest}
145
+ disableSelection={disableSelection}
146
+ displayProperty={displayProperty}
147
+ expanded={expanded}
148
+ filter={filter}
149
+ highlighted={highlighted}
150
+ isUserDataViewFallback={isUserDataViewFallback}
151
+ onChange={onChange}
152
+ onChildrenLoaded={onChildrenLoaded}
153
+ onCollapse={onCollapse}
154
+ onExpand={onExpand}
155
+ parentPath={path}
156
+ renderNodeLabel={renderNodeLabel}
157
+ rootId={rootId}
158
+ selected={selected}
159
+ singleSelection={singleSelection}
160
+ suppressAlphabeticalSorting={suppressAlphabeticalSorting}
161
+ />
162
+ )}
163
+ </Node>
164
+ )
165
+ }
166
+
167
+ OrganisationUnitNode.propTypes = {
168
+ dataTest: PropTypes.string.isRequired,
169
+ id: PropTypes.string.isRequired,
170
+ renderNodeLabel: PropTypes.func.isRequired,
171
+ rootId: PropTypes.string.isRequired,
172
+ onChange: PropTypes.func.isRequired,
173
+
174
+ autoExpandLoadingError: PropTypes.bool,
175
+ disableSelection: PropTypes.bool,
176
+ displayName: PropTypes.string,
177
+ displayProperty: PropTypes.oneOf(['displayName', 'displayShortName']),
178
+ expanded: PropTypes.arrayOf(orgUnitPathPropType),
179
+ filter: PropTypes.arrayOf(orgUnitPathPropType),
180
+ highlighted: PropTypes.arrayOf(orgUnitPathPropType),
181
+ isUserDataViewFallback: PropTypes.bool,
182
+ path: orgUnitPathPropType,
183
+ selected: PropTypes.arrayOf(orgUnitPathPropType),
184
+ singleSelection: PropTypes.bool,
185
+ suppressAlphabeticalSorting: PropTypes.bool,
186
+
187
+ onChildrenLoaded: PropTypes.func,
188
+ onCollapse: PropTypes.func,
189
+ onExpand: PropTypes.func,
190
+ }
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ /**
4
+ * @param {Object} args
5
+ * @param {string} args.path
6
+ * @param {string} [args.errorMessage]
7
+ * @param {string} [args.autoExpandLoadingError]
8
+ * @param {string[]} args.expanded
9
+ * @param {Function} [args.onExpand]
10
+ * @param {Function} [args.onCollapse]
11
+ * @returns {Object}
12
+ */
13
+ export const useOpenState = ({
14
+ path,
15
+ expanded,
16
+ onExpand,
17
+ onCollapse,
18
+ errorMessage,
19
+ autoExpandLoadingError,
20
+ }) => {
21
+ const autoExpand = autoExpandLoadingError && !!errorMessage
22
+ const [openedOnceDueToError, setOpenedOnce] = useState(!!errorMessage)
23
+
24
+ useEffect(() => {
25
+ if (autoExpand && !openedOnceDueToError) {
26
+ onExpand({ path })
27
+ setOpenedOnce(true)
28
+ }
29
+ }, [autoExpand, openedOnceDueToError])
30
+
31
+ const open =
32
+ (autoExpand && !openedOnceDueToError) || !!expanded.includes(path)
33
+ const onToggleOpen = () =>
34
+ !open ? onExpand({ path }) : onCollapse({ path })
35
+
36
+ return { open, onToggleOpen, openedOnceDueToError }
37
+ }
@@ -0,0 +1,111 @@
1
+ import { renderHook } from '@testing-library/react'
2
+ import { useOpenState } from './use-open-state.js'
3
+
4
+ describe('OrganisationUnitTree - useOpenState', () => {
5
+ const onExpand = jest.fn()
6
+ const onCollapse = jest.fn()
7
+
8
+ afterEach(() => {
9
+ onExpand.mockClear()
10
+ onCollapse.mockClear()
11
+ })
12
+
13
+ it('should set open to false if the path is not in the expanded array', () => {
14
+ const path = '/foo'
15
+ const expanded = ['/bar']
16
+ const expected = false
17
+ const { result } = renderHook(() => useOpenState({ path, expanded }))
18
+ const { open: actual } = result.current
19
+
20
+ expect(actual).toBe(expected)
21
+ })
22
+
23
+ it('should set open to true if the path is in the expanded array', () => {
24
+ const path = '/foo'
25
+ const expanded = ['/foo']
26
+ const expected = true
27
+ const { result } = renderHook(() => useOpenState({ path, expanded }))
28
+ const { open: actual } = result.current
29
+
30
+ expect(actual).toBe(expected)
31
+ })
32
+
33
+ it('should call onCollapse when calling onToggleOpen while open is true', () => {
34
+ const path = '/foo'
35
+ const expanded = ['/foo']
36
+
37
+ const { result } = renderHook(() =>
38
+ useOpenState({ path, expanded, onCollapse })
39
+ )
40
+ const { onToggleOpen } = result.current
41
+ onToggleOpen()
42
+
43
+ expect(onCollapse).toHaveBeenCalledWith({ path: '/foo' })
44
+ expect(onExpand).toHaveBeenCalledTimes(0)
45
+ })
46
+
47
+ it('should call onExpand when calling onToggleOpen while open is false', () => {
48
+ const path = '/foo'
49
+ const expanded = []
50
+
51
+ const { result } = renderHook(() =>
52
+ useOpenState({ path, expanded, onExpand })
53
+ )
54
+ const { onToggleOpen } = result.current
55
+ onToggleOpen()
56
+
57
+ expect(onExpand).toHaveBeenCalledWith({ path: '/foo' })
58
+ expect(onCollapse).toHaveBeenCalledTimes(0)
59
+ })
60
+
61
+ it('should set openedOnceDueToError to true if there is an error and autoExpandLoadingError is true', async () => {
62
+ const path = '/foo'
63
+ const { result } = renderHook(() =>
64
+ useOpenState({
65
+ autoExpandLoadingError: true,
66
+ errorMessage: 'error message',
67
+ path,
68
+ expanded: [],
69
+ onExpand,
70
+ onCollapse,
71
+ })
72
+ )
73
+
74
+ const { openedOnceDueToError } = result.current
75
+ expect(openedOnceDueToError).toBe(true)
76
+ })
77
+
78
+ it('should not set open to true if there is an error but autoExpandLoadingError is falsy', () => {
79
+ const path = '/foo'
80
+ const { result } = renderHook(() =>
81
+ useOpenState({
82
+ autoExpandLoadingError: undefined,
83
+ errorMessage: 'error message',
84
+ path,
85
+ expanded: [],
86
+ onExpand,
87
+ onCollapse,
88
+ })
89
+ )
90
+ const { open } = result.current
91
+
92
+ expect(open).toBe(false)
93
+ })
94
+
95
+ it('should not set open to true if autoExpandLoadingError is true but there is no error', () => {
96
+ const path = '/foo'
97
+ const { result } = renderHook(() =>
98
+ useOpenState({
99
+ autoExpandLoadingError: true,
100
+ errorMessage: '',
101
+ path,
102
+ expanded: [],
103
+ onExpand,
104
+ onCollapse,
105
+ })
106
+ )
107
+ const { open } = result.current
108
+
109
+ expect(open).toBe(false)
110
+ })
111
+ })
@@ -0,0 +1,63 @@
1
+ import { useDataQuery } from '@dhis2/app-runtime'
2
+ import { useMemo, useEffect, useRef } from 'react'
3
+ import { sortNodeChildrenAlphabetically } from '../helpers/index.js'
4
+
5
+ const ORG_DATA_QUERY = {
6
+ orgUnit: {
7
+ resource: `organisationUnits`,
8
+ id: ({ id }) => id,
9
+ params: ({ displayProperty }) => ({
10
+ fields:
11
+ displayProperty === 'displayName'
12
+ ? 'children[id,path,displayName]'
13
+ : `children[id,path,${displayProperty}~rename(displayName)]`,
14
+ }),
15
+ },
16
+ }
17
+
18
+ /**
19
+ * @param {string[]} ids
20
+ * @param {Object} options
21
+ * @param {string} options.displayName
22
+ * @param {boolean} [options.withChildren]
23
+ * @param {'displayName'|'displayShortName'} [options.displayProperty]
24
+ * @returns {Object}
25
+ */
26
+ export const useOrgChildren = ({
27
+ node,
28
+ suppressAlphabeticalSorting,
29
+ onComplete,
30
+ displayProperty = 'displayName',
31
+ }) => {
32
+ const onCompleteCalledRef = useRef(false)
33
+ const { called, loading, error, data } = useDataQuery(ORG_DATA_QUERY, {
34
+ variables: { id: node.id, displayProperty },
35
+ })
36
+
37
+ const orgChildren = useMemo(() => {
38
+ if (!data) {
39
+ return undefined
40
+ }
41
+
42
+ // undefined or zero
43
+ if (!node.children) {
44
+ return []
45
+ }
46
+
47
+ const { orgUnit } = data
48
+
49
+ return suppressAlphabeticalSorting
50
+ ? orgUnit.children
51
+ : sortNodeChildrenAlphabetically(orgUnit.children)
52
+ }, [data, node.children, suppressAlphabeticalSorting])
53
+
54
+ useEffect(() => {
55
+ if (onComplete && orgChildren && !onCompleteCalledRef.current) {
56
+ // For backwards compatibility: Pass entire node incl. children
57
+ onComplete({ ...node, children: orgChildren })
58
+ onCompleteCalledRef.current = true
59
+ }
60
+ }, [node, onComplete, orgChildren, onCompleteCalledRef])
61
+
62
+ return { called, loading, error: error || null, data: orgChildren }
63
+ }