@squiz/resource-browser 1.69.1 → 2.1.8-rc.0

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 (141) hide show
  1. package/CHANGELOG.md +88 -35
  2. package/LICENSE.md +15 -0
  3. package/README.md +9 -0
  4. package/jest.config.ts +22 -21
  5. package/lib/Hooks/useSelectedState.d.ts +15 -0
  6. package/lib/Hooks/useSelectedState.js +16 -0
  7. package/lib/Hooks/useSources.d.ts +5 -4
  8. package/lib/Hooks/useSources.js +25 -1
  9. package/lib/MainContainer/MainContainer.d.ts +17 -0
  10. package/lib/MainContainer/MainContainer.js +61 -0
  11. package/lib/Plugin/Plugin.d.ts +13 -0
  12. package/lib/Plugin/Plugin.js +17 -0
  13. package/lib/ResourceBrowserContext/ResourceBrowserContext.d.ts +2 -3
  14. package/lib/ResourceBrowserContext/ResourceBrowserContext.js +4 -17
  15. package/lib/ResourceBrowserInput/ResourceBrowserInput.d.ts +24 -0
  16. package/lib/ResourceBrowserInput/ResourceBrowserInput.js +16 -0
  17. package/lib/ResourcePicker/ResourcePicker.d.ts +6 -4
  18. package/lib/ResourcePicker/ResourcePicker.js +14 -8
  19. package/lib/ResourcePicker/States/Selected.d.ts +10 -4
  20. package/lib/ResourcePicker/States/Selected.js +11 -32
  21. package/lib/SourceDropdown/SourceDropdown.d.ts +5 -11
  22. package/lib/SourceDropdown/SourceDropdown.js +20 -99
  23. package/lib/SourceList/SourceList.d.ts +5 -16
  24. package/lib/SourceList/SourceList.js +14 -75
  25. package/lib/index.css +42 -202
  26. package/lib/index.d.ts +7 -7
  27. package/lib/index.js +69 -13
  28. package/lib/types.d.ts +41 -59
  29. package/package.json +82 -80
  30. package/src/Hooks/useSelectedState.spec.ts +46 -0
  31. package/src/Hooks/useSelectedState.ts +22 -0
  32. package/src/Hooks/useSources.spec.ts +30 -12
  33. package/src/Hooks/useSources.ts +33 -4
  34. package/src/Icons/CircledLoopIcon.tsx +8 -8
  35. package/src/MainContainer/MainContainer.spec.tsx +203 -0
  36. package/src/MainContainer/MainContainer.stories.tsx +62 -0
  37. package/src/MainContainer/MainContainer.tsx +101 -0
  38. package/src/Plugin/Plugin.spec.tsx +46 -0
  39. package/src/Plugin/Plugin.tsx +20 -0
  40. package/src/ResourceBrowserContext/ResourceBrowserContext.spec.tsx +65 -106
  41. package/src/ResourceBrowserContext/ResourceBrowserContext.tsx +24 -39
  42. package/src/ResourceBrowserInput/ResourceBrowserInput.spec.tsx +192 -0
  43. package/src/ResourceBrowserInput/ResourceBrowserInput.tsx +81 -0
  44. package/src/ResourcePicker/ResourcePicker.spec.tsx +159 -116
  45. package/src/ResourcePicker/ResourcePicker.stories.tsx +28 -24
  46. package/src/ResourcePicker/ResourcePicker.tsx +79 -59
  47. package/src/ResourcePicker/States/Error.tsx +8 -8
  48. package/src/ResourcePicker/States/Loading.tsx +3 -3
  49. package/src/ResourcePicker/States/Selected.tsx +66 -73
  50. package/src/ResourcePicker/mock-image-resource.json +25 -47
  51. package/src/ResourcePicker/mock-resource.json +11 -13
  52. package/src/ResourcePicker/resource-picker.scss +13 -13
  53. package/src/SourceDropdown/SourceDropdown.spec.tsx +65 -391
  54. package/src/SourceDropdown/SourceDropdown.stories.tsx +21 -24
  55. package/src/SourceDropdown/SourceDropdown.tsx +80 -258
  56. package/src/SourceList/SourceList.spec.tsx +37 -430
  57. package/src/SourceList/SourceList.stories.tsx +17 -37
  58. package/src/SourceList/SourceList.tsx +28 -155
  59. package/src/__mocks__/MockModels.ts +56 -25
  60. package/src/__mocks__/PluginExample.tsx +98 -0
  61. package/src/__mocks__/StorybookHelpers.tsx +141 -0
  62. package/src/__mocks__/renderWithContext.tsx +14 -18
  63. package/src/__mocks__/sample-sources.json +32 -0
  64. package/src/index.scss +18 -8
  65. package/src/index.spec.tsx +277 -99
  66. package/src/index.stories.tsx +65 -39
  67. package/src/index.tsx +119 -57
  68. package/src/types.ts +54 -63
  69. package/tailwind.config.cjs +92 -92
  70. package/vite.config.js +12 -12
  71. package/lib/Hooks/useCategorisedSources.d.ts +0 -14
  72. package/lib/Hooks/useCategorisedSources.js +0 -38
  73. package/lib/Hooks/useChildResources.d.ts +0 -16
  74. package/lib/Hooks/useChildResources.js +0 -13
  75. package/lib/Hooks/usePreselectedResourcePath.d.ts +0 -20
  76. package/lib/Hooks/usePreselectedResourcePath.js +0 -31
  77. package/lib/Hooks/useRecentLocations.d.ts +0 -5
  78. package/lib/Hooks/useRecentLocations.js +0 -38
  79. package/lib/Hooks/useRecentResourcesPaths.d.ts +0 -20
  80. package/lib/Hooks/useRecentResourcesPaths.js +0 -30
  81. package/lib/Hooks/useResource.d.ts +0 -28
  82. package/lib/Hooks/useResource.js +0 -23
  83. package/lib/Hooks/useResourcePath.d.ts +0 -16
  84. package/lib/Hooks/useResourcePath.js +0 -64
  85. package/lib/Icons/HistoryIcon.d.ts +0 -4
  86. package/lib/Icons/HistoryIcon.js +0 -13
  87. package/lib/PreviewPanel/PreviewPanel.d.ts +0 -5
  88. package/lib/PreviewPanel/PreviewPanel.js +0 -8
  89. package/lib/PreviewPanel/details/MatrixResource.d.ts +0 -7
  90. package/lib/PreviewPanel/details/MatrixResource.js +0 -35
  91. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +0 -9
  92. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +0 -54
  93. package/lib/ResourceList/ResourceList.d.ts +0 -18
  94. package/lib/ResourceList/ResourceList.js +0 -49
  95. package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +0 -17
  96. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +0 -166
  97. package/lib/StatusIndicator/StatusIndicator.d.ts +0 -8
  98. package/lib/StatusIndicator/StatusIndicator.js +0 -27
  99. package/lib/utils/findBestMatchLineage.d.ts +0 -2
  100. package/lib/utils/findBestMatchLineage.js +0 -28
  101. package/lib/utils/uuid.d.ts +0 -1
  102. package/lib/utils/uuid.js +0 -6
  103. package/src/Hooks/useCategorisedSources.spec.ts +0 -39
  104. package/src/Hooks/useCategorisedSources.ts +0 -46
  105. package/src/Hooks/useChildResources.spec.ts +0 -29
  106. package/src/Hooks/useChildResources.ts +0 -21
  107. package/src/Hooks/usePreselectedResourcePath.ts +0 -54
  108. package/src/Hooks/useRecentLocations.spec.ts +0 -81
  109. package/src/Hooks/useRecentLocations.ts +0 -44
  110. package/src/Hooks/useRecentResourcesPaths.ts +0 -54
  111. package/src/Hooks/useResource.spec.ts +0 -61
  112. package/src/Hooks/useResource.ts +0 -38
  113. package/src/Hooks/useResourcePath.spec.ts +0 -120
  114. package/src/Hooks/useResourcePath.ts +0 -76
  115. package/src/Icons/HistoryIcon.tsx +0 -17
  116. package/src/PreviewPanel/PreviewPanel.spec.tsx +0 -198
  117. package/src/PreviewPanel/PreviewPanel.stories.tsx +0 -76
  118. package/src/PreviewPanel/PreviewPanel.tsx +0 -6
  119. package/src/PreviewPanel/details/MatrixResource.tsx +0 -54
  120. package/src/PreviewPanel/details/matrix-resource.scss +0 -16
  121. package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +0 -133
  122. package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +0 -24
  123. package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +0 -79
  124. package/src/ResourceBreadcrumb/resource-breadcrumb.scss +0 -28
  125. package/src/ResourceBreadcrumb/sample-hierarchy.json +0 -27
  126. package/src/ResourceList/ResourceList.spec.tsx +0 -202
  127. package/src/ResourceList/ResourceList.stories.tsx +0 -40
  128. package/src/ResourceList/ResourceList.tsx +0 -83
  129. package/src/ResourceList/sample-resources.json +0 -851
  130. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +0 -780
  131. package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +0 -45
  132. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +0 -290
  133. package/src/SourceList/sample-sources.json +0 -251
  134. package/src/StatusIndicator/StatusIndicator.stories.tsx +0 -83
  135. package/src/StatusIndicator/StatusIndicator.tsx +0 -38
  136. package/src/__mocks__/JestHelpers.ts +0 -65
  137. package/src/__mocks__/StorybookHelpers.ts +0 -128
  138. package/src/__mocks__/jestHelpers.spec.ts +0 -38
  139. package/src/utils/findBestMatchLineage.spec.ts +0 -81
  140. package/src/utils/findBestMatchLineage.ts +0 -30
  141. package/src/utils/uuid.ts +0 -5
@@ -1,271 +1,93 @@
1
- import React, { useState, useRef, useEffect } from 'react';
2
- import { useFocusWithin, useKeyboard } from '@react-aria/interactions';
3
- import { Icon, IconOptions, Spinner } from '@squiz/generic-browser-lib';
1
+ import React, { useRef, useState } from 'react';
4
2
 
5
- import type { Source, ScopedSource, Resource } from '../types';
6
-
7
- import uuid from '../utils/uuid';
8
- import { useCategorisedSources } from '../Hooks/useCategorisedSources';
9
- import { HistoryIcon } from '../Icons/HistoryIcon';
10
- import { RecentResourcesPaths } from '../Hooks/useRecentResourcesPaths';
3
+ import { ResourceBrowserSource } from '../types';
4
+ import { Icon, IconOptions, uuid } from '@squiz/generic-browser-lib';
5
+ import { useFocusWithin, useKeyboard } from 'react-aria';
11
6
 
12
7
  export default function SourceDropdown({
13
- sources,
14
- selectedSource,
15
- isLoading,
16
- onRootSelect,
17
- onSourceSelect,
18
- setSource,
19
- currentResource,
20
- recentSources,
8
+ sources,
9
+ selectedSource,
10
+ onSourceSelect,
21
11
  }: {
22
- sources: Source[];
23
- selectedSource: ScopedSource | null;
24
- isLoading: boolean;
25
- onRootSelect: () => void;
26
- onSourceSelect: (source: ScopedSource) => void;
27
- setSource: (source: ScopedSource | null, path?: Resource[]) => void;
28
- currentResource: Resource | null;
29
- recentSources: RecentResourcesPaths[];
12
+ sources: ResourceBrowserSource[];
13
+ selectedSource: ResourceBrowserSource;
14
+ onSourceSelect(source: ResourceBrowserSource): void;
30
15
  }) {
31
- const categorisedSources = useCategorisedSources(sources);
32
- const filteredRecentSources = recentSources.filter((item) => item.path?.length);
33
- const [recentLocationSelection, setRecentLocationSelection] = useState<RecentResourcesPaths | null>();
34
-
35
- const [uniqueId] = useState(uuid());
36
- const buttonRef = useRef<HTMLButtonElement>(null);
37
- const [isOpen, setIsOpen] = useState(false);
38
-
39
- // Watch the focus and blur on the menu and close if focus leaves the control
40
- const { focusWithinProps } = useFocusWithin({
41
- onBlurWithin: () => {
42
- setIsOpen(false);
43
- },
44
- });
16
+ const [uniqueId] = useState(uuid());
17
+ const buttonRef = useRef<HTMLButtonElement>(null);
18
+ const [isOpen, setIsOpen] = useState(false);
19
+
20
+ // Watch the focus and blur on the menu and close if focus leaves the control
21
+ const { focusWithinProps } = useFocusWithin({
22
+ onBlurWithin: () => {
23
+ setIsOpen(false);
24
+ },
25
+ });
26
+
27
+ // Listen for Esc key within this element
28
+ const { keyboardProps } = useKeyboard({
29
+ onKeyDown: (e) => {
30
+ if (isOpen && e.key === 'Escape') {
31
+ setIsOpen(false);
32
+ buttonRef.current?.focus(); // Restore focus to the element which opened the menu
33
+ }
34
+ },
35
+ });
45
36
 
46
- // Listen for Esc key within this element
47
- const { keyboardProps } = useKeyboard({
48
- onKeyDown: (e) => {
49
- if (isOpen && e.key === 'Escape') {
37
+ const handleSourceClick = (source: ResourceBrowserSource) => {
50
38
  setIsOpen(false);
51
- buttonRef.current?.focus(); // Restore focus to the element which opened the menu
52
- }
53
- },
54
- });
39
+ buttonRef.current?.focus();
40
+ onSourceSelect(source);
41
+ };
55
42
 
56
- const handleSourceClick = (source: ScopedSource) => {
57
- setIsOpen(false);
58
- setRecentLocationSelection(null);
59
- buttonRef.current?.focus();
60
- onSourceSelect(source);
61
- };
62
-
63
- const handleRootSelect = () => {
64
- setIsOpen(false);
65
- setRecentLocationSelection(null);
66
- buttonRef.current?.focus();
67
- onRootSelect();
68
- };
69
-
70
- const handleRecentLocationClick = (location: RecentResourcesPaths) => {
71
- setIsOpen(false);
72
- setRecentLocationSelection(location);
73
- buttonRef.current?.focus();
74
-
75
- if (location.path) {
76
- const [rootNode, ...path] = location.path;
77
- setSource(
78
- {
79
- source: location.source as Source,
80
- resource: rootNode,
81
- },
82
- path,
83
- );
43
+ if (!sources.length) {
44
+ return <></>;
84
45
  }
85
- };
86
46
 
87
- useEffect(() => {
88
- if (recentLocationSelection?.path) {
89
- const lastResource = recentLocationSelection.path[recentLocationSelection.path.length - 1];
90
- // If the current resource selected in the resource browser is no longer the item selected in the
91
- // recent locations section dropdown then we set the selection to null to prevent active statuses.
92
- if (currentResource && currentResource.id !== lastResource?.id) {
93
- setRecentLocationSelection(null);
94
- }
95
- }
96
- }, [recentLocationSelection, currentResource]);
97
-
98
- return (
99
- <div {...focusWithinProps} {...keyboardProps} className="relative w-72 border-2 rounded border-gray-300">
100
- <button
101
- ref={buttonRef}
102
- type="button"
103
- aria-label="Source quick select"
104
- aria-expanded={isOpen}
105
- aria-controls={`${uniqueId}-button-menu`}
106
- onClick={() => setIsOpen(!isOpen)}
107
- className="relative flex items-center text-sm font-semibold p-1.5 w-full"
108
- >
109
- {selectedSource && (
110
- <>
111
- <span className="sr-only">current source </span>
112
- <Icon
113
- icon={
114
- // Ignoring this specific line in test coverage because its a super niche issue that I could only replicate in Matrix
115
- /* istanbul ignore next */
116
- recentLocationSelection
117
- ? currentResource?.type.code || selectedSource.resource?.type.code
118
- : selectedSource.resource?.type.code
119
- }
120
- resourceSource="matrix"
121
- aria-hidden
122
- className="mr-2.5 h-[20px] w-[20px]"
123
- />
124
- <div className="truncate max-w-[200px]">
125
- {
126
- // Ignoring this specific line in test coverage because its a super niche issue that I could only replicate in Matrix
127
- /* istanbul ignore next */
128
- recentLocationSelection
129
- ? currentResource?.name || selectedSource.resource?.name || selectedSource.source.name
130
- : selectedSource.resource?.name || selectedSource.source.name
131
- }
132
- </div>
133
- </>
134
- )}
135
-
136
- {!selectedSource && (
137
- <>
138
- <span className="sr-only">view </span>
139
- <Icon icon={'root' as IconOptions} aria-hidden className="mr-2.5 h-[20px] w-[20px]" />
140
- All available sources
141
- </>
142
- )}
143
-
144
- <Icon icon={'arrow-down' as IconOptions} aria-hidden className="absolute right-3" />
145
- </button>
146
- <ul
147
- id={`${uniqueId}-button-menu`}
148
- aria-hidden={!isOpen}
149
- className={`absolute z-50 top-[calc(100%+5px)] -left-0.5 w-[calc(100%+4px)] bg-gray-100 border-2 rounded border-gray-300 p-2 overflow-y-scroll max-h-80 ${
150
- !isOpen ? 'hidden' : ''
151
- }`}
152
- >
153
- <li
154
- key="return-root"
155
- className="flex items-center text-sm font-semibold mb-2 bg-white border-1 rounded border-grey-200"
156
- >
157
- <button
158
- type="button"
159
- onClick={handleRootSelect}
160
- className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100`}
161
- >
162
- <Icon icon={'root' as IconOptions} aria-hidden className="mr-2.5" />
163
- All available sources
164
- </button>
165
- </li>
166
- {isLoading && (
167
- <li className="mt-2">
168
- <Spinner size="sm" label="Loading sources" className="m-3" />
169
- </li>
170
- )}
171
-
172
- {!isLoading && filteredRecentSources.length > 0 && (
173
- <li className={`flex flex-col text-sm font-semibold text-grey-800`}>
174
- <div className="relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0">
175
- <span className="z-10 bg-gray-100 px-2.5 flex gap-1 items-center">
176
- <HistoryIcon />
177
- Recent locations
178
- </span>
179
- </div>
180
- <ul aria-label="recent location nodes" className="flex flex-col mt-2">
181
- {filteredRecentSources.map((item, index) => {
182
- const lastResource = item.path && item.path[item.path.length - 1];
183
- const isSelectedSource =
184
- item.source?.id === selectedSource?.source.id &&
185
- lastResource?.id === recentLocationSelection?.path?.[recentLocationSelection.path?.length - 1]?.id;
186
-
187
- return (
188
- <li
189
- key={`${index}-${item.source?.id}-${lastResource?.id}`}
190
- className="flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b"
191
- >
192
- <button
193
- type="button"
194
- onClick={() => handleRecentLocationClick(item)}
195
- className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100`}
196
- >
197
- <Icon
198
- icon={(lastResource?.type.code || 'folder') as IconOptions}
199
- resourceSource="matrix"
200
- aria-label={lastResource?.name || item.source?.name}
201
- className="shrink-0 mr-2.5"
202
- />
203
- <span className="text-left mr-7">{lastResource?.name || item.source?.name}</span>
204
- {isSelectedSource && (
205
- <Icon icon={'selected' as IconOptions} aria-label="selected" className="absolute right-4" />
206
- )}
207
- </button>
208
- </li>
209
- );
210
- })}
211
- </ul>
212
- </li>
213
- )}
214
-
215
- {!isLoading &&
216
- categorisedSources.map(({ key, label, sources }, index) => {
217
- return (
218
- <li
219
- key={key}
220
- className={`flex flex-col text-sm font-semibold text-grey-800 ${
221
- index > 0 || filteredRecentSources.length > 0 ? 'mt-3' : ''
47
+ return (
48
+ <div {...focusWithinProps} {...keyboardProps} className="relative w-72 border-2 rounded border-gray-300">
49
+ <button
50
+ ref={buttonRef}
51
+ type="button"
52
+ aria-label="Source quick select"
53
+ aria-expanded={isOpen}
54
+ aria-controls={`${uniqueId}-button-menu`}
55
+ onClick={() => setIsOpen(!isOpen)}
56
+ className="relative flex items-center text-sm font-semibold p-1.5 w-full"
57
+ >
58
+ <span className="sr-only">current source </span>
59
+ <Icon icon={selectedSource.type as IconOptions} aria-hidden className="mr-2.5 h-[20px] w-[20px]" />
60
+ <div className="truncate max-w-[200px]">{selectedSource.name}</div>
61
+ <Icon icon={'arrow-down' as IconOptions} aria-hidden className="absolute right-3" />
62
+ </button>
63
+ <ul
64
+ id={`${uniqueId}-button-menu`}
65
+ aria-hidden={!isOpen}
66
+ className={`absolute z-50 top-[calc(100%+5px)] -left-0.5 w-[calc(100%+4px)] bg-gray-100 border-1 shadow-md rounded border-gray-300 p-2 pb-0 overflow-y-scroll max-h-80 ${
67
+ !isOpen ? 'hidden' : ''
222
68
  }`}
223
- >
224
- <div className="relative flex justify-center before:w-full before:h-px before:bg-gray-300 before:absolute before:top-2/4 before:z-0">
225
- <span className="z-10 bg-gray-100 px-2.5">{label}</span>
226
- </div>
227
- {sources?.length > 0 && (
228
- <ul aria-label={`${label} nodes`} className="flex flex-col mt-2">
229
- {sources.map(({ source, resource }) => {
230
- const isSelectedSource =
231
- source.id === selectedSource?.source.id &&
232
- resource?.id === selectedSource?.resource?.id &&
233
- !recentLocationSelection;
234
-
235
- return (
236
- <li
237
- key={`${source.id}-${resource?.id}`}
238
- className="flex items-center bg-white border border-b-0 last:border-b border-grey-200 first:rounded-t last:rounded-b"
239
- >
240
- <button
241
- type="button"
242
- onClick={() => handleSourceClick({ source, resource })}
243
- className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100`}
244
- >
245
- <Icon
246
- icon={resource?.type.code as IconOptions}
247
- resourceSource="matrix"
248
- aria-label={resource?.type.name}
249
- className="shrink-0 mr-2.5"
250
- />
251
- <span className="text-left mr-7">{resource?.name || source.name}</span>
252
- {isSelectedSource && (
253
- <Icon
254
- icon={'selected' as IconOptions}
255
- aria-label="selected"
256
- className="absolute right-4"
257
- />
258
- )}
259
- </button>
69
+ >
70
+ {sources.map((source) => {
71
+ const { id, name, type } = source;
72
+ const isSelectedSource = id === selectedSource.id;
73
+
74
+ return (
75
+ <li key={id} className="flex items-center text-sm font-semibold mb-2 bg-white rounded">
76
+ <button
77
+ type="button"
78
+ onClick={() => handleSourceClick(source)}
79
+ className={`relative grow flex items-center p-2 border-1 border-white rounded hover:bg-gray-50 hover:border-gray-300 focus:bg-gray-100`}
80
+ >
81
+ <Icon icon={type as IconOptions} aria-label={type} className="shrink-0 mr-2.5" />
82
+ <span className="text-left mr-7">{name}</span>
83
+ {isSelectedSource && (
84
+ <Icon icon={'selected' as IconOptions} aria-label="selected" className="absolute right-4" />
85
+ )}
86
+ </button>
260
87
  </li>
261
- );
262
- })}
263
- </ul>
264
- )}
265
- </li>
266
- );
267
- })}
268
- </ul>
269
- </div>
270
- );
88
+ );
89
+ })}
90
+ </ul>
91
+ </div>
92
+ );
271
93
  }