@squiz/resource-browser 1.32.1-alpha.28 → 1.32.1-alpha.30
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/lib/Modal/ModalTrigger.d.ts +2 -1
- package/lib/Modal/ModalTrigger.js +5 -4
- package/lib/PreviewPanel/PreviewPanel.d.ts +0 -1
- package/lib/PreviewPanel/details/MatrixResource.d.ts +0 -1
- package/lib/PreviewPanel/details/MatrixResource.js +2 -17
- package/lib/ResourceBreadcrumb/ResourceBreadcrumb.d.ts +0 -1
- package/lib/ResourceError/ResourceError.d.ts +0 -1
- package/lib/ResourceItem/ResourceItem.d.ts +0 -1
- package/lib/ResourceItem/ResourceItem.js +7 -6
- package/lib/ResourceList/ResourceList.d.ts +0 -1
- package/lib/ResourcePickerContainer/ResourcePickerContainer.d.ts +0 -1
- package/lib/ResourcePickerContainer/ResourcePickerContainer.js +4 -4
- package/lib/Skeleton/List/SkeletonList.d.ts +0 -1
- package/lib/Skeleton/ListItem/SkeletonListItem.d.ts +0 -1
- package/lib/Skeleton/ListItem/SkeletonListItem.js +1 -1
- package/lib/SourceDropdown/SourceDropdown.d.ts +0 -1
- package/lib/SourceDropdown/SourceDropdown.js +5 -5
- package/lib/SourceList/SourceList.d.ts +0 -1
- package/lib/Spinner/Spinner.d.ts +1 -1
- package/lib/Spinner/Spinner.js +1 -1
- package/lib/StatusIndicator/StatusIndicator.d.ts +6 -0
- package/lib/StatusIndicator/StatusIndicator.js +26 -0
- package/lib/index.css +187 -23
- package/lib/index.d.ts +2 -5
- package/lib/index.js +9 -3
- package/package.json +5 -3
- package/src/Icons/Icon.stories.tsx +5 -0
- package/src/Modal/Modal.spec.tsx +25 -0
- package/src/Modal/ModalTrigger.tsx +14 -2
- package/src/PreviewPanel/details/MatrixResource.tsx +2 -22
- package/src/ResourceError/ResourceError.spec.tsx +0 -4
- package/src/ResourceItem/ResourceItem.spec.tsx +0 -4
- package/src/ResourceItem/ResourceItem.tsx +9 -7
- package/src/ResourcePicker/ResetButton.tsx +14 -0
- package/src/ResourcePicker/ResourcePicker.spec.tsx +81 -0
- package/src/ResourcePicker/ResourcePicker.stories.tsx +62 -0
- package/src/ResourcePicker/ResourcePicker.tsx +55 -0
- package/src/ResourcePicker/States/Error.tsx +12 -0
- package/src/ResourcePicker/States/Loading.tsx +9 -0
- package/src/ResourcePicker/States/Selected.tsx +51 -0
- package/src/ResourcePicker/mock-image-resource.json +51 -0
- package/src/ResourcePicker/mock-resource.json +15 -0
- package/src/ResourcePicker/resource-picker.scss +13 -0
- package/src/ResourcePickerContainer/ResourcePickerContainer.stories.tsx +1 -1
- package/src/ResourcePickerContainer/ResourcePickerContainer.tsx +4 -4
- package/src/Skeleton/ListItem/SkeletonListItem.tsx +1 -1
- package/src/Skeleton/_skeleton.scss +15 -0
- package/src/SourceDropdown/SourceDropdown.tsx +5 -7
- package/src/Spinner/Spinner.stories.tsx +2 -2
- package/src/Spinner/Spinner.tsx +2 -2
- package/src/Spinner/_spinner.scss +8 -5
- package/src/StatusIndicator/StatusIndicator.stories.tsx +83 -0
- package/src/StatusIndicator/StatusIndicator.tsx +35 -0
- package/src/index.scss +15 -0
- package/src/index.spec.tsx +44 -0
- package/src/index.stories.tsx +15 -17
- package/src/index.tsx +41 -20
- package/src/types.d.ts +5 -4
@@ -0,0 +1,81 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { render, screen } from '@testing-library/react';
|
3
|
+
import ResourcePicker from './ResourcePicker';
|
4
|
+
import mockResource from './mock-resource.json';
|
5
|
+
|
6
|
+
const defaultProps: any = {
|
7
|
+
resource: null,
|
8
|
+
allowedTypes: undefined,
|
9
|
+
isLoading: false,
|
10
|
+
isError: false,
|
11
|
+
};
|
12
|
+
|
13
|
+
describe('Resource picker', () => {
|
14
|
+
it('should render the initial state with the default label', () => {
|
15
|
+
render(<ResourcePicker {...defaultProps} />);
|
16
|
+
const pickerLabel = screen.getByText('Choose asset');
|
17
|
+
|
18
|
+
expect(pickerLabel).toBeInTheDocument();
|
19
|
+
});
|
20
|
+
|
21
|
+
it('should render the initial state with the image label if it s an image picker', () => {
|
22
|
+
render(<ResourcePicker {...defaultProps} allowedTypes={['image']} />);
|
23
|
+
const pickerLabel = screen.getByText('Choose image');
|
24
|
+
|
25
|
+
expect(pickerLabel).toBeInTheDocument();
|
26
|
+
});
|
27
|
+
|
28
|
+
it('should render the loading state if set to true', () => {
|
29
|
+
render(<ResourcePicker {...defaultProps} isLoading={true} />);
|
30
|
+
const pickerLabel = screen.queryByText('Choose image');
|
31
|
+
const loadingLabel = screen.queryByLabelText('Loading selection');
|
32
|
+
|
33
|
+
expect(pickerLabel).not.toBeInTheDocument();
|
34
|
+
expect(loadingLabel).toBeInTheDocument();
|
35
|
+
});
|
36
|
+
|
37
|
+
it('should render the error state if set to true', () => {
|
38
|
+
render(<ResourcePicker {...defaultProps} isError={true} />);
|
39
|
+
const pickerLabel = screen.queryByText('Choose image');
|
40
|
+
const errorLabel = screen.queryByText('Failed to retrieve asset info due to a Component Service API key problem.');
|
41
|
+
|
42
|
+
expect(pickerLabel).not.toBeInTheDocument();
|
43
|
+
expect(errorLabel).toBeInTheDocument();
|
44
|
+
});
|
45
|
+
|
46
|
+
it('should render the selected state if there is a resource returned', () => {
|
47
|
+
render(<ResourcePicker {...defaultProps} resource={mockResource} />);
|
48
|
+
const pickerLabel = screen.queryByText('Choose image');
|
49
|
+
const resourceName = screen.queryByText('Products');
|
50
|
+
|
51
|
+
expect(pickerLabel).not.toBeInTheDocument();
|
52
|
+
expect(resourceName).toBeInTheDocument();
|
53
|
+
});
|
54
|
+
|
55
|
+
it('should display the reset button in selected state', () => {
|
56
|
+
render(<ResourcePicker {...defaultProps} resource={mockResource} />);
|
57
|
+
const resourceName = screen.queryByText('Products');
|
58
|
+
const removeButton = screen.queryByLabelText('Remove selection');
|
59
|
+
|
60
|
+
expect(resourceName).toBeInTheDocument();
|
61
|
+
expect(removeButton).toBeInTheDocument();
|
62
|
+
});
|
63
|
+
|
64
|
+
it('should display the reset button in error state', () => {
|
65
|
+
render(<ResourcePicker {...defaultProps} isError={true} />);
|
66
|
+
const errorLabel = screen.queryByText('Failed to retrieve asset info due to a Component Service API key problem.');
|
67
|
+
const removeButton = screen.queryByLabelText('Remove selection');
|
68
|
+
|
69
|
+
expect(errorLabel).toBeInTheDocument();
|
70
|
+
expect(removeButton).toBeInTheDocument();
|
71
|
+
});
|
72
|
+
|
73
|
+
it('should not display the reset button in the empty state', () => {
|
74
|
+
render(<ResourcePicker {...defaultProps} />);
|
75
|
+
const pickerLabel = screen.getByText('Choose asset');
|
76
|
+
const removeButton = screen.queryByLabelText('Remove selection');
|
77
|
+
|
78
|
+
expect(pickerLabel).toBeInTheDocument();
|
79
|
+
expect(removeButton).not.toBeInTheDocument();
|
80
|
+
});
|
81
|
+
});
|
@@ -0,0 +1,62 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { StoryFn, Meta } from '@storybook/react';
|
3
|
+
|
4
|
+
import ResourcePicker, { ResourcePickerProps } from './ResourcePicker';
|
5
|
+
import mockResource from './mock-resource.json';
|
6
|
+
import mockImageResource from './mock-image-resource.json';
|
7
|
+
|
8
|
+
export default {
|
9
|
+
title: 'Resource picker field',
|
10
|
+
component: ResourcePicker,
|
11
|
+
} as Meta<typeof ResourcePicker>;
|
12
|
+
|
13
|
+
const Template: StoryFn<ResourcePickerProps> = (args: ResourcePickerProps) => (
|
14
|
+
<div className="w-[400px] m-3">
|
15
|
+
<ResourcePicker {...args} />
|
16
|
+
</div>
|
17
|
+
);
|
18
|
+
|
19
|
+
export const Empty = Template.bind({});
|
20
|
+
Empty.args = {
|
21
|
+
resource: null,
|
22
|
+
allowedTypes: undefined,
|
23
|
+
isLoading: false,
|
24
|
+
isError: false,
|
25
|
+
isDisabled: false,
|
26
|
+
};
|
27
|
+
|
28
|
+
export const ImagePicker = Template.bind({});
|
29
|
+
ImagePicker.args = {
|
30
|
+
...Empty.args,
|
31
|
+
allowedTypes: ['image'],
|
32
|
+
};
|
33
|
+
|
34
|
+
export const Loading = Template.bind({});
|
35
|
+
Loading.args = {
|
36
|
+
...Empty.args,
|
37
|
+
isLoading: true,
|
38
|
+
};
|
39
|
+
|
40
|
+
export const Error = Template.bind({});
|
41
|
+
Error.args = {
|
42
|
+
...Empty.args,
|
43
|
+
isError: true,
|
44
|
+
};
|
45
|
+
|
46
|
+
export const Selected = Template.bind({});
|
47
|
+
Selected.args = {
|
48
|
+
...Empty.args,
|
49
|
+
resource: mockResource,
|
50
|
+
};
|
51
|
+
|
52
|
+
export const SelectedImage = Template.bind({});
|
53
|
+
SelectedImage.args = {
|
54
|
+
...Empty.args,
|
55
|
+
resource: mockImageResource,
|
56
|
+
};
|
57
|
+
|
58
|
+
export const Disabled = Template.bind({});
|
59
|
+
Disabled.args = {
|
60
|
+
...Empty.args,
|
61
|
+
isDisabled: true,
|
62
|
+
};
|
@@ -0,0 +1,55 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import { Resource } from '../types';
|
4
|
+
import AdsClickRoundedIcon from '@mui/icons-material/AdsClickRounded';
|
5
|
+
import AddCircleOutlineRoundedIcon from '@mui/icons-material/AddCircleOutlineRounded';
|
6
|
+
import PhotoLibraryRoundedIcon from '@mui/icons-material/PhotoLibraryRounded';
|
7
|
+
|
8
|
+
import ModalTrigger from '../Modal/ModalTrigger';
|
9
|
+
import { ErrorState } from './States/Error';
|
10
|
+
import { LoadingState } from './States/Loading';
|
11
|
+
import { SelectedState } from './States/Selected';
|
12
|
+
import clsx from 'clsx';
|
13
|
+
|
14
|
+
export type ResourcePickerProps = {
|
15
|
+
resource: Resource;
|
16
|
+
allowedTypes: string[] | undefined;
|
17
|
+
isError: boolean;
|
18
|
+
isLoading: boolean;
|
19
|
+
isDisabled: boolean;
|
20
|
+
};
|
21
|
+
|
22
|
+
const ResourcePicker = ({ resource, allowedTypes, isError, isLoading, isDisabled }: ResourcePickerProps) => {
|
23
|
+
const isImagePicker = allowedTypes && allowedTypes.length === 1 && (allowedTypes.includes('image') as boolean);
|
24
|
+
const isEmpty = resource === null && !isLoading && (!isError as boolean);
|
25
|
+
|
26
|
+
return (
|
27
|
+
<div className={clsx('resource-picker', isDisabled && 'bg-gray-300')}>
|
28
|
+
{isImagePicker ? (
|
29
|
+
<PhotoLibraryRoundedIcon aria-hidden className="w-6 h-6" />
|
30
|
+
) : (
|
31
|
+
<AdsClickRoundedIcon aria-hidden className="w-6 h-6" />
|
32
|
+
)}
|
33
|
+
{isEmpty ? (
|
34
|
+
<ModalTrigger
|
35
|
+
showLabel={true}
|
36
|
+
label={isImagePicker ? `Choose image` : `Choose asset`}
|
37
|
+
icon={<AddCircleOutlineRoundedIcon aria-hidden className="!w-4 !h-4" />}
|
38
|
+
isDisabled={isDisabled}
|
39
|
+
>
|
40
|
+
{() => <div>Resource browser here</div>}
|
41
|
+
</ModalTrigger>
|
42
|
+
) : (
|
43
|
+
<div className="resource-picker-info">
|
44
|
+
<div className="resource-picker-info__layout">
|
45
|
+
{isLoading && <LoadingState />}
|
46
|
+
{isError && <ErrorState isDisabled={isDisabled} />}
|
47
|
+
{resource !== null && <SelectedState resource={resource} isDisabled={isDisabled} />}
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
)}
|
51
|
+
</div>
|
52
|
+
);
|
53
|
+
};
|
54
|
+
|
55
|
+
export default ResourcePicker;
|
@@ -0,0 +1,12 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
|
3
|
+
import Icon, { IconOptions } from '../../Icons/Icon';
|
4
|
+
import { ResetButton } from '../ResetButton';
|
5
|
+
|
6
|
+
export const ErrorState = ({ isDisabled }: { isDisabled: boolean }) => (
|
7
|
+
<>
|
8
|
+
<Icon icon={'error' as IconOptions} aria-hidden className="w-6 h-6 text-red-300" />
|
9
|
+
<div className="text-red-300">Failed to retrieve asset info due to a Component Service API key problem.</div>
|
10
|
+
<ResetButton isDisabled={isDisabled} />
|
11
|
+
</>
|
12
|
+
);
|
@@ -0,0 +1,51 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import prettyBytes from 'pretty-bytes';
|
3
|
+
|
4
|
+
import { Resource } from '../../types';
|
5
|
+
import Icon, { IconOptions } from '../../Icons/Icon';
|
6
|
+
import StatusIndicator from '../../StatusIndicator/StatusIndicator';
|
7
|
+
import { ResetButton } from '../ResetButton';
|
8
|
+
|
9
|
+
export type SelectedStateProps = {
|
10
|
+
resource: Resource;
|
11
|
+
isDisabled: boolean;
|
12
|
+
};
|
13
|
+
|
14
|
+
export const SelectedState = ({ resource: { id, type, name, status, squizImage }, isDisabled }: SelectedStateProps) => {
|
15
|
+
const fileSize = squizImage?.imageVariations?.original?.byteSize;
|
16
|
+
const fileWidth = squizImage?.imageVariations?.original?.width;
|
17
|
+
const fileHeight = squizImage?.imageVariations?.original?.height;
|
18
|
+
return (
|
19
|
+
<>
|
20
|
+
<>
|
21
|
+
{/* Left column */}
|
22
|
+
<Icon icon={type.code as IconOptions} resourceSource="matrix" className="w-4 h-4 flex self-center" />
|
23
|
+
{/* Center column */}
|
24
|
+
<div className="justify-self-start self-center">{name}</div>
|
25
|
+
{/* End column */}
|
26
|
+
<ResetButton isDisabled={isDisabled} />
|
27
|
+
</>
|
28
|
+
<dl className="col-start-2 col-end-2 flex flex-column gap-1 justify-self-start items-center font-normal text-sm">
|
29
|
+
<div>
|
30
|
+
<dt className="hidden">Status: {status.name}</dt>
|
31
|
+
<dd className="flex items-center">
|
32
|
+
<StatusIndicator status={status} />
|
33
|
+
</dd>
|
34
|
+
</div>
|
35
|
+
|
36
|
+
<div>
|
37
|
+
<dt className="hidden">Asset ID</dt>
|
38
|
+
<dd className="text-gray-700">#{id}</dd>
|
39
|
+
</div>
|
40
|
+
{fileSize && (
|
41
|
+
<div>
|
42
|
+
<dt className="hidden">Asset size</dt>
|
43
|
+
<dd className="ml-4 text-gray-600">
|
44
|
+
{prettyBytes(fileSize)}, {fileWidth} x {fileHeight}px
|
45
|
+
</dd>
|
46
|
+
</div>
|
47
|
+
)}
|
48
|
+
</dl>
|
49
|
+
</>
|
50
|
+
);
|
51
|
+
};
|
@@ -0,0 +1,51 @@
|
|
1
|
+
{
|
2
|
+
"id": "45104",
|
3
|
+
"type": {
|
4
|
+
"code": "image",
|
5
|
+
"name": "Image"
|
6
|
+
},
|
7
|
+
"name": "DfBFfNQyB77dQMJYU4eH.jpg",
|
8
|
+
"status": {
|
9
|
+
"code": "under_construction",
|
10
|
+
"name": "Under Construction"
|
11
|
+
},
|
12
|
+
"url": "",
|
13
|
+
"urls": [],
|
14
|
+
"childCount": 0,
|
15
|
+
"squizImage": {
|
16
|
+
"name": "Fancy music",
|
17
|
+
"alt": "",
|
18
|
+
"caption": "",
|
19
|
+
"imageVariations": {
|
20
|
+
"original": {
|
21
|
+
"url": "",
|
22
|
+
"width": 400,
|
23
|
+
"height": 400,
|
24
|
+
"byteSize": 37174,
|
25
|
+
"mimeType": "image/",
|
26
|
+
"aspectRatio": "4:3",
|
27
|
+
"sha1Hash": ""
|
28
|
+
},
|
29
|
+
"aspectRatios": [
|
30
|
+
{
|
31
|
+
"url": "",
|
32
|
+
"width": 100,
|
33
|
+
"height": 100,
|
34
|
+
"byteSize": 4628,
|
35
|
+
"mimeType": "image/",
|
36
|
+
"aspectRatio": "4:3",
|
37
|
+
"sha1Hash": ""
|
38
|
+
},
|
39
|
+
{
|
40
|
+
"url": "",
|
41
|
+
"width": 104,
|
42
|
+
"height": 96,
|
43
|
+
"byteSize": 5718,
|
44
|
+
"mimeType": "image/",
|
45
|
+
"aspectRatio": "4:3",
|
46
|
+
"sha1Hash": ""
|
47
|
+
}
|
48
|
+
]
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
{
|
2
|
+
"id": "1345",
|
3
|
+
"name": "Products",
|
4
|
+
"type": {
|
5
|
+
"code": "page_standard",
|
6
|
+
"name": "Standard Page"
|
7
|
+
},
|
8
|
+
"status": {
|
9
|
+
"code": "under_construction",
|
10
|
+
"name": "Under construction"
|
11
|
+
},
|
12
|
+
"url": "http://my-squiz.net/assets/1",
|
13
|
+
"urls": [],
|
14
|
+
"childCount": 0
|
15
|
+
}
|
@@ -0,0 +1,13 @@
|
|
1
|
+
.resource-picker {
|
2
|
+
@apply grid grid-cols-[24px_1fr] gap-2;
|
3
|
+
@apply border-2 border-gray-300;
|
4
|
+
@apply text-gray-500 rounded p-2;
|
5
|
+
&-info {
|
6
|
+
@apply w-full p-2 bg-gray-100 rounded-md;
|
7
|
+
@apply text-gray-900 text-base text-md font-semibold;
|
8
|
+
&__layout {
|
9
|
+
@apply grid grid-cols-[24px_1fr_24px] gap-2;
|
10
|
+
@apply justify-items-center;
|
11
|
+
}
|
12
|
+
}
|
13
|
+
}
|
@@ -4,7 +4,7 @@ import ResourcePickerContainer from './ResourcePickerContainer';
|
|
4
4
|
import { createResourceBrowserCallbacks } from '../__mocks__/StorybookHelpers';
|
5
5
|
|
6
6
|
export default {
|
7
|
-
title: 'Resource Picker',
|
7
|
+
title: 'Resource Picker container',
|
8
8
|
component: ResourcePickerContainer,
|
9
9
|
} as Meta<typeof ResourcePickerContainer>;
|
10
10
|
|
@@ -92,12 +92,12 @@ function ResourcePickerContainer({
|
|
92
92
|
}, [hierarchy]);
|
93
93
|
|
94
94
|
return (
|
95
|
-
<div className="relative flex flex-col h-full">
|
96
|
-
<div className="flex items-center p-
|
95
|
+
<div className="relative flex flex-col h-full text-gray-800 antialiased">
|
96
|
+
<div className="flex items-center p-4.5">
|
97
97
|
<h2 {...titleAriaProps} className="text-xl leading-6 text-gray-800 font-semibold mr-6">
|
98
98
|
{title}
|
99
99
|
</h2>
|
100
|
-
<div className="px-3 border-l border-
|
100
|
+
<div className="px-3 border-l border-gray-300 w-300px">
|
101
101
|
<SourceDropdown
|
102
102
|
sources={sources}
|
103
103
|
selectedSource={source}
|
@@ -120,7 +120,7 @@ function ResourcePickerContainer({
|
|
120
120
|
</svg>
|
121
121
|
</button>
|
122
122
|
</div>
|
123
|
-
<div className="flex border-t border-
|
123
|
+
<div className="flex border-t border-gray-300 h-[calc(100%-72px)]">
|
124
124
|
<div className="overflow-y-scroll flex-1 grow-[3] border-r border-gray-300">
|
125
125
|
<h3 className="sr-only">Resource List</h3>
|
126
126
|
{hierarchy.length > 0 && (
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
2
2
|
|
3
3
|
export const SkeletonListItem = () => (
|
4
4
|
<li className="flex items-center p-1 first:mt-0 bg-white border border-b-0 border-grey-200 first:rounded-t-lg last:rounded-b-lg last:border-b">
|
5
|
-
<div className="grid grid-cols-[24px_1fr_45px] w-full flex items-center p-4 rounded">
|
5
|
+
<div className="animate-skeleton-pulse grid grid-cols-[24px_1fr_45px] w-full flex items-center p-4 rounded">
|
6
6
|
<span className="w-6 h-6 bg-gray-200 rounded-full" />
|
7
7
|
<div className="w-full d-flex flex-col mx-4">
|
8
8
|
<div className="mb-1 w-3/4 h-2 bg-gray-200 rounded" />
|
@@ -64,7 +64,7 @@ export default function SourceDropdown({
|
|
64
64
|
aria-expanded={isOpen}
|
65
65
|
aria-controls={`${uniqueId}-button-menu`}
|
66
66
|
onClick={() => setIsOpen(!isOpen)}
|
67
|
-
className="relative flex items-center text-sm font-semibold p-
|
67
|
+
className="relative flex items-center text-sm font-semibold p-1.5 w-full"
|
68
68
|
>
|
69
69
|
{selectedSource && (
|
70
70
|
<>
|
@@ -73,7 +73,7 @@ export default function SourceDropdown({
|
|
73
73
|
icon={selectedSource.resource?.type.code as IconOptions}
|
74
74
|
resourceSource="matrix"
|
75
75
|
aria-hidden
|
76
|
-
className="mr-2.5"
|
76
|
+
className="mr-2.5 h-[20px] w-[20px]"
|
77
77
|
/>
|
78
78
|
<div className="truncate max-w-[200px]">{selectedSource.resource?.name || selectedSource.source.name}</div>
|
79
79
|
</>
|
@@ -82,7 +82,7 @@ export default function SourceDropdown({
|
|
82
82
|
{!selectedSource && (
|
83
83
|
<>
|
84
84
|
<span className="sr-only">view </span>
|
85
|
-
<Icon icon={'root' as IconOptions} aria-hidden className="mr-2.5" />
|
85
|
+
<Icon icon={'root' as IconOptions} aria-hidden className="mr-2.5 h-[20px] w-[20px]" />
|
86
86
|
All available sources
|
87
87
|
</>
|
88
88
|
)}
|
@@ -111,7 +111,7 @@ export default function SourceDropdown({
|
|
111
111
|
</li>
|
112
112
|
{isLoading && (
|
113
113
|
<li className="mt-6">
|
114
|
-
<Spinner size="
|
114
|
+
<Spinner size="sm" label="Loading sources" className="m-3" />
|
115
115
|
</li>
|
116
116
|
)}
|
117
117
|
{!isLoading &&
|
@@ -135,9 +135,7 @@ export default function SourceDropdown({
|
|
135
135
|
<button
|
136
136
|
type="button"
|
137
137
|
onClick={() => handleSourceClick({ source, resource })}
|
138
|
-
className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100
|
139
|
-
isSelectedSource ? 'bg-blue-100 text-blue-400' : ''
|
140
|
-
}`}
|
138
|
+
className={`relative grow flex items-center p-2.5 hover:bg-gray-100 focus:bg-gray-100`}
|
141
139
|
>
|
142
140
|
<Icon
|
143
141
|
icon={resource?.type.code as IconOptions}
|
@@ -16,11 +16,11 @@ Default.args = {};
|
|
16
16
|
export const Large = Template.bind({});
|
17
17
|
Large.args = {
|
18
18
|
...Default.args,
|
19
|
-
size: '
|
19
|
+
size: 'md',
|
20
20
|
};
|
21
21
|
|
22
22
|
export const Custom = Template.bind({});
|
23
23
|
Custom.args = {
|
24
24
|
...Default.args,
|
25
|
-
className: 'text-blue-300',
|
25
|
+
className: 'text-blue-300 m-3',
|
26
26
|
};
|
package/src/Spinner/Spinner.tsx
CHANGED
@@ -2,12 +2,12 @@ import React, { ReactElement } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
3
3
|
|
4
4
|
export type SpinnerProps = {
|
5
|
-
size?: '
|
5
|
+
size?: 'sm' | 'md';
|
6
6
|
className?: string;
|
7
7
|
label?: string;
|
8
8
|
};
|
9
9
|
|
10
|
-
const Spinner = ({ size = '
|
10
|
+
const Spinner = ({ size = 'sm', className, label = 'Loading' }: SpinnerProps): ReactElement => {
|
11
11
|
return (
|
12
12
|
<div className="spinner__wrapper" aria-label={label}>
|
13
13
|
<div className={clsx(`spinner`, size && `spinner--${size}`, className)} role="status" />
|
@@ -1,11 +1,14 @@
|
|
1
1
|
.spinner {
|
2
|
-
@apply inline-block rounded-full
|
3
|
-
@apply border-
|
2
|
+
@apply inline-block rounded-full;
|
3
|
+
@apply border-2 border-solid border-current border-r-transparent;
|
4
4
|
@apply animate-spin align-[-0.125em] motion-reduce:animate-[spin_1.5s_linear_infinite];
|
5
|
+
height: 20px;
|
6
|
+
width: 20px;
|
5
7
|
&__wrapper {
|
6
|
-
@apply flex items-center justify-center
|
8
|
+
@apply flex items-center justify-center text-gray-300;
|
7
9
|
}
|
8
|
-
&--
|
9
|
-
|
10
|
+
&--md {
|
11
|
+
height: 32px;
|
12
|
+
width: 32px;
|
10
13
|
}
|
11
14
|
}
|
@@ -0,0 +1,83 @@
|
|
1
|
+
import StatusIndicator, { StatusIndicatorProps } from './StatusIndicator';
|
2
|
+
import { Meta, StoryFn } from '@storybook/react';
|
3
|
+
|
4
|
+
export default {
|
5
|
+
title: 'Status Indicator',
|
6
|
+
component: StatusIndicator,
|
7
|
+
} as Meta<typeof StatusIndicator>;
|
8
|
+
|
9
|
+
const Template: StoryFn<StatusIndicatorProps> = (args: StatusIndicatorProps) => (
|
10
|
+
<div className="m-3">
|
11
|
+
<StatusIndicator {...args} />
|
12
|
+
</div>
|
13
|
+
);
|
14
|
+
|
15
|
+
export const UnderConstruction = Template.bind({});
|
16
|
+
UnderConstruction.args = {
|
17
|
+
status: {
|
18
|
+
code: 'under_construction',
|
19
|
+
name: 'Under construction',
|
20
|
+
},
|
21
|
+
};
|
22
|
+
|
23
|
+
export const PendingApproval = Template.bind({});
|
24
|
+
PendingApproval.args = {
|
25
|
+
status: {
|
26
|
+
code: 'pending_approval',
|
27
|
+
name: 'Pending approval',
|
28
|
+
},
|
29
|
+
};
|
30
|
+
|
31
|
+
export const ApprovedToGoLive = Template.bind({});
|
32
|
+
ApprovedToGoLive.args = {
|
33
|
+
status: {
|
34
|
+
code: 'approved_to_go_live',
|
35
|
+
name: 'Approved to go live',
|
36
|
+
},
|
37
|
+
};
|
38
|
+
|
39
|
+
export const Live = Template.bind({});
|
40
|
+
Live.args = {
|
41
|
+
status: {
|
42
|
+
code: 'live',
|
43
|
+
name: 'Live',
|
44
|
+
},
|
45
|
+
};
|
46
|
+
|
47
|
+
export const UpForReview = Template.bind({});
|
48
|
+
UpForReview.args = {
|
49
|
+
status: {
|
50
|
+
code: 'up_for_review',
|
51
|
+
name: 'Up for review',
|
52
|
+
},
|
53
|
+
};
|
54
|
+
|
55
|
+
export const SafeEditing = Template.bind({});
|
56
|
+
SafeEditing.args = {
|
57
|
+
status: {
|
58
|
+
code: 'safe_editing',
|
59
|
+
name: 'Safe editing',
|
60
|
+
},
|
61
|
+
};
|
62
|
+
|
63
|
+
export const SafeEditingPendingApproval = Template.bind({});
|
64
|
+
SafeEditingPendingApproval.args = {
|
65
|
+
status: {
|
66
|
+
code: 'safe_editing_pending_approval',
|
67
|
+
name: 'Safe editing pending approval',
|
68
|
+
},
|
69
|
+
};
|
70
|
+
export const SafeEditApprovedToGoLive = Template.bind({});
|
71
|
+
SafeEditApprovedToGoLive.args = {
|
72
|
+
status: {
|
73
|
+
code: 'safe_edit_approved_to_go_live',
|
74
|
+
name: 'Safe edit approved to go live',
|
75
|
+
},
|
76
|
+
};
|
77
|
+
export const Archived = Template.bind({});
|
78
|
+
Archived.args = {
|
79
|
+
status: {
|
80
|
+
code: 'archived',
|
81
|
+
name: 'Archived',
|
82
|
+
},
|
83
|
+
};
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import React from 'react';
|
2
|
+
import { Status } from '../types';
|
3
|
+
|
4
|
+
const statusColour = {
|
5
|
+
// Duplicated from the Matrix repository.
|
6
|
+
// src/Api/AssetManagementApi/Constants/AssetStatuses.php - contains a list of possible statuses.
|
7
|
+
// frontend/src/styles/common/status-colors.scss - contains the colours used for those statuses in Matrix.
|
8
|
+
unknown: '#ff0000',
|
9
|
+
archived: '#c98a67',
|
10
|
+
under_construction: '#94d1f9',
|
11
|
+
pending_approval: '#d0bbf0',
|
12
|
+
approved_to_go_live: '#f7eaa2',
|
13
|
+
live: '#bfe60a',
|
14
|
+
up_for_review: '#72cd32',
|
15
|
+
safe_editing: '#ff97b0',
|
16
|
+
safe_editing_pending_approval: '#d688db',
|
17
|
+
safe_edit_approved_to_go_live: '#ffb34a',
|
18
|
+
};
|
19
|
+
|
20
|
+
export type StatusIndicatorProps = {
|
21
|
+
status: Status;
|
22
|
+
};
|
23
|
+
|
24
|
+
const StatusIndicator = ({ status }: StatusIndicatorProps) => {
|
25
|
+
const color = statusColour[status.code as keyof typeof statusColour] || statusColour.unknown;
|
26
|
+
|
27
|
+
return (
|
28
|
+
<span
|
29
|
+
style={{ backgroundColor: color }}
|
30
|
+
className="block rounded-full w-3 h-3 border border-solid border-black border-opacity-20"
|
31
|
+
></span>
|
32
|
+
);
|
33
|
+
};
|
34
|
+
|
35
|
+
export default StatusIndicator;
|
package/src/index.scss
CHANGED
@@ -5,4 +5,19 @@
|
|
5
5
|
|
6
6
|
// Components
|
7
7
|
@import './Spinner/spinner';
|
8
|
+
@import './Skeleton/skeleton';
|
8
9
|
@import './ResourceBreadcrumb/ResourceBreadcrumb';
|
10
|
+
@import './ResourcePicker/resource-picker';
|
11
|
+
|
12
|
+
*,
|
13
|
+
button {
|
14
|
+
letter-spacing: -0.02em;
|
15
|
+
}
|
16
|
+
|
17
|
+
svg {
|
18
|
+
@apply text-gray-600;
|
19
|
+
}
|
20
|
+
|
21
|
+
.p-4\.5 {
|
22
|
+
padding: 18px;
|
23
|
+
}
|