@squiz/resource-browser 1.32.1-alpha.14 → 1.32.1-alpha.16

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 (90) hide show
  1. package/jest.config.ts +12 -1
  2. package/lib/Hooks/useCategorisedSources.d.ts +14 -0
  3. package/lib/Hooks/useCategorisedSources.js +38 -0
  4. package/lib/Hooks/useChildResources.d.ts +19 -0
  5. package/lib/Hooks/useChildResources.js +35 -0
  6. package/lib/Hooks/useResourcePath.d.ts +16 -0
  7. package/lib/Hooks/useResourcePath.js +64 -0
  8. package/lib/Hooks/useSources.d.ts +16 -0
  9. package/lib/Hooks/useSources.js +29 -0
  10. package/lib/Icons/Icon.d.ts +7 -7
  11. package/lib/Icons/Icon.js +7 -9
  12. package/lib/Icons/MatrixResources/Audio.js +1 -1
  13. package/lib/Icons/MatrixResources/Excel.js +1 -1
  14. package/lib/Icons/MatrixResources/MatrixResourceMap.d.ts +6 -6
  15. package/lib/Icons/MatrixResources/MatrixResourceMap.js +6 -6
  16. package/lib/Icons/MatrixResources/Pdf.js +1 -1
  17. package/lib/Icons/MatrixResources/Powerpoint.js +1 -1
  18. package/lib/Icons/MatrixResources/Video.js +1 -1
  19. package/lib/Icons/MatrixResources/Word.js +1 -1
  20. package/lib/Modal/Modal.js +1 -1
  21. package/lib/PreviewPanel/PreviewModal.js +1 -1
  22. package/lib/PreviewPanel/PreviewPanel.d.ts +4 -6
  23. package/lib/PreviewPanel/PreviewPanel.js +11 -39
  24. package/lib/PreviewPanel/details/MatrixResource.d.ts +4 -9
  25. package/lib/PreviewPanel/details/MatrixResource.js +20 -16
  26. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +5 -5
  27. package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +3 -3
  28. package/lib/ResourceItem/ResourceItem.d.ts +6 -8
  29. package/lib/ResourceItem/ResourceItem.js +3 -3
  30. package/lib/ResourceList/ResourceList.d.ts +5 -4
  31. package/lib/ResourceList/ResourceList.js +3 -3
  32. package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +4 -5
  33. package/lib/ResourcePickerContainer/ResourcePickerContainer.js +34 -89
  34. package/lib/SourceDropdown/SourceDropdown.d.ts +5 -5
  35. package/lib/SourceDropdown/SourceDropdown.js +19 -27
  36. package/lib/SourceList/SourceList.d.ts +4 -4
  37. package/lib/SourceList/SourceList.js +7 -5
  38. package/lib/index.css +6 -0
  39. package/lib/index.d.ts +6 -29
  40. package/lib/index.js +2 -3
  41. package/lib/uuid.js +1 -3
  42. package/package.json +3 -2
  43. package/src/Hooks/useCategorisedSources.spec.ts +39 -0
  44. package/src/Hooks/useCategorisedSources.ts +46 -0
  45. package/src/Hooks/useChildResources.spec.ts +49 -0
  46. package/src/Hooks/useChildResources.ts +43 -0
  47. package/src/Hooks/useResourcePath.spec.ts +124 -0
  48. package/src/Hooks/useResourcePath.ts +76 -0
  49. package/src/Hooks/useSources.spec.ts +33 -0
  50. package/src/Hooks/useSources.ts +33 -0
  51. package/src/Icons/Icon.stories.tsx +7 -7
  52. package/src/Icons/Icon.tsx +9 -14
  53. package/src/Icons/MatrixResources/Audio.tsx +1 -1
  54. package/src/Icons/MatrixResources/Excel.tsx +1 -1
  55. package/src/Icons/MatrixResources/MatrixResourceMap.ts +7 -7
  56. package/src/Icons/MatrixResources/Pdf.tsx +1 -1
  57. package/src/Icons/MatrixResources/Powerpoint.tsx +1 -1
  58. package/src/Icons/MatrixResources/Video.tsx +1 -1
  59. package/src/Icons/MatrixResources/Word.tsx +1 -1
  60. package/src/Modal/Modal.tsx +1 -1
  61. package/src/PreviewPanel/PreviewModal.tsx +1 -1
  62. package/src/PreviewPanel/PreviewPanel.spec.tsx +20 -62
  63. package/src/PreviewPanel/PreviewPanel.stories.tsx +16 -24
  64. package/src/PreviewPanel/PreviewPanel.tsx +15 -51
  65. package/src/PreviewPanel/details/MatrixResource.tsx +23 -19
  66. package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +13 -23
  67. package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +1 -1
  68. package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +8 -9
  69. package/src/ResourceBreadcrumb/sample-hierarchy.json +15 -25
  70. package/src/ResourceItem/ResourceItem.tsx +10 -12
  71. package/src/ResourceList/ResourceList.spec.tsx +8 -53
  72. package/src/ResourceList/ResourceList.stories.tsx +2 -2
  73. package/src/ResourceList/ResourceList.tsx +12 -10
  74. package/src/ResourceList/sample-resources.json +551 -49
  75. package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +196 -315
  76. package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +7 -29
  77. package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +63 -127
  78. package/src/SourceDropdown/SourceDropdown.spec.tsx +63 -60
  79. package/src/SourceDropdown/SourceDropdown.stories.tsx +4 -7
  80. package/src/SourceDropdown/SourceDropdown.tsx +34 -41
  81. package/src/SourceList/SourceList.spec.tsx +38 -32
  82. package/src/SourceList/SourceList.tsx +17 -19
  83. package/src/SourceList/sample-sources.json +186 -77
  84. package/src/__mocks__/MockModels.ts +30 -0
  85. package/src/__mocks__/StorybookHelpers.ts +46 -0
  86. package/src/index.stories.tsx +13 -38
  87. package/src/index.tsx +5 -29
  88. package/src/types.d.ts +71 -0
  89. package/src/uuid.ts +2 -4
  90. package/src/SourceDropdown/sample-sources.json +0 -110
@@ -9,6 +9,7 @@ import { DOMAttributes, FocusableElement } from '@react-types/shared';
9
9
  import { useOverlayTrigger } from 'react-aria';
10
10
 
11
11
  import PreviewPanel from './PreviewPanel';
12
+ import { Resource } from '../types';
12
13
 
13
14
  function PreviewPanelTestWrapper({
14
15
  constructFunction,
@@ -24,35 +25,20 @@ function PreviewPanelTestWrapper({
24
25
  return constructFunction(previewModalState, overlayProps);
25
26
  }
26
27
 
27
- describe('PreviewPanel', () => {
28
- it('Show loading when isLoading is true', async () => {
29
- const onSelect = jest.fn();
30
- const onClose = jest.fn();
31
-
32
- render(
33
- <PreviewPanelTestWrapper
34
- constructFunction={(previewModalState, overlayProps) => {
35
- return (
36
- <PreviewPanel
37
- node={{ id: '1', source: '1' }}
38
- resourceDetail={null}
39
- isLoading
40
- allowedTypes={undefined}
41
- modalState={previewModalState}
42
- previewModalOverlayProps={overlayProps}
43
- onSelect={onSelect}
44
- onClose={onClose}
45
- />
46
- );
47
- }}
48
- />,
49
- );
50
-
51
- await waitFor(() => {
52
- expect(screen.getAllByText('Loading info')).toBeTruthy();
53
- });
54
- });
28
+ function mockResource(properties: Partial<Resource> = {}): Resource {
29
+ return {
30
+ id: 'test-resource',
31
+ name: 'Test resource',
32
+ type: { code: 'folder', name: 'Folder' },
33
+ status: { code: 'live', name: 'Live' },
34
+ url: 'https://no-where.com',
35
+ urls: [],
36
+ childCount: 0,
37
+ ...properties,
38
+ };
39
+ }
55
40
 
41
+ describe('PreviewPanel', () => {
56
42
  it('Renders a message when no item selected', async () => {
57
43
  const onSelect = jest.fn();
58
44
  const onClose = jest.fn();
@@ -62,9 +48,7 @@ describe('PreviewPanel', () => {
62
48
  constructFunction={(previewModalState, overlayProps) => {
63
49
  return (
64
50
  <PreviewPanel
65
- node={null}
66
- resourceDetail={null}
67
- isLoading={false}
51
+ resource={null}
68
52
  allowedTypes={undefined}
69
53
  modalState={previewModalState}
70
54
  previewModalOverlayProps={overlayProps}
@@ -91,16 +75,7 @@ describe('PreviewPanel', () => {
91
75
  constructFunction={(previewModalState, overlayProps) => {
92
76
  return (
93
77
  <PreviewPanel
94
- node={{ id: '1', source: '1' }}
95
- resourceDetail={{
96
- type: 'folder',
97
- name: 'TestResource',
98
- properties: new Map([
99
- ['assetId', '12345'],
100
- ['status', 'UnderConstruction'],
101
- ]),
102
- }}
103
- isLoading={false}
78
+ resource={mockResource({ name: 'TestResource' })}
104
79
  allowedTypes={undefined}
105
80
  modalState={previewModalState}
106
81
  previewModalOverlayProps={overlayProps}
@@ -129,16 +104,7 @@ describe('PreviewPanel', () => {
129
104
  constructFunction={(previewModalState, overlayProps) => {
130
105
  return (
131
106
  <PreviewPanel
132
- node={{ id: '1', source: '1' }}
133
- resourceDetail={{
134
- type: 'folder',
135
- name: 'TestResource',
136
- properties: new Map([
137
- ['assetId', '12345'],
138
- ['status', 'UnderConstruction'],
139
- ]),
140
- }}
141
- isLoading={false}
107
+ resource={mockResource({ name: 'TestResource' })}
142
108
  allowedTypes={undefined}
143
109
  modalState={previewModalState}
144
110
  previewModalOverlayProps={overlayProps}
@@ -160,22 +126,14 @@ describe('PreviewPanel', () => {
160
126
  it('Clicking select button return source and id of resource being shown', async () => {
161
127
  const onSelect = jest.fn();
162
128
  const onClose = jest.fn();
129
+ const resource = mockResource();
163
130
 
164
131
  render(
165
132
  <PreviewPanelTestWrapper
166
133
  constructFunction={(previewModalState, overlayProps) => {
167
134
  return (
168
135
  <PreviewPanel
169
- node={{ id: '1', source: '1' }}
170
- resourceDetail={{
171
- type: 'folder',
172
- name: 'TestResource',
173
- properties: new Map([
174
- ['assetId', '12345'],
175
- ['status', 'UnderConstruction'],
176
- ]),
177
- }}
178
- isLoading={false}
136
+ resource={resource}
179
137
  allowedTypes={undefined}
180
138
  modalState={previewModalState}
181
139
  previewModalOverlayProps={overlayProps}
@@ -191,7 +149,7 @@ describe('PreviewPanel', () => {
191
149
  user.click(screen.getByRole('button'));
192
150
 
193
151
  await waitFor(() => {
194
- expect(onSelect).toHaveBeenCalledWith({ id: '1', source: '1' });
152
+ expect(onSelect).toHaveBeenCalledWith(resource);
195
153
  });
196
154
  });
197
155
  });
@@ -10,19 +10,17 @@ export default {
10
10
  component: PreviewPanel,
11
11
  } as Meta<typeof PreviewPanel>;
12
12
 
13
- const Template: StoryFn<typeof PreviewPanel> = ({ node, resourceDetail, isLoading, allowedTypes }) => {
13
+ const Template: StoryFn<typeof PreviewPanel> = ({ resource, allowedTypes }) => {
14
14
  const previewModalState = useOverlayTriggerState({});
15
15
  const { overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
16
16
 
17
17
  return (
18
18
  <PreviewPanel
19
- node={node}
20
- resourceDetail={resourceDetail}
19
+ resource={resource}
21
20
  modalState={previewModalState}
22
21
  previewModalOverlayProps={overlayProps}
23
- isLoading={isLoading}
24
22
  allowedTypes={allowedTypes}
25
- onSelect={({ source, id }) => alert(`Resource Selected: ${source} - ${id}`)}
23
+ onSelect={({ id, name }) => alert(`Resource Selected: ${id} - ${name}`)}
26
24
  onClose={() => alert(`OnClose Selected`)}
27
25
  />
28
26
  );
@@ -30,32 +28,26 @@ const Template: StoryFn<typeof PreviewPanel> = ({ node, resourceDetail, isLoadin
30
28
 
31
29
  export const Primary = Template.bind({});
32
30
  Primary.args = {
33
- node: {
31
+ resource: {
34
32
  id: '1',
35
- source: '1',
36
- },
37
- resourceDetail: {
38
- type: 'page',
39
33
  name: 'Products',
40
- properties: new Map([
41
- ['assetId', '12345'],
42
- ['status', 'UnderConstruction'],
43
- ]),
34
+ type: {
35
+ code: 'page_standard',
36
+ name: 'Standard Page',
37
+ },
38
+ status: {
39
+ code: 'live',
40
+ name: 'Live',
41
+ },
42
+ url: 'http://my-squiz.net/assets/1',
43
+ urls: [],
44
+ childCount: 0,
44
45
  },
45
- isLoading: false,
46
- isNotSelected: false,
47
46
  allowedTypes: undefined,
48
47
  };
49
48
 
50
49
  export const NoSelected = Template.bind({});
51
50
  NoSelected.args = {
52
51
  ...Primary.args,
53
- node: null,
54
- resourceDetail: null,
55
- };
56
-
57
- export const Loading = Template.bind({});
58
- Loading.args = {
59
- ...Primary.args,
60
- isLoading: true,
52
+ resource: null,
61
53
  };
@@ -1,29 +1,24 @@
1
- import React, { useEffect, useRef } from 'react';
1
+ import React, { useEffect } from 'react';
2
2
  import { useMediaQuery } from 'react-responsive';
3
3
  import { OverlayTriggerState } from 'react-stately';
4
4
  import { DOMAttributes, FocusableElement } from '@react-types/shared';
5
5
 
6
6
  import Icon, { IconOptions } from '../Icons/Icon';
7
7
  import MatrixResource from './details/MatrixResource';
8
- import { NodeIdentifier, ResourceDetail } from '../index';
8
+ import { Resource } from '../types';
9
9
  import PreviewModal from './PreviewModal';
10
- import Spinner from '../Spinner/Spinner';
11
10
 
12
11
  export interface PreviewPanelProps {
13
- node: NodeIdentifier | null;
14
- resourceDetail: ResourceDetail | null;
15
- isLoading: boolean;
12
+ resource: Resource | null;
16
13
  modalState: OverlayTriggerState;
17
14
  previewModalOverlayProps: DOMAttributes<FocusableElement>;
18
15
  allowedTypes: string[] | undefined;
19
- onSelect: (node: NodeIdentifier) => void;
16
+ onSelect: (resource: Resource) => void;
20
17
  onClose: () => void;
21
18
  }
22
19
 
23
20
  const PreviewPanel = function ({
24
- node,
25
- resourceDetail,
26
- isLoading,
21
+ resource,
27
22
  previewModalOverlayProps,
28
23
  modalState,
29
24
  onSelect,
@@ -31,44 +26,23 @@ const PreviewPanel = function ({
31
26
  }: PreviewPanelProps) {
32
27
  // Watch the media size to see if we are on mobile size
33
28
  const isMobile = useMediaQuery({ query: '(max-width: 640px)' });
34
- const previousIsMobile = useRef<boolean | null>(null);
35
29
 
30
+ // If we are on mobile and the selected resource changes show the preview panel modal.
36
31
  useEffect(() => {
37
- if (node && isMobile) {
38
- modalState.setOpen(true);
39
- previousIsMobile.current = true;
40
- } else {
41
- previousIsMobile.current = false;
32
+ if (!modalState.isOpen) {
33
+ modalState.setOpen(Boolean(resource && isMobile));
42
34
  }
43
- }, []);
35
+ }, [resource, isMobile]);
44
36
 
45
- // If the media changes from mobile to non mobile or reverse toggle modal showing to undo the aria-hidden etc
46
- useEffect(() => {
47
- // Only trigger if we have a node selected, otherwise the modal will re-open itself as soon as its closed
48
- if (node) {
49
- if (previousIsMobile.current !== isMobile) {
50
- previousIsMobile.current = isMobile;
51
-
52
- if (isMobile) {
53
- // If no mobile open the modal automatically
54
- modalState.setOpen(true);
55
- } else {
56
- // If not in mobile close the modal automatically
57
- modalState.setOpen(false);
58
- }
59
- }
60
- }
61
- }, [node, isMobile, modalState]);
62
-
63
- let previewPanel = node && resourceDetail && (
37
+ const previewPanel = resource && (
64
38
  <>
65
39
  <div className="flex flex-col grow">
66
- <MatrixResource {...resourceDetail} />
40
+ <MatrixResource resource={resource} />
67
41
  </div>
68
42
  <div className="flex justify-end border-t border-gray-300">
69
43
  <button
70
44
  type="button"
71
- onClick={() => onSelect(node)}
45
+ onClick={() => onSelect(resource)}
72
46
  className="rounded text-sm text-white bg-blue-300 py-2 px-2.5 m-5"
73
47
  >
74
48
  Select
@@ -76,16 +50,6 @@ const PreviewPanel = function ({
76
50
  </div>
77
51
  </>
78
52
  );
79
- if (isLoading) {
80
- previewPanel = (
81
- <div className="flex flex-col grow">
82
- <div className="flex flex-col grow items-center mt-20 mx-20">
83
- <Spinner />
84
- <div className="text-sm text-gray-600 text-center mt-4">Loading info</div>
85
- </div>
86
- </div>
87
- );
88
- }
89
53
 
90
54
  return (
91
55
  <div className="sm:overflow-y-scroll sm:flex-1 sm:grow-[2] bg-white">
@@ -93,7 +57,7 @@ const PreviewPanel = function ({
93
57
  {!isMobile && <h3 className="sr-only">Resource Details</h3>}
94
58
 
95
59
  {/* Nothing selected, show an info message */}
96
- {node === null && (
60
+ {resource === null && (
97
61
  <div className="max-sm:hidden flex flex-col h-full">
98
62
  <div className="flex flex-col grow items-center mt-20 mx-20">
99
63
  <Icon icon={'resource-select' as IconOptions} aria-hidden />
@@ -108,14 +72,14 @@ const PreviewPanel = function ({
108
72
  )}
109
73
 
110
74
  {/* Resource details shows in a new modal / bottom popover on mobile size */}
111
- {node && isMobile && modalState.isOpen && (
75
+ {resource && isMobile && modalState.isOpen && (
112
76
  <PreviewModal state={modalState} overlayProps={previewModalOverlayProps} onClose={onClose}>
113
77
  {previewPanel}
114
78
  </PreviewModal>
115
79
  )}
116
80
 
117
81
  {/* If not mobile, just print the details out */}
118
- {node && !isMobile && <div className="flex flex-col h-full">{previewPanel}</div>}
82
+ {resource && !isMobile && <div className="flex flex-col h-full">{previewPanel}</div>}
119
83
  </div>
120
84
  );
121
85
  };
@@ -1,53 +1,57 @@
1
1
  import React from 'react';
2
2
 
3
- import { ResourceDetail } from '../../index';
3
+ import { Resource } from '../../types';
4
4
  import Icon, { IconOptions } from '../../Icons/Icon';
5
5
 
6
6
  const statusColour = {
7
- UnderConstruction: '#94D1F9',
8
- Live: '#BEE509',
7
+ // Duplicated from the Matrix repository.
8
+ // src/Api/AssetManagementApi/Constants/AssetStatuses.php - contains a list of possible statuses.
9
+ // frontend/src/styles/common/status-colors.scss - contains the colours used for those statuses in Matrix.
10
+ unknown: '#ff0000',
11
+ archived: '#c98a67',
12
+ under_construction: '#94d1f9',
13
+ pending_approval: '#d0bbf0',
14
+ approved_to_go_live: '#f7eaa2',
15
+ live: '#bfe60a',
16
+ up_for_review: '#72cd32',
17
+ safe_editing: '#ff97b0',
18
+ safe_editing_pending_approval: '#d688db',
19
+ safe_edit_approved_to_go_live: '#ffb34a',
9
20
  };
10
21
 
11
- export enum MatrixStatus {
12
- UnderConstruction = 'Under Construction',
13
- Live = 'Live',
14
- }
15
-
16
- export type MatrixAsset = ResourceDetail & {
17
- assetId?: string;
18
- status?: MatrixStatus;
22
+ type MatrixResourceProps = {
23
+ resource: Resource;
19
24
  };
20
25
 
21
- const MatrixResource = ({ type, name, properties }: ResourceDetail) => {
22
- const assetId = properties.get('assetId') as string;
23
- const status = properties.get('status');
26
+ const MatrixResource = ({ resource: { id, type, name, status } }: MatrixResourceProps) => {
27
+ const color = statusColour[status.code as keyof typeof statusColour] || statusColour.unknown;
24
28
 
25
29
  return (
26
30
  <div>
27
31
  <div className="flex flex-col items-center text-gray-800 mt-7 mx-5 pb-4 border-b border-gray-300">
28
- <Icon icon={type as IconOptions} resourceSource="matrix" className="w-14 h-14" />
32
+ <Icon icon={type.code as IconOptions} resourceSource="matrix" className="w-14 h-14" />
29
33
  <div className="mt-4 font-semibold text-base">{name}</div>
30
34
  </div>
31
35
  <div className="text-gray-800 mx-6 mt-4">
32
36
  <dl className="flex flex-col text-sm">
33
37
  <div className="flex mb-2">
34
38
  <dt className="w-[60px] mr-4 text-gray-600">Type</dt>
35
- <dd className="font-semibold">{type}</dd>
39
+ <dd className="font-semibold">{type.name}</dd>
36
40
  </div>
37
41
 
38
42
  <div className="flex mb-2">
39
43
  <dt className="w-[60px] mr-4 text-gray-600">Asset ID</dt>
40
- <dd className="font-semibold">#{assetId}</dd>
44
+ <dd className="font-semibold">#{id}</dd>
41
45
  </div>
42
46
 
43
47
  <div className="flex mb-2">
44
48
  <dt className="w-[60px] mr-4 text-gray-600">Status</dt>
45
49
  <dd className="flex items-center font-semibold">
46
50
  <span
47
- style={{ backgroundColor: statusColour[status as keyof typeof statusColour] }}
51
+ style={{ backgroundColor: color }}
48
52
  className="block rounded-full w-3 h-3 mr-1 border border-solid border-black border-opacity-20"
49
53
  ></span>
50
- {MatrixStatus[status as keyof typeof MatrixStatus]}
54
+ {status.name}
51
55
  </dd>
52
56
  </div>
53
57
  </dl>
@@ -7,29 +7,25 @@ import ResourceBreadcrumb from './ResourceBreadcrumb';
7
7
 
8
8
  const hierarchy = [
9
9
  {
10
- id: {
11
- id: '1',
12
- source: '1',
13
- },
10
+ key: '1',
14
11
  label: 'HandyHomes website',
12
+ node: 'A',
15
13
  },
16
14
  {
17
- id: {
18
- id: '2',
19
- source: '1',
20
- },
15
+ key: '2',
21
16
  label: 'Section 1',
17
+ node: 'B',
22
18
  },
23
19
  {
24
- id: {
25
- id: '3',
26
- source: '1',
27
- },
20
+ key: '3',
28
21
  label: 'Pages',
22
+ node: 'C',
29
23
  },
30
24
  ];
31
25
 
32
26
  describe('ResourceBreadcrumb', () => {
27
+ const onBreadcrumbSelect = jest.fn();
28
+
33
29
  it('Breadcrumb renders each item', async () => {
34
30
  render(<ResourceBreadcrumb hierarchy={hierarchy} onBreadcrumbSelect={() => {}} onReturnToRoot={() => {}} />);
35
31
 
@@ -47,8 +43,6 @@ describe('ResourceBreadcrumb', () => {
47
43
  });
48
44
 
49
45
  it('Clicking breadcrumb calls onBreadcrumbSelect', async () => {
50
- const onBreadcrumbSelect = jest.fn();
51
-
52
46
  render(
53
47
  <ResourceBreadcrumb hierarchy={hierarchy} onBreadcrumbSelect={onBreadcrumbSelect} onReturnToRoot={() => {}} />,
54
48
  );
@@ -57,7 +51,7 @@ describe('ResourceBreadcrumb', () => {
57
51
  user.click(screen.getByRole('button', { name: 'HandyHomes website' }));
58
52
 
59
53
  await waitFor(() => {
60
- expect(onBreadcrumbSelect).toHaveBeenCalledWith({ source: '1', id: '1' });
54
+ expect(onBreadcrumbSelect).toHaveBeenCalledWith(hierarchy[0].node);
61
55
  });
62
56
  });
63
57
 
@@ -80,18 +74,14 @@ describe('ResourceBreadcrumb', () => {
80
74
  hierarchy={[
81
75
  ...hierarchy,
82
76
  {
83
- id: {
84
- id: '4',
85
- source: '1',
86
- },
77
+ key: '4',
87
78
  label: 'Item 4',
79
+ node: 'D',
88
80
  },
89
81
  {
90
- id: {
91
- id: '5',
92
- source: '1',
93
- },
82
+ key: '5',
94
83
  label: 'Item 5',
84
+ node: 'E',
95
85
  },
96
86
  ]}
97
87
  onBreadcrumbSelect={() => {}}
@@ -12,7 +12,7 @@ export default {
12
12
  const Template: StoryFn<typeof ResourceBreadcrumb> = ({ hierarchy }) => (
13
13
  <ResourceBreadcrumb
14
14
  hierarchy={hierarchy}
15
- onBreadcrumbSelect={({ source, id }) => alert(`Breadcrumb Select: ${source} - ${id}`)}
15
+ onBreadcrumbSelect={(node) => alert(`Node Select: ${node}`)}
16
16
  onReturnToRoot={() => alert(`Return to Root`)}
17
17
  />
18
18
  );
@@ -1,16 +1,15 @@
1
1
  import React, { useState, useCallback, useEffect } from 'react';
2
2
 
3
3
  import Icon, { IconOptions } from '../Icons/Icon';
4
+ import { Hierarchy } from '../types';
4
5
 
5
- import { NodeIdentifier, Hierarchy } from '../index';
6
-
7
- export interface ResourceBreadcrumbProps {
8
- hierarchy: Array<Hierarchy>;
9
- onBreadcrumbSelect: (node: NodeIdentifier) => void;
6
+ export interface ResourceBreadcrumbProps<T> {
7
+ hierarchy: Hierarchy<T>;
8
+ onBreadcrumbSelect: (node: T) => void;
10
9
  onReturnToRoot: () => void;
11
10
  }
12
11
 
13
- const ResourceBreadcrumb = function ({ hierarchy, onBreadcrumbSelect, onReturnToRoot }: ResourceBreadcrumbProps) {
12
+ const ResourceBreadcrumb = function <T>({ hierarchy, onBreadcrumbSelect, onReturnToRoot }: ResourceBreadcrumbProps<T>) {
14
13
  // Track expanded state
15
14
  const [expandedHierarchy, setExpandedHierarchy] = useState(false);
16
15
 
@@ -37,14 +36,14 @@ const ResourceBreadcrumb = function ({ hierarchy, onBreadcrumbSelect, onReturnTo
37
36
  <Icon icon={'root' as IconOptions} aria-label="Return to source list" className="" />
38
37
  </button>
39
38
  </li>
40
- {hierarchy.map(({ id, label }, index) => {
39
+ {hierarchy.map(({ key, label, node }, index) => {
41
40
  return (
42
41
  <li
43
- key={`${id.source}-${id.id}`}
42
+ key={`${key}`}
44
43
  className="resource-breadcrumb__item max-md:hidden flex items-center mr-2 before:content-['/'] before:mr-2"
45
44
  >
46
45
  {index !== hierarchy.length - 1 && (
47
- <button type="button" onClick={() => onBreadcrumbSelect(id)}>
46
+ <button type="button" onClick={() => onBreadcrumbSelect(node)}>
48
47
  <div className={`resource-breadcrumb__label`} title={label}>
49
48
  {label}
50
49
  </div>
@@ -1,37 +1,27 @@
1
1
  [
2
2
  {
3
- "id": {
4
- "id": "1",
5
- "source": "1"
6
- },
7
- "label": "HandyHomes website"
3
+ "key": "1",
4
+ "label": "HandyHomes website",
5
+ "node": "A"
8
6
  },
9
7
  {
10
- "id": {
11
- "id": "2",
12
- "source": "1"
13
- },
14
- "label": "Section 1 very long name"
8
+ "key": "2",
9
+ "label": "Section 1 very long name",
10
+ "node": "B"
15
11
  },
16
12
  {
17
- "id": {
18
- "id": "4",
19
- "source": "1"
20
- },
21
- "label": "Page 1 very long name"
13
+ "key": "3",
14
+ "label": "Page 1 very long name",
15
+ "node": "C"
22
16
  },
23
17
  {
24
- "id": {
25
- "id": "5",
26
- "source": "1"
27
- },
28
- "label": "Page 71 very long name"
18
+ "key": "4",
19
+ "label": "Page 71 very long name",
20
+ "node": "D"
29
21
  },
30
22
  {
31
- "id": {
32
- "id": "3",
33
- "source": "1"
34
- },
35
- "label": "Pages very long name"
23
+ "key": "5",
24
+ "label": "Pages very long name",
25
+ "node": "E"
36
26
  }
37
27
  ]
@@ -4,25 +4,23 @@ import { useOverlayTrigger } from 'react-aria';
4
4
  import { OverlayTriggerState } from 'react-stately';
5
5
 
6
6
  import Icon, { IconOptions } from '../Icons/Icon';
7
- import { NodeIdentifier } from '../index';
8
7
  import ModalOpeningButton from '../Modal/ModalOpeningButton';
9
8
 
10
- interface ResourceItem {
11
- key: string;
12
- id: NodeIdentifier;
13
- selected: boolean;
9
+ interface ResourceItem<T> {
10
+ item: T;
11
+ selected?: boolean;
14
12
  label: string;
15
13
  type: string;
16
14
  childCount: number;
17
15
  previewModalState: OverlayTriggerState;
18
- onSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
19
- onDrillDown: (node: NodeIdentifier) => void;
16
+ onSelect: (node: T, overlayProps: DOMAttributes<FocusableElement>) => void;
17
+ onDrillDown: (node: T) => void;
20
18
  className: string;
21
19
  allowedTypes?: string[] | undefined;
22
20
  }
23
21
 
24
- const ResourceItem = ({
25
- id,
22
+ const ResourceItem = <T,>({
23
+ item,
26
24
  selected,
27
25
  label,
28
26
  type,
@@ -32,7 +30,7 @@ const ResourceItem = ({
32
30
  onDrillDown,
33
31
  className,
34
32
  allowedTypes,
35
- }: ResourceItem) => {
33
+ }: ResourceItem<T>) => {
36
34
  const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
37
35
  const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(type);
38
36
  const title = isDisabled ? "You can't select this item" : label;
@@ -43,7 +41,7 @@ const ResourceItem = ({
43
41
  type="button"
44
42
  {...triggerProps}
45
43
  isDisabled={isDisabled}
46
- onPress={() => onSelect(id, overlayProps)}
44
+ onPress={() => onSelect(item, overlayProps)}
47
45
  className={`
48
46
  relative grow flex items-center p-4 rounded ${selected ? 'bg-blue-100 text-blue-400' : ''} ${
49
47
  childCount > 0 ? 'mr-2' : ''
@@ -64,7 +62,7 @@ const ResourceItem = ({
64
62
  <button
65
63
  type="button"
66
64
  aria-label={`Drill down to ${label} children`}
67
- onClick={() => onDrillDown(id)}
65
+ onClick={() => onDrillDown(item)}
68
66
  className={`relative shrink-0 flex items-center p-4 rounded before:w-px before:h-[calc(100%-0.75rem)] before:bg-gray-200 before:absolute before:top-1.5 before:-left-1 hover:bg-gray-100 focus:bg-gray-100`}
69
67
  >
70
68
  <span className="ml-auto flex items-center">