@squiz/resource-browser 1.32.1-alpha.12
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/.storybook/main.ts +23 -0
- package/.storybook/preview-head.html +15 -0
- package/.storybook/preview.ts +16 -0
- package/build.js +21 -0
- package/jest.config.ts +18 -0
- package/lib/Icons/Generics/ArrowDown.d.ts +15 -0
- package/lib/Icons/Generics/ArrowDown.js +23 -0
- package/lib/Icons/Generics/ArrowRight.d.ts +15 -0
- package/lib/Icons/Generics/ArrowRight.js +23 -0
- package/lib/Icons/Generics/Close.d.ts +15 -0
- package/lib/Icons/Generics/Close.js +23 -0
- package/lib/Icons/Generics/GenericIconMap.d.ts +10 -0
- package/lib/Icons/Generics/GenericIconMap.js +14 -0
- package/lib/Icons/Generics/ResourceSelect.d.ts +15 -0
- package/lib/Icons/Generics/ResourceSelect.js +28 -0
- package/lib/Icons/Generics/Root.d.ts +15 -0
- package/lib/Icons/Generics/Root.js +23 -0
- package/lib/Icons/Generics/Selected.d.ts +15 -0
- package/lib/Icons/Generics/Selected.js +23 -0
- package/lib/Icons/Generics/index.d.ts +6 -0
- package/lib/Icons/Generics/index.js +19 -0
- package/lib/Icons/Icon.d.ts +47 -0
- package/lib/Icons/Icon.js +44 -0
- package/lib/Icons/MatrixResources/Audio.d.ts +15 -0
- package/lib/Icons/MatrixResources/Audio.js +28 -0
- package/lib/Icons/MatrixResources/Excel.d.ts +15 -0
- package/lib/Icons/MatrixResources/Excel.js +27 -0
- package/lib/Icons/MatrixResources/Folder.d.ts +15 -0
- package/lib/Icons/MatrixResources/Folder.js +24 -0
- package/lib/Icons/MatrixResources/GenericFile.d.ts +15 -0
- package/lib/Icons/MatrixResources/GenericFile.js +28 -0
- package/lib/Icons/MatrixResources/Image.d.ts +15 -0
- package/lib/Icons/MatrixResources/Image.js +26 -0
- package/lib/Icons/MatrixResources/MatrixResourceMap.d.ts +15 -0
- package/lib/Icons/MatrixResources/MatrixResourceMap.js +19 -0
- package/lib/Icons/MatrixResources/Page.d.ts +15 -0
- package/lib/Icons/MatrixResources/Page.js +30 -0
- package/lib/Icons/MatrixResources/Pdf.d.ts +15 -0
- package/lib/Icons/MatrixResources/Pdf.js +31 -0
- package/lib/Icons/MatrixResources/Powerpoint.d.ts +15 -0
- package/lib/Icons/MatrixResources/Powerpoint.js +28 -0
- package/lib/Icons/MatrixResources/Site.d.ts +15 -0
- package/lib/Icons/MatrixResources/Site.js +30 -0
- package/lib/Icons/MatrixResources/Video.d.ts +15 -0
- package/lib/Icons/MatrixResources/Video.js +24 -0
- package/lib/Icons/MatrixResources/Word.d.ts +17 -0
- package/lib/Icons/MatrixResources/Word.js +28 -0
- package/lib/Icons/MatrixResources/index.d.ts +11 -0
- package/lib/Icons/MatrixResources/index.js +29 -0
- package/lib/Modal/Modal.d.ts +11 -0
- package/lib/Modal/Modal.js +46 -0
- package/lib/Modal/ModalOpeningButton.d.ts +10 -0
- package/lib/Modal/ModalOpeningButton.js +13 -0
- package/lib/Modal/ModalTrigger.d.ts +9 -0
- package/lib/Modal/ModalTrigger.js +24 -0
- package/lib/PreviewPanel/PreviewModal.d.ts +11 -0
- package/lib/PreviewPanel/PreviewModal.js +81 -0
- package/lib/PreviewPanel/PreviewPanel.d.ts +16 -0
- package/lib/PreviewPanel/PreviewPanel.js +87 -0
- package/lib/PreviewPanel/details/MatrixResource.d.ts +12 -0
- package/lib/PreviewPanel/details/MatrixResource.js +41 -0
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +9 -0
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.js +20 -0
- package/lib/ResourceItem/ResourceItem.d.ts +19 -0
- package/lib/ResourceItem/ResourceItem.js +26 -0
- package/lib/ResourceList/ResourceList.d.ts +14 -0
- package/lib/ResourceList/ResourceList.js +51 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +15 -0
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +145 -0
- package/lib/Skeleton/List/SkeletonList.d.ts +6 -0
- package/lib/Skeleton/List/SkeletonList.js +13 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.d.ts +2 -0
- package/lib/Skeleton/ListItem/SkeletonListItem.js +15 -0
- package/lib/SourceDropdown/SourceDropdown.d.ts +9 -0
- package/lib/SourceDropdown/SourceDropdown.js +106 -0
- package/lib/SourceList/SourceList.d.ts +14 -0
- package/lib/SourceList/SourceList.js +58 -0
- package/lib/Spinner/Spinner.d.ts +8 -0
- package/lib/Spinner/Spinner.js +12 -0
- package/lib/index.css +968 -0
- package/lib/index.d.ts +37 -0
- package/lib/index.js +15 -0
- package/lib/uuid.d.ts +1 -0
- package/lib/uuid.js +8 -0
- package/package.json +74 -0
- package/postcss.config.js +11 -0
- package/src/Icons/Generics/ArrowDown.tsx +27 -0
- package/src/Icons/Generics/ArrowRight.tsx +27 -0
- package/src/Icons/Generics/Close.tsx +26 -0
- package/src/Icons/Generics/GenericIconMap.ts +14 -0
- package/src/Icons/Generics/ResourceSelect.tsx +40 -0
- package/src/Icons/Generics/Root.tsx +24 -0
- package/src/Icons/Generics/Selected.tsx +27 -0
- package/src/Icons/Generics/index.tsx +7 -0
- package/src/Icons/Icon.spec.tsx +62 -0
- package/src/Icons/Icon.stories.tsx +105 -0
- package/src/Icons/Icon.tsx +61 -0
- package/src/Icons/MatrixResources/Audio.tsx +30 -0
- package/src/Icons/MatrixResources/Excel.tsx +29 -0
- package/src/Icons/MatrixResources/Folder.tsx +29 -0
- package/src/Icons/MatrixResources/GenericFile.tsx +34 -0
- package/src/Icons/MatrixResources/Image.tsx +36 -0
- package/src/Icons/MatrixResources/MatrixResourceMap.ts +19 -0
- package/src/Icons/MatrixResources/Page.tsx +33 -0
- package/src/Icons/MatrixResources/Pdf.tsx +34 -0
- package/src/Icons/MatrixResources/Powerpoint.tsx +34 -0
- package/src/Icons/MatrixResources/Site.tsx +37 -0
- package/src/Icons/MatrixResources/Video.tsx +27 -0
- package/src/Icons/MatrixResources/Word.tsx +30 -0
- package/src/Icons/MatrixResources/index.tsx +12 -0
- package/src/Modal/Modal.spec.tsx +244 -0
- package/src/Modal/Modal.tsx +58 -0
- package/src/Modal/ModalContainer.stories.tsx +33 -0
- package/src/Modal/ModalOpeningButton.tsx +20 -0
- package/src/Modal/ModalTrigger.tsx +45 -0
- package/src/PreviewPanel/PreviewModal.spec.tsx +164 -0
- package/src/PreviewPanel/PreviewModal.tsx +92 -0
- package/src/PreviewPanel/PreviewPanel.spec.tsx +197 -0
- package/src/PreviewPanel/PreviewPanel.stories.tsx +61 -0
- package/src/PreviewPanel/PreviewPanel.tsx +123 -0
- package/src/PreviewPanel/details/MatrixResource.tsx +59 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.spec.tsx +76 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.stories.tsx +24 -0
- package/src/ResourceBreadcrumb/ResourceBreadcrumb.tsx +39 -0
- package/src/ResourceBreadcrumb/sample-hierarchy.json +23 -0
- package/src/ResourceItem/ResourceItem.spec.tsx +69 -0
- package/src/ResourceItem/ResourceItem.tsx +82 -0
- package/src/ResourceList/ResourceList.spec.tsx +196 -0
- package/src/ResourceList/ResourceList.stories.tsx +40 -0
- package/src/ResourceList/ResourceList.tsx +74 -0
- package/src/ResourceList/sample-resources.json +75 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.spec.tsx +706 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +56 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +224 -0
- package/src/Skeleton/List/SkeletonList.spec.tsx +18 -0
- package/src/Skeleton/List/SkeletonList.stories.tsx +15 -0
- package/src/Skeleton/List/SkeletonList.tsx +16 -0
- package/src/Skeleton/ListItem/SkeletonListItem.stories.tsx +15 -0
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +14 -0
- package/src/SourceDropdown/SourceDropdown.spec.tsx +263 -0
- package/src/SourceDropdown/SourceDropdown.stories.tsx +36 -0
- package/src/SourceDropdown/SourceDropdown.tsx +175 -0
- package/src/SourceDropdown/sample-sources.json +110 -0
- package/src/SourceList/SourceList.spec.tsx +224 -0
- package/src/SourceList/SourceList.stories.tsx +40 -0
- package/src/SourceList/SourceList.tsx +93 -0
- package/src/SourceList/sample-sources.json +110 -0
- package/src/Spinner/Spinner.spec.tsx +18 -0
- package/src/Spinner/Spinner.stories.tsx +26 -0
- package/src/Spinner/Spinner.tsx +18 -0
- package/src/Spinner/_spinner.scss +11 -0
- package/src/__mocks__/JestHelpers.ts +65 -0
- package/src/__mocks__/jestHelpers.spec.ts +38 -0
- package/src/__mocks__/styleMock.ts +1 -0
- package/src/index.scss +7 -0
- package/src/index.stories.tsx +70 -0
- package/src/index.tsx +71 -0
- package/src/uuid.ts +7 -0
- package/tailwind.config.cjs +84 -0
- package/tsconfig.json +22 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import { ResourceDetail } from '../../index';
|
4
|
+
import Icon, { IconOptions } from '../../Icons/Icon';
|
5
|
+
|
6
|
+
const statusColour = {
|
7
|
+
UnderConstruction: '#94D1F9',
|
8
|
+
Live: '#BEE509',
|
9
|
+
};
|
10
|
+
|
11
|
+
export enum MatrixStatus {
|
12
|
+
UnderConstruction = 'Under Construction',
|
13
|
+
Live = 'Live',
|
14
|
+
}
|
15
|
+
|
16
|
+
export type MatrixAsset = ResourceDetail & {
|
17
|
+
assetId?: string;
|
18
|
+
status?: MatrixStatus;
|
19
|
+
};
|
20
|
+
|
21
|
+
const MatrixResource = ({ type, name, properties }: ResourceDetail) => {
|
22
|
+
const assetId = properties.get('assetId') as string;
|
23
|
+
const status = properties.get('status');
|
24
|
+
|
25
|
+
return (
|
26
|
+
<div>
|
27
|
+
<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" />
|
29
|
+
<div className="mt-4 font-semibold text-base">{name}</div>
|
30
|
+
</div>
|
31
|
+
<div className="text-gray-800 mx-6 mt-4">
|
32
|
+
<dl className="flex flex-col text-sm">
|
33
|
+
<div className="flex mb-2">
|
34
|
+
<dt className="w-[60px] mr-4 text-gray-600">Type</dt>
|
35
|
+
<dd className="font-semibold">{type}</dd>
|
36
|
+
</div>
|
37
|
+
|
38
|
+
<div className="flex mb-2">
|
39
|
+
<dt className="w-[60px] mr-4 text-gray-600">Asset ID</dt>
|
40
|
+
<dd className="font-semibold">#{assetId}</dd>
|
41
|
+
</div>
|
42
|
+
|
43
|
+
<div className="flex mb-2">
|
44
|
+
<dt className="w-[60px] mr-4 text-gray-600">Status</dt>
|
45
|
+
<dd className="flex items-center font-semibold">
|
46
|
+
<span
|
47
|
+
style={{ backgroundColor: statusColour[status as keyof typeof statusColour] }}
|
48
|
+
className="block rounded-full w-3 h-3 mr-1 border border-solid border-black border-opacity-20"
|
49
|
+
></span>
|
50
|
+
{MatrixStatus[status as keyof typeof MatrixStatus]}
|
51
|
+
</dd>
|
52
|
+
</div>
|
53
|
+
</dl>
|
54
|
+
</div>
|
55
|
+
</div>
|
56
|
+
);
|
57
|
+
};
|
58
|
+
|
59
|
+
export default MatrixResource;
|
@@ -0,0 +1,76 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
2
|
+
import React from 'react';
|
3
|
+
import { screen, render, waitFor } from '@testing-library/react';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
5
|
+
|
6
|
+
import ResourceBreadcrumb from './ResourceBreadcrumb';
|
7
|
+
|
8
|
+
const hierarchy = [
|
9
|
+
{
|
10
|
+
id: {
|
11
|
+
id: '1',
|
12
|
+
source: '1',
|
13
|
+
},
|
14
|
+
label: 'HandyHomes website',
|
15
|
+
},
|
16
|
+
{
|
17
|
+
id: {
|
18
|
+
id: '2',
|
19
|
+
source: '1',
|
20
|
+
},
|
21
|
+
label: 'Section 1',
|
22
|
+
},
|
23
|
+
{
|
24
|
+
id: {
|
25
|
+
id: '3',
|
26
|
+
source: '1',
|
27
|
+
},
|
28
|
+
label: 'Pages',
|
29
|
+
},
|
30
|
+
];
|
31
|
+
|
32
|
+
describe('ResourceBreadcrumb', () => {
|
33
|
+
it('Breadcrumb renders each item', async () => {
|
34
|
+
render(<ResourceBreadcrumb hierarchy={hierarchy} onBreadcrumbSelect={() => {}} onReturnToRoot={() => {}} />);
|
35
|
+
|
36
|
+
await waitFor(() => {
|
37
|
+
expect(screen.getByText('HandyHomes website')).toBeTruthy();
|
38
|
+
expect(screen.getByText('HandyHomes website').tagName.toLocaleLowerCase()).toEqual('button');
|
39
|
+
|
40
|
+
expect(screen.getByText('Section 1')).toBeTruthy();
|
41
|
+
expect(screen.getByText('Section 1').tagName.toLocaleLowerCase()).toEqual('button');
|
42
|
+
|
43
|
+
// Last item isn't a button
|
44
|
+
expect(screen.getByText('Pages')).toBeTruthy();
|
45
|
+
expect(screen.getByText('Pages').tagName.toLocaleLowerCase()).toEqual('span');
|
46
|
+
});
|
47
|
+
});
|
48
|
+
|
49
|
+
it('Clicking breadcrumb calls onBreadcrumbSelect', async () => {
|
50
|
+
const onBreadcrumbSelect = jest.fn();
|
51
|
+
|
52
|
+
render(
|
53
|
+
<ResourceBreadcrumb hierarchy={hierarchy} onBreadcrumbSelect={onBreadcrumbSelect} onReturnToRoot={() => {}} />,
|
54
|
+
);
|
55
|
+
|
56
|
+
const user = userEvent.setup();
|
57
|
+
user.click(screen.getByRole('button', { name: 'HandyHomes website' }));
|
58
|
+
|
59
|
+
await waitFor(() => {
|
60
|
+
expect(onBreadcrumbSelect).toHaveBeenCalledWith({ source: '1', id: '1' });
|
61
|
+
});
|
62
|
+
});
|
63
|
+
|
64
|
+
it('Clicking root icon calls onReturnToRoot', async () => {
|
65
|
+
const onReturnToRoot = jest.fn();
|
66
|
+
|
67
|
+
render(<ResourceBreadcrumb hierarchy={hierarchy} onBreadcrumbSelect={() => {}} onReturnToRoot={onReturnToRoot} />);
|
68
|
+
|
69
|
+
const user = userEvent.setup();
|
70
|
+
user.click(screen.getByRole('button', { name: 'Return to source list' }));
|
71
|
+
|
72
|
+
await waitFor(() => {
|
73
|
+
expect(onReturnToRoot).toHaveBeenCalled();
|
74
|
+
});
|
75
|
+
});
|
76
|
+
});
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import ResourceBreadcrumb from './ResourceBreadcrumb';
|
5
|
+
import sampleHierarchy from './sample-hierarchy.json';
|
6
|
+
|
7
|
+
export default {
|
8
|
+
title: 'Resource Breadcrumb',
|
9
|
+
component: ResourceBreadcrumb,
|
10
|
+
} as Meta<typeof ResourceBreadcrumb>;
|
11
|
+
|
12
|
+
const Template: StoryFn<typeof ResourceBreadcrumb> = ({ hierarchy }) => (
|
13
|
+
<ResourceBreadcrumb
|
14
|
+
hierarchy={hierarchy}
|
15
|
+
onBreadcrumbSelect={({ source, id }) => alert(`Breadcrumb Select: ${source} - ${id}`)}
|
16
|
+
onReturnToRoot={() => alert(`Return to Root`)}
|
17
|
+
/>
|
18
|
+
);
|
19
|
+
|
20
|
+
export const Primary = Template.bind({});
|
21
|
+
|
22
|
+
Primary.args = {
|
23
|
+
hierarchy: sampleHierarchy,
|
24
|
+
};
|
@@ -0,0 +1,39 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import Icon, { IconOptions } from '../Icons/Icon';
|
4
|
+
|
5
|
+
import { NodeIdentifier, Hierarchy } from '../index';
|
6
|
+
|
7
|
+
export interface ResourceBreadcrumbProps {
|
8
|
+
hierarchy: Array<Hierarchy>;
|
9
|
+
onBreadcrumbSelect: (node: NodeIdentifier) => void;
|
10
|
+
onReturnToRoot: () => void;
|
11
|
+
}
|
12
|
+
|
13
|
+
const ResourceBreadcrumb = function ({ hierarchy, onBreadcrumbSelect, onReturnToRoot }: ResourceBreadcrumbProps) {
|
14
|
+
return (
|
15
|
+
<nav aria-label="Resource breadcrumb" className="text-sm text-gray-600 px-7 my-5">
|
16
|
+
<ol className="flex items-center">
|
17
|
+
<li className="flex items-center mr-3">
|
18
|
+
<button type="button" onClick={onReturnToRoot}>
|
19
|
+
<Icon icon={'root' as IconOptions} aria-label="Return to source list" className="" />
|
20
|
+
</button>
|
21
|
+
</li>
|
22
|
+
{hierarchy.map(({ id, label }, index) => {
|
23
|
+
return (
|
24
|
+
<li key={`${id.source}-${id.id}`} className="mr-2 before:content-['/'] before:mr-2">
|
25
|
+
{index !== hierarchy.length - 1 && (
|
26
|
+
<button type="button" onClick={() => onBreadcrumbSelect(id)}>
|
27
|
+
{label}
|
28
|
+
</button>
|
29
|
+
)}
|
30
|
+
{index === hierarchy.length - 1 && <span className="font-semibold">{label}</span>}
|
31
|
+
</li>
|
32
|
+
);
|
33
|
+
})}
|
34
|
+
</ol>
|
35
|
+
</nav>
|
36
|
+
);
|
37
|
+
};
|
38
|
+
|
39
|
+
export default ResourceBreadcrumb;
|
@@ -0,0 +1,23 @@
|
|
1
|
+
[
|
2
|
+
{
|
3
|
+
"id": {
|
4
|
+
"id": "1",
|
5
|
+
"source": "1"
|
6
|
+
},
|
7
|
+
"label": "HandyHomes website"
|
8
|
+
},
|
9
|
+
{
|
10
|
+
"id": {
|
11
|
+
"id": "2",
|
12
|
+
"source": "1"
|
13
|
+
},
|
14
|
+
"label": "Section 1"
|
15
|
+
},
|
16
|
+
{
|
17
|
+
"id": {
|
18
|
+
"id": "3",
|
19
|
+
"source": "1"
|
20
|
+
},
|
21
|
+
"label": "Pages"
|
22
|
+
}
|
23
|
+
]
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, fireEvent } from '@testing-library/react';
|
3
|
+
import ResourceItem from './ResourceItem';
|
4
|
+
|
5
|
+
const mockOnSelect = jest.fn();
|
6
|
+
const mockOnDrillDown = jest.fn();
|
7
|
+
const previewModalState = jest.fn();
|
8
|
+
|
9
|
+
const defaultProps: any = {
|
10
|
+
id: '123',
|
11
|
+
selected: false,
|
12
|
+
label: 'My Resource',
|
13
|
+
type: 'site',
|
14
|
+
childCount: 0,
|
15
|
+
previewModalState: previewModalState,
|
16
|
+
onSelect: mockOnSelect,
|
17
|
+
onDrillDown: mockOnDrillDown,
|
18
|
+
className: '',
|
19
|
+
};
|
20
|
+
|
21
|
+
describe('ResourceItem', () => {
|
22
|
+
afterEach(() => {
|
23
|
+
jest.clearAllMocks();
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should render the component with the correct label', () => {
|
27
|
+
const { getByText } = render(<ResourceItem {...defaultProps} />);
|
28
|
+
const labelElement = getByText('My Resource');
|
29
|
+
|
30
|
+
expect(labelElement).toBeInTheDocument();
|
31
|
+
});
|
32
|
+
|
33
|
+
it('should call the onSelect function when the button is pressed', () => {
|
34
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} />);
|
35
|
+
const buttonElement = getByRole('button');
|
36
|
+
fireEvent.click(buttonElement);
|
37
|
+
|
38
|
+
expect(mockOnSelect).toHaveBeenCalledTimes(1);
|
39
|
+
});
|
40
|
+
|
41
|
+
it('should call the onDrillDown function when the drill down button is pressed', () => {
|
42
|
+
const { getByLabelText } = render(<ResourceItem {...defaultProps} childCount={1} />);
|
43
|
+
const drillDownButtonElement = getByLabelText('Drill down to My Resource children');
|
44
|
+
fireEvent.click(drillDownButtonElement);
|
45
|
+
|
46
|
+
expect(mockOnDrillDown).toHaveBeenCalledTimes(1);
|
47
|
+
});
|
48
|
+
|
49
|
+
it('should disable the button when the allowedTypes prop is not in the list of allowedItems', () => {
|
50
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} allowedTypes={['page']} />);
|
51
|
+
const buttonElement = getByRole('button');
|
52
|
+
|
53
|
+
expect(buttonElement).toBeDisabled();
|
54
|
+
});
|
55
|
+
|
56
|
+
it('should not disable the button when the allowedTypes prop is in the list of allowedItems', () => {
|
57
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} allowedTypes={['site']} />);
|
58
|
+
const buttonElement = getByRole('button');
|
59
|
+
|
60
|
+
expect(buttonElement).not.toBeDisabled();
|
61
|
+
});
|
62
|
+
|
63
|
+
it('should not disable the button when the allowedTypes is undefined', () => {
|
64
|
+
const { getByRole } = render(<ResourceItem {...defaultProps} />);
|
65
|
+
const buttonElement = getByRole('button');
|
66
|
+
|
67
|
+
expect(buttonElement).not.toBeDisabled();
|
68
|
+
});
|
69
|
+
});
|
@@ -0,0 +1,82 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
3
|
+
import { useOverlayTrigger } from 'react-aria';
|
4
|
+
import { OverlayTriggerState } from 'react-stately';
|
5
|
+
|
6
|
+
import Icon, { IconOptions } from '../Icons/Icon';
|
7
|
+
import { NodeIdentifier } from '../index';
|
8
|
+
import ModalOpeningButton from '../Modal/ModalOpeningButton';
|
9
|
+
|
10
|
+
interface ResourceItem {
|
11
|
+
key: string;
|
12
|
+
id: NodeIdentifier;
|
13
|
+
selected: boolean;
|
14
|
+
label: string;
|
15
|
+
type: string;
|
16
|
+
childCount: number;
|
17
|
+
previewModalState: OverlayTriggerState;
|
18
|
+
onSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
|
19
|
+
onDrillDown: (node: NodeIdentifier) => void;
|
20
|
+
className: string;
|
21
|
+
allowedTypes?: string[] | undefined;
|
22
|
+
}
|
23
|
+
|
24
|
+
const ResourceItem = ({
|
25
|
+
id,
|
26
|
+
selected,
|
27
|
+
label,
|
28
|
+
type,
|
29
|
+
childCount,
|
30
|
+
previewModalState,
|
31
|
+
onSelect,
|
32
|
+
onDrillDown,
|
33
|
+
className,
|
34
|
+
allowedTypes,
|
35
|
+
}: ResourceItem) => {
|
36
|
+
const { triggerProps, overlayProps } = useOverlayTrigger({ type: 'dialog' }, previewModalState);
|
37
|
+
const isDisabled = allowedTypes !== undefined && !allowedTypes.includes(type);
|
38
|
+
const title = isDisabled ? "You can't select this item" : label;
|
39
|
+
|
40
|
+
return (
|
41
|
+
<li className={`flex items-stretch p-1 bg-white border border-grey-200 ${className}`}>
|
42
|
+
<ModalOpeningButton
|
43
|
+
type="button"
|
44
|
+
{...triggerProps}
|
45
|
+
isDisabled={isDisabled}
|
46
|
+
onPress={() => onSelect(id, overlayProps)}
|
47
|
+
className={`
|
48
|
+
relative grow flex items-center p-4 rounded ${selected ? 'bg-blue-100 text-blue-400' : ''} ${
|
49
|
+
childCount > 0 ? 'mr-2' : ''
|
50
|
+
} ${isDisabled ? 'font-normal text-gray-600 cursor-not-allowed' : 'hover:bg-gray-100 focus:bg-gray-100'}
|
51
|
+
`}
|
52
|
+
title={title}
|
53
|
+
>
|
54
|
+
<Icon
|
55
|
+
icon={type as IconOptions}
|
56
|
+
resourceSource="matrix"
|
57
|
+
aria-label={type}
|
58
|
+
className={`mr-4 shrink-0 ${isDisabled && 'opacity-40'}`}
|
59
|
+
/>
|
60
|
+
<span className="text-left break-all">{label}</span>
|
61
|
+
{childCount <= 0 && <Icon icon={'arrow-right' as IconOptions} className="absolute right-5" />}
|
62
|
+
</ModalOpeningButton>
|
63
|
+
{childCount > 0 && (
|
64
|
+
<button
|
65
|
+
type="button"
|
66
|
+
aria-label={`Drill down to ${label} children`}
|
67
|
+
onClick={() => onDrillDown(id)}
|
68
|
+
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
|
+
>
|
70
|
+
<span className="ml-auto flex items-center">
|
71
|
+
<span className="truncate w-10 text-right" title={String(childCount)}>
|
72
|
+
{childCount}
|
73
|
+
</span>
|
74
|
+
<Icon icon={'arrow-right' as IconOptions} className="ml-1" />
|
75
|
+
</span>
|
76
|
+
</button>
|
77
|
+
)}
|
78
|
+
</li>
|
79
|
+
);
|
80
|
+
};
|
81
|
+
|
82
|
+
export default ResourceItem;
|
@@ -0,0 +1,196 @@
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-empty-function */
|
2
|
+
import React from 'react';
|
3
|
+
import { screen, render, waitFor } from '@testing-library/react';
|
4
|
+
import userEvent from '@testing-library/user-event';
|
5
|
+
|
6
|
+
import { useOverlayTriggerState, OverlayTriggerState } from 'react-stately';
|
7
|
+
import ResourceList from './ResourceList';
|
8
|
+
|
9
|
+
const resources = [
|
10
|
+
{
|
11
|
+
id: {
|
12
|
+
id: '1',
|
13
|
+
source: '1',
|
14
|
+
},
|
15
|
+
type: 'page',
|
16
|
+
selected: false,
|
17
|
+
label: 'Resource 1',
|
18
|
+
childCount: 0,
|
19
|
+
},
|
20
|
+
{
|
21
|
+
id: {
|
22
|
+
id: '2',
|
23
|
+
source: '1',
|
24
|
+
},
|
25
|
+
type: 'page',
|
26
|
+
selected: false,
|
27
|
+
label: 'Resource 2',
|
28
|
+
childCount: 0,
|
29
|
+
},
|
30
|
+
{
|
31
|
+
id: {
|
32
|
+
id: '3',
|
33
|
+
source: '1',
|
34
|
+
},
|
35
|
+
type: 'folder',
|
36
|
+
selected: false,
|
37
|
+
label: 'Resource 3',
|
38
|
+
childCount: 31,
|
39
|
+
},
|
40
|
+
{
|
41
|
+
id: {
|
42
|
+
id: '4',
|
43
|
+
source: '1',
|
44
|
+
},
|
45
|
+
type: 'folder',
|
46
|
+
selected: false,
|
47
|
+
label: 'Resource 4',
|
48
|
+
childCount: 55,
|
49
|
+
},
|
50
|
+
{
|
51
|
+
id: {
|
52
|
+
id: '5',
|
53
|
+
source: '1',
|
54
|
+
},
|
55
|
+
type: 'docx',
|
56
|
+
selected: false,
|
57
|
+
label: 'Resource 5',
|
58
|
+
childCount: 0,
|
59
|
+
},
|
60
|
+
];
|
61
|
+
|
62
|
+
function ResourceListTestWrapper({
|
63
|
+
constructFunction,
|
64
|
+
}: {
|
65
|
+
constructFunction: (previewModalState: OverlayTriggerState) => JSX.Element;
|
66
|
+
}) {
|
67
|
+
const previewModalState = useOverlayTriggerState({});
|
68
|
+
return constructFunction(previewModalState);
|
69
|
+
}
|
70
|
+
|
71
|
+
describe('ResourceList', () => {
|
72
|
+
it('Shows loading when isLoading true', async () => {
|
73
|
+
render(
|
74
|
+
<ResourceListTestWrapper
|
75
|
+
constructFunction={(previewModalState) => {
|
76
|
+
return (
|
77
|
+
<ResourceList
|
78
|
+
resources={resources}
|
79
|
+
previewModalState={previewModalState}
|
80
|
+
isLoading={true}
|
81
|
+
onResourceSelect={() => {}}
|
82
|
+
onResourceDrillDown={() => {}}
|
83
|
+
/>
|
84
|
+
);
|
85
|
+
}}
|
86
|
+
/>,
|
87
|
+
);
|
88
|
+
|
89
|
+
await waitFor(() => {
|
90
|
+
expect(screen.getByLabelText('loading Resource list')).toBeTruthy();
|
91
|
+
});
|
92
|
+
});
|
93
|
+
|
94
|
+
it('Focus is moved to the resource list', async () => {
|
95
|
+
render(
|
96
|
+
<ResourceListTestWrapper
|
97
|
+
constructFunction={(previewModalState) => {
|
98
|
+
return (
|
99
|
+
<ResourceList
|
100
|
+
resources={resources}
|
101
|
+
previewModalState={previewModalState}
|
102
|
+
isLoading={false}
|
103
|
+
onResourceSelect={() => {}}
|
104
|
+
onResourceDrillDown={() => {}}
|
105
|
+
/>
|
106
|
+
);
|
107
|
+
}}
|
108
|
+
/>,
|
109
|
+
);
|
110
|
+
|
111
|
+
await waitFor(() => {
|
112
|
+
expect(screen.queryByLabelText('Resource list')).toHaveFocus();
|
113
|
+
});
|
114
|
+
});
|
115
|
+
|
116
|
+
it('Resource list render each resource', async () => {
|
117
|
+
render(
|
118
|
+
<ResourceListTestWrapper
|
119
|
+
constructFunction={(previewModalState) => {
|
120
|
+
return (
|
121
|
+
<ResourceList
|
122
|
+
resources={resources}
|
123
|
+
previewModalState={previewModalState}
|
124
|
+
isLoading={false}
|
125
|
+
onResourceSelect={() => {}}
|
126
|
+
onResourceDrillDown={() => {}}
|
127
|
+
/>
|
128
|
+
);
|
129
|
+
}}
|
130
|
+
/>,
|
131
|
+
);
|
132
|
+
|
133
|
+
await waitFor(() => {
|
134
|
+
expect(screen.queryByText('Resource 1')).toBeTruthy();
|
135
|
+
expect(screen.queryByText('Resource 2')).toBeTruthy();
|
136
|
+
expect(screen.queryByText('Resource 3')).toBeTruthy();
|
137
|
+
expect(screen.queryByText('Resource 4')).toBeTruthy();
|
138
|
+
expect(screen.queryByText('Resource 5')).toBeTruthy();
|
139
|
+
});
|
140
|
+
});
|
141
|
+
|
142
|
+
it('Clicking resource body triggers correct onResourceSelect', async () => {
|
143
|
+
const onResourceSelect = jest.fn();
|
144
|
+
|
145
|
+
render(
|
146
|
+
<ResourceListTestWrapper
|
147
|
+
constructFunction={(previewModalState) => {
|
148
|
+
return (
|
149
|
+
<ResourceList
|
150
|
+
resources={resources}
|
151
|
+
previewModalState={previewModalState}
|
152
|
+
isLoading={false}
|
153
|
+
onResourceSelect={onResourceSelect}
|
154
|
+
onResourceDrillDown={() => {}}
|
155
|
+
/>
|
156
|
+
);
|
157
|
+
}}
|
158
|
+
/>,
|
159
|
+
);
|
160
|
+
|
161
|
+
const user = userEvent.setup();
|
162
|
+
user.click(screen.getByRole('button', { name: 'page Resource 1' }));
|
163
|
+
|
164
|
+
await waitFor(() => {
|
165
|
+
// Provides the item that was clicked and an id reference to the button that was clicked
|
166
|
+
expect(onResourceSelect).toHaveBeenCalledWith({ source: '1', id: '1' }, { id: expect.any(String) });
|
167
|
+
});
|
168
|
+
});
|
169
|
+
|
170
|
+
it('Clicking node child count triggers correct onResourceDrillDown', async () => {
|
171
|
+
const onResourceDrillDown = jest.fn();
|
172
|
+
|
173
|
+
render(
|
174
|
+
<ResourceListTestWrapper
|
175
|
+
constructFunction={(previewModalState) => {
|
176
|
+
return (
|
177
|
+
<ResourceList
|
178
|
+
resources={resources}
|
179
|
+
previewModalState={previewModalState}
|
180
|
+
isLoading={false}
|
181
|
+
onResourceSelect={() => {}}
|
182
|
+
onResourceDrillDown={onResourceDrillDown}
|
183
|
+
/>
|
184
|
+
);
|
185
|
+
}}
|
186
|
+
/>,
|
187
|
+
);
|
188
|
+
|
189
|
+
const user = userEvent.setup();
|
190
|
+
user.click(screen.getByRole('button', { name: 'Drill down to Resource 3 children' }));
|
191
|
+
|
192
|
+
await waitFor(() => {
|
193
|
+
expect(onResourceDrillDown).toHaveBeenCalledWith({ source: '1', id: '3' });
|
194
|
+
});
|
195
|
+
});
|
196
|
+
});
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
import { useOverlayTriggerState } from 'react-stately';
|
4
|
+
|
5
|
+
import ResourceList from './ResourceList';
|
6
|
+
import sampleResources from './sample-resources.json';
|
7
|
+
|
8
|
+
export default {
|
9
|
+
title: 'Resource List',
|
10
|
+
component: ResourceList,
|
11
|
+
} as Meta<typeof ResourceList>;
|
12
|
+
|
13
|
+
const Template: StoryFn<typeof ResourceList> = ({ resources, isLoading, allowedTypes }) => {
|
14
|
+
const previewModalState = useOverlayTriggerState({});
|
15
|
+
|
16
|
+
return (
|
17
|
+
<ResourceList
|
18
|
+
resources={resources}
|
19
|
+
previewModalState={previewModalState}
|
20
|
+
isLoading={isLoading}
|
21
|
+
onResourceSelect={({ source, id }) => alert(`Resource Select: ${source} - ${id}`)}
|
22
|
+
onResourceDrillDown={({ source, id }) => alert(`Child Drill Down: ${source} - ${id}`)}
|
23
|
+
allowedTypes={allowedTypes}
|
24
|
+
/>
|
25
|
+
);
|
26
|
+
};
|
27
|
+
|
28
|
+
export const Primary = Template.bind({});
|
29
|
+
Primary.args = {
|
30
|
+
resources: sampleResources,
|
31
|
+
isLoading: false,
|
32
|
+
allowedTypes: ['site', 'image'],
|
33
|
+
};
|
34
|
+
|
35
|
+
export const Loading = Template.bind({});
|
36
|
+
Loading.args = {
|
37
|
+
...Primary.args,
|
38
|
+
isLoading: true,
|
39
|
+
allowedTypes: ['site', 'image'],
|
40
|
+
};
|
@@ -0,0 +1,74 @@
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
2
|
+
import { OverlayTriggerState } from 'react-stately';
|
3
|
+
import { DOMAttributes, FocusableElement } from '@react-types/shared';
|
4
|
+
|
5
|
+
import ResourceItem from '../ResourceItem/ResourceItem';
|
6
|
+
import { NodeIdentifier, Resource } from '../index';
|
7
|
+
import { SkeletonListItem } from '../Skeleton/ListItem/SkeletonListItem';
|
8
|
+
|
9
|
+
export interface ResourceListProps {
|
10
|
+
resources: Array<Resource>;
|
11
|
+
previewModalState: OverlayTriggerState;
|
12
|
+
isLoading: boolean;
|
13
|
+
onResourceSelect: (node: NodeIdentifier, overlayProps: DOMAttributes<FocusableElement>) => void;
|
14
|
+
onResourceDrillDown: (node: NodeIdentifier) => void;
|
15
|
+
allowedTypes?: string[] | undefined;
|
16
|
+
}
|
17
|
+
|
18
|
+
const ResourceList = function ({
|
19
|
+
resources,
|
20
|
+
previewModalState,
|
21
|
+
isLoading,
|
22
|
+
onResourceSelect,
|
23
|
+
onResourceDrillDown,
|
24
|
+
allowedTypes,
|
25
|
+
}: ResourceListProps) {
|
26
|
+
const listRef = useRef<HTMLUListElement>(null);
|
27
|
+
|
28
|
+
// When resources change, because we are on a new page, reset focus to the list
|
29
|
+
useEffect(() => {
|
30
|
+
if (listRef.current) {
|
31
|
+
listRef.current?.focus({
|
32
|
+
preventScroll: true,
|
33
|
+
});
|
34
|
+
}
|
35
|
+
}, [resources]);
|
36
|
+
|
37
|
+
return (
|
38
|
+
<ul
|
39
|
+
ref={listRef}
|
40
|
+
tabIndex={-1}
|
41
|
+
aria-label={`${isLoading ? 'loading' : ''} Resource list`}
|
42
|
+
className="flex flex-col text-sm font-semibold px-7 my-4"
|
43
|
+
>
|
44
|
+
{isLoading && (
|
45
|
+
<>
|
46
|
+
{[...Array(8)].map((_item, index: number) => {
|
47
|
+
return <SkeletonListItem key={index} />;
|
48
|
+
})}
|
49
|
+
</>
|
50
|
+
)}
|
51
|
+
|
52
|
+
{!isLoading &&
|
53
|
+
resources.map(({ type, id, selected, label, childCount }) => {
|
54
|
+
return (
|
55
|
+
<ResourceItem
|
56
|
+
key={`${id.source}-${id.id}`}
|
57
|
+
id={id}
|
58
|
+
selected={selected}
|
59
|
+
label={label}
|
60
|
+
type={type}
|
61
|
+
childCount={childCount}
|
62
|
+
previewModalState={previewModalState}
|
63
|
+
onSelect={onResourceSelect}
|
64
|
+
onDrillDown={onResourceDrillDown}
|
65
|
+
className="border-b-0 first:mt-0 first:rounded-t-lg last:rounded-b-lg last:border-b"
|
66
|
+
allowedTypes={allowedTypes}
|
67
|
+
/>
|
68
|
+
);
|
69
|
+
})}
|
70
|
+
</ul>
|
71
|
+
);
|
72
|
+
};
|
73
|
+
|
74
|
+
export default ResourceList;
|