@strato-admin/cloudscape 0.1.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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/Admin.d.ts +17 -0
- package/dist/Admin.js +69 -0
- package/dist/RecordLink.d.ts +9 -0
- package/dist/RecordLink.js +43 -0
- package/dist/__mocks__/strato-core.js +50 -0
- package/dist/__mocks__to__delete/strato-core.js +50 -0
- package/dist/button/BulkDeleteButton.d.ts +7 -0
- package/dist/button/BulkDeleteButton.js +17 -0
- package/dist/button/Button.d.ts +6 -0
- package/dist/button/Button.js +6 -0
- package/dist/button/CreateButton.d.ts +6 -0
- package/dist/button/CreateButton.js +24 -0
- package/dist/button/EditButton.d.ts +8 -0
- package/dist/button/EditButton.js +24 -0
- package/dist/button/SaveButton.d.ts +6 -0
- package/dist/button/SaveButton.js +8 -0
- package/dist/button/index.d.ts +5 -0
- package/dist/button/index.js +5 -0
- package/dist/collection-hooks/index.d.ts +2 -0
- package/dist/collection-hooks/index.js +2 -0
- package/dist/collection-hooks/interfaces.d.ts +93 -0
- package/dist/collection-hooks/interfaces.js +1 -0
- package/dist/collection-hooks/useCollection.d.ts +3 -0
- package/dist/collection-hooks/useCollection.js +102 -0
- package/dist/create/Create.d.ts +40 -0
- package/dist/create/Create.js +34 -0
- package/dist/create/CreateHeader.d.ts +7 -0
- package/dist/create/CreateHeader.js +18 -0
- package/dist/create/index.d.ts +2 -0
- package/dist/create/index.js +2 -0
- package/dist/detail/KeyValuePairs.d.ts +36 -0
- package/dist/detail/KeyValuePairs.js +58 -0
- package/dist/detail/Show.d.ts +39 -0
- package/dist/detail/Show.js +40 -0
- package/dist/detail/ShowHeader.d.ts +7 -0
- package/dist/detail/ShowHeader.js +19 -0
- package/dist/detail/index.d.ts +3 -0
- package/dist/detail/index.js +3 -0
- package/dist/edit/Edit.d.ts +42 -0
- package/dist/edit/Edit.js +38 -0
- package/dist/edit/EditHeader.d.ts +7 -0
- package/dist/edit/EditHeader.js +18 -0
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +2 -0
- package/dist/field/ArrayField.d.ts +29 -0
- package/dist/field/ArrayField.js +30 -0
- package/dist/field/BadgeField.d.ts +12 -0
- package/dist/field/BadgeField.js +15 -0
- package/dist/field/BooleanField.d.ts +18 -0
- package/dist/field/BooleanField.js +14 -0
- package/dist/field/CurrencyField.d.ts +19 -0
- package/dist/field/CurrencyField.js +23 -0
- package/dist/field/DateField.d.ts +14 -0
- package/dist/field/DateField.js +17 -0
- package/dist/field/IdField.d.ts +17 -0
- package/dist/field/IdField.js +21 -0
- package/dist/field/NumberField.d.ts +14 -0
- package/dist/field/NumberField.js +18 -0
- package/dist/field/ReferenceField.d.ts +16 -0
- package/dist/field/ReferenceField.js +23 -0
- package/dist/field/ReferenceManyField.d.ts +55 -0
- package/dist/field/ReferenceManyField.js +19 -0
- package/dist/field/StatusIndicatorField.d.ts +56 -0
- package/dist/field/StatusIndicatorField.js +48 -0
- package/dist/field/TextField.d.ts +5 -0
- package/dist/field/TextField.js +11 -0
- package/dist/field/index.d.ts +23 -0
- package/dist/field/index.js +23 -0
- package/dist/field/types.d.ts +56 -0
- package/dist/field/types.js +1 -0
- package/dist/form/Form.d.ts +13 -0
- package/dist/form/Form.js +33 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +2 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/input/AttributeEditor.d.ts +25 -0
- package/dist/input/AttributeEditor.js +80 -0
- package/dist/input/AutocompleteInput.d.ts +10 -0
- package/dist/input/AutocompleteInput.js +67 -0
- package/dist/input/FieldTitle.d.ts +8 -0
- package/dist/input/FieldTitle.js +29 -0
- package/dist/input/FormField.d.ts +7 -0
- package/dist/input/FormField.js +35 -0
- package/dist/input/FormFieldContext.d.ts +6 -0
- package/dist/input/FormFieldContext.js +3 -0
- package/dist/input/NumberInput.d.ts +7 -0
- package/dist/input/NumberInput.js +27 -0
- package/dist/input/ReferenceInput.d.ts +3 -0
- package/dist/input/ReferenceInput.js +25 -0
- package/dist/input/SelectInput.d.ts +15 -0
- package/dist/input/SelectInput.js +47 -0
- package/dist/input/SliderInput.d.ts +6 -0
- package/dist/input/SliderInput.js +25 -0
- package/dist/input/TextAreaInput.d.ts +6 -0
- package/dist/input/TextAreaInput.js +23 -0
- package/dist/input/TextInput.d.ts +7 -0
- package/dist/input/TextInput.js +23 -0
- package/dist/input/index.d.ts +11 -0
- package/dist/input/index.js +11 -0
- package/dist/input/types.d.ts +6 -0
- package/dist/input/types.js +1 -0
- package/dist/layout/AppLayout.d.ts +8 -0
- package/dist/layout/AppLayout.js +38 -0
- package/dist/layout/TopNavigation.d.ts +6 -0
- package/dist/layout/TopNavigation.js +53 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +2 -0
- package/dist/list/Cards.d.ts +11 -0
- package/dist/list/Cards.js +27 -0
- package/dist/list/List.d.ts +43 -0
- package/dist/list/List.js +28 -0
- package/dist/list/Table.d.ts +112 -0
- package/dist/list/Table.examples.d.ts +1 -0
- package/dist/list/Table.examples.js +3 -0
- package/dist/list/Table.js +218 -0
- package/dist/list/TableHeader.d.ts +7 -0
- package/dist/list/TableHeader.js +22 -0
- package/dist/list/index.d.ts +4 -0
- package/dist/list/index.js +4 -0
- package/dist/preferences/index.d.ts +0 -0
- package/dist/preferences/index.js +1 -0
- package/dist/theme/ThemeManager.d.ts +2 -0
- package/dist/theme/ThemeManager.js +11 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/package.json +73 -0
- package/src/Admin.test.tsx +32 -0
- package/src/Admin.tsx +123 -0
- package/src/RecordLink.stories.tsx +56 -0
- package/src/RecordLink.tsx +67 -0
- package/src/__mocks__/strato-core.tsx +52 -0
- package/src/button/BulkDeleteButton.stories.tsx +59 -0
- package/src/button/BulkDeleteButton.test.tsx +64 -0
- package/src/button/BulkDeleteButton.tsx +41 -0
- package/src/button/Button.stories.tsx +31 -0
- package/src/button/Button.tsx +12 -0
- package/src/button/CreateButton.stories.tsx +42 -0
- package/src/button/CreateButton.tsx +38 -0
- package/src/button/EditButton.stories.tsx +29 -0
- package/src/button/EditButton.tsx +38 -0
- package/src/button/SaveButton.stories.tsx +35 -0
- package/src/button/SaveButton.tsx +19 -0
- package/src/button/index.ts +5 -0
- package/src/collection-hooks/index.ts +2 -0
- package/src/collection-hooks/interfaces.ts +80 -0
- package/src/collection-hooks/useCollection.test.ts +413 -0
- package/src/collection-hooks/useCollection.ts +125 -0
- package/src/create/Create.test.tsx +63 -0
- package/src/create/Create.tsx +93 -0
- package/src/create/CreateHeader.tsx +34 -0
- package/src/create/index.ts +2 -0
- package/src/detail/KeyValuePairs.test.tsx +98 -0
- package/src/detail/KeyValuePairs.tsx +107 -0
- package/src/detail/Show.test.tsx +96 -0
- package/src/detail/Show.tsx +104 -0
- package/src/detail/ShowHeader.test.tsx +80 -0
- package/src/detail/ShowHeader.tsx +35 -0
- package/src/detail/index.ts +3 -0
- package/src/edit/Edit.test.tsx +91 -0
- package/src/edit/Edit.tsx +102 -0
- package/src/edit/EditHeader.tsx +34 -0
- package/src/edit/index.ts +2 -0
- package/src/field/ArrayField.tsx +51 -0
- package/src/field/BadgeField.tsx +33 -0
- package/src/field/BooleanField.stories.tsx +56 -0
- package/src/field/BooleanField.test.tsx +63 -0
- package/src/field/BooleanField.tsx +42 -0
- package/src/field/CurrencyField.stories.tsx +67 -0
- package/src/field/CurrencyField.tsx +45 -0
- package/src/field/DateField.stories.tsx +67 -0
- package/src/field/DateField.tsx +33 -0
- package/src/field/IdField.test.tsx +88 -0
- package/src/field/IdField.tsx +40 -0
- package/src/field/NumberField.stories.tsx +75 -0
- package/src/field/NumberField.tsx +35 -0
- package/src/field/ReferenceField.test.tsx +88 -0
- package/src/field/ReferenceField.tsx +64 -0
- package/src/field/ReferenceManyField.test.tsx +41 -0
- package/src/field/ReferenceManyField.tsx +73 -0
- package/src/field/StatusIndicatorField.stories.tsx +93 -0
- package/src/field/StatusIndicatorField.test.tsx +143 -0
- package/src/field/StatusIndicatorField.tsx +119 -0
- package/src/field/TextField.stories.tsx +45 -0
- package/src/field/TextField.tsx +17 -0
- package/src/field/index.ts +23 -0
- package/src/field/types.ts +58 -0
- package/src/form/Form.test.tsx +55 -0
- package/src/form/Form.tsx +66 -0
- package/src/form/index.ts +2 -0
- package/src/index.ts +25 -0
- package/src/input/AttributeEditor.test.tsx +147 -0
- package/src/input/AttributeEditor.tsx +185 -0
- package/src/input/AutocompleteInput.test.tsx +178 -0
- package/src/input/AutocompleteInput.tsx +116 -0
- package/src/input/FieldTitle.tsx +53 -0
- package/src/input/FormField.tsx +87 -0
- package/src/input/FormFieldContext.ts +9 -0
- package/src/input/NumberInput.tsx +56 -0
- package/src/input/ReferenceInput.test.tsx +35 -0
- package/src/input/ReferenceInput.tsx +36 -0
- package/src/input/SelectInput.tsx +91 -0
- package/src/input/SliderInput.test.tsx +103 -0
- package/src/input/SliderInput.tsx +49 -0
- package/src/input/TextAreaInput.tsx +48 -0
- package/src/input/TextInput.test.tsx +91 -0
- package/src/input/TextInput.tsx +51 -0
- package/src/input/index.ts +11 -0
- package/src/input/types.ts +14 -0
- package/src/layout/AppLayout.test.tsx +87 -0
- package/src/layout/AppLayout.tsx +60 -0
- package/src/layout/TopNavigation.test.tsx +78 -0
- package/src/layout/TopNavigation.tsx +84 -0
- package/src/layout/index.ts +2 -0
- package/src/list/Cards.tsx +58 -0
- package/src/list/List.tsx +76 -0
- package/src/list/Table.examples.tsx +11 -0
- package/src/list/Table.stories.tsx +73 -0
- package/src/list/Table.test.tsx +255 -0
- package/src/list/Table.tsx +438 -0
- package/src/list/TableHeader.test.tsx +114 -0
- package/src/list/TableHeader.tsx +44 -0
- package/src/list/index.ts +4 -0
- package/src/preferences/index.ts +0 -0
- package/src/stories/Button.stories.ts +54 -0
- package/src/stories/Button.tsx +31 -0
- package/src/stories/Configure.mdx +369 -0
- package/src/stories/Header.stories.ts +34 -0
- package/src/stories/Header.tsx +47 -0
- package/src/stories/Page.stories.ts +33 -0
- package/src/stories/Page.tsx +71 -0
- package/src/stories/RaStoryDecorator.tsx +38 -0
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +1 -0
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +1 -0
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +1 -0
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +1 -0
- package/src/stories/assets/youtube.svg +1 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/theme/ThemeManager.tsx +15 -0
- package/src/theme/index.ts +2 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Header, { HeaderProps } from '@cloudscape-design/components/header';
|
|
3
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
4
|
+
import { useResourceContext, useShowContext, useTranslate, useResourceDefinitions } from '@strato-admin/core';
|
|
5
|
+
import { EditButton } from '../button/EditButton';
|
|
6
|
+
|
|
7
|
+
export interface ShowHeaderProps extends Omit<HeaderProps, 'children'> {
|
|
8
|
+
title?: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ShowHeader = ({ title, actions, ...props }: ShowHeaderProps) => {
|
|
12
|
+
const translate = useTranslate();
|
|
13
|
+
const { record, defaultTitle } = useShowContext();
|
|
14
|
+
|
|
15
|
+
const headerTitle = React.useMemo(() => {
|
|
16
|
+
if (title !== undefined) {
|
|
17
|
+
return typeof title === 'string' ? translate(title, { _: title }) : title;
|
|
18
|
+
}
|
|
19
|
+
return defaultTitle;
|
|
20
|
+
}, [title, defaultTitle, translate]);
|
|
21
|
+
|
|
22
|
+
const headerActions = actions || (
|
|
23
|
+
<SpaceBetween direction="horizontal" size="xs">
|
|
24
|
+
<EditButton record={record} />
|
|
25
|
+
</SpaceBetween>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Header variant="h2" {...props} actions={headerActions}>
|
|
30
|
+
{headerTitle}
|
|
31
|
+
</Header>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default ShowHeader;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useEditContext, useResourceContext } from '@strato-admin/core';
|
|
5
|
+
import { Edit } from './Edit';
|
|
6
|
+
|
|
7
|
+
// Mock ra-core
|
|
8
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
|
+
|
|
10
|
+
// Mock Cloudscape components
|
|
11
|
+
vi.mock('@cloudscape-design/components/container', () => ({
|
|
12
|
+
default: ({ children, header }: any) => (
|
|
13
|
+
<div data-testid="container">
|
|
14
|
+
<div data-testid="container-header">{header}</div>
|
|
15
|
+
<div data-testid="container-content">{children}</div>
|
|
16
|
+
</div>
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('@cloudscape-design/components/header', () => ({
|
|
21
|
+
default: ({ children, actions }: any) => (
|
|
22
|
+
<header>
|
|
23
|
+
<div>{children}</div>
|
|
24
|
+
<div>{actions}</div>
|
|
25
|
+
</header>
|
|
26
|
+
),
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('@cloudscape-design/components/space-between', () => ({
|
|
30
|
+
default: ({ children }: any) => <div>{children}</div>,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
vi.mock('@cloudscape-design/components/button', () => ({
|
|
34
|
+
default: ({ children }: any) => <button>{children}</button>,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('Edit', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render nothing when loading', () => {
|
|
43
|
+
(useEditContext as any).mockReturnValue({
|
|
44
|
+
isLoading: true,
|
|
45
|
+
record: undefined,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const { queryByTestId } = render(
|
|
49
|
+
<Edit>
|
|
50
|
+
<div data-testid="content" />
|
|
51
|
+
</Edit>,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(queryByTestId('container')).toBeNull();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should render content and title when record is loaded', () => {
|
|
58
|
+
(useEditContext as any).mockReturnValue({
|
|
59
|
+
isLoading: false,
|
|
60
|
+
record: { id: 1 }, defaultTitle: 'Products',
|
|
61
|
+
resource: 'products',
|
|
62
|
+
});
|
|
63
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
64
|
+
|
|
65
|
+
const { getByTestId, getByText } = render(
|
|
66
|
+
<Edit>
|
|
67
|
+
<div data-testid="content">Hello World</div>
|
|
68
|
+
</Edit>,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
expect(getByTestId('container')).toBeDefined();
|
|
72
|
+
expect(getByTestId('content').textContent).toBe('Hello World');
|
|
73
|
+
expect(getByText('Products')).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should use provided title', () => {
|
|
77
|
+
(useEditContext as any).mockReturnValue({
|
|
78
|
+
isLoading: false,
|
|
79
|
+
record: { id: 1 }, defaultTitle: 'Products',
|
|
80
|
+
resource: 'products',
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { getByText } = render(
|
|
84
|
+
<Edit title="My Product">
|
|
85
|
+
<div />
|
|
86
|
+
</Edit>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(getByText('My Product')).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EditBase, useEditContext, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
|
|
3
|
+
import Container from '@cloudscape-design/components/container';
|
|
4
|
+
import { EditHeader } from './EditHeader';
|
|
5
|
+
import Form from '../form/Form';
|
|
6
|
+
|
|
7
|
+
export interface EditProps<_RecordType extends RaRecord = RaRecord> {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
inputSchema?: React.ReactNode;
|
|
10
|
+
title?: React.ReactNode;
|
|
11
|
+
actions?: React.ReactNode;
|
|
12
|
+
resource?: string;
|
|
13
|
+
id?: any;
|
|
14
|
+
mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
|
|
15
|
+
mutationOptions?: any;
|
|
16
|
+
queryOptions?: any;
|
|
17
|
+
redirect?: any;
|
|
18
|
+
transform?: any;
|
|
19
|
+
include?: string[];
|
|
20
|
+
exclude?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const EditUI = ({
|
|
24
|
+
children,
|
|
25
|
+
resource,
|
|
26
|
+
inputSchema,
|
|
27
|
+
title,
|
|
28
|
+
actions,
|
|
29
|
+
include,
|
|
30
|
+
exclude,
|
|
31
|
+
}: {
|
|
32
|
+
children?: React.ReactNode;
|
|
33
|
+
resource?: string;
|
|
34
|
+
inputSchema?: React.ReactNode;
|
|
35
|
+
title?: React.ReactNode;
|
|
36
|
+
actions?: React.ReactNode;
|
|
37
|
+
include?: string[];
|
|
38
|
+
exclude?: string[];
|
|
39
|
+
}) => {
|
|
40
|
+
const { record, isLoading } = useEditContext();
|
|
41
|
+
|
|
42
|
+
if (isLoading || !record) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const finalChildren = children || <Form include={include} exclude={exclude} />;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<ResourceSchemaProvider resource={resource} inputSchema={inputSchema}>
|
|
50
|
+
<Container header={<EditHeader title={title} actions={actions} />}>{finalChildren}</Container>
|
|
51
|
+
</ResourceSchemaProvider>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* An Edit component that provides record context and a Cloudscape Container.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* <Edit>
|
|
60
|
+
* <Form>
|
|
61
|
+
* <TextInput source="name" />
|
|
62
|
+
* </Form>
|
|
63
|
+
* </Edit>
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // Using InputSchema from context
|
|
67
|
+
* <Edit include={['name', 'price']} />
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* // Passing a custom input schema
|
|
71
|
+
* <Edit inputSchema={<InputSchema>...</InputSchema>}>
|
|
72
|
+
* <Form />
|
|
73
|
+
* </Edit>
|
|
74
|
+
*/
|
|
75
|
+
export const Edit = <RecordType extends RaRecord = any>({
|
|
76
|
+
children,
|
|
77
|
+
inputSchema,
|
|
78
|
+
title,
|
|
79
|
+
actions,
|
|
80
|
+
include,
|
|
81
|
+
exclude,
|
|
82
|
+
...props
|
|
83
|
+
}: EditProps<RecordType>) => {
|
|
84
|
+
return (
|
|
85
|
+
<EditBase {...props}>
|
|
86
|
+
<EditUI
|
|
87
|
+
resource={props.resource}
|
|
88
|
+
title={title}
|
|
89
|
+
actions={actions}
|
|
90
|
+
include={include}
|
|
91
|
+
exclude={exclude}
|
|
92
|
+
inputSchema={inputSchema}
|
|
93
|
+
>
|
|
94
|
+
{children}
|
|
95
|
+
</EditUI>
|
|
96
|
+
</EditBase>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
Edit.Header = EditHeader;
|
|
101
|
+
|
|
102
|
+
export default Edit;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Header, { HeaderProps } from '@cloudscape-design/components/header';
|
|
3
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
4
|
+
import { useResourceContext, useEditContext, useTranslate, useResourceDefinitions } from '@strato-admin/core';
|
|
5
|
+
|
|
6
|
+
export interface EditHeaderProps extends Omit<HeaderProps, 'children'> {
|
|
7
|
+
title?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const EditHeader = ({ title, actions, ...props }: EditHeaderProps) => {
|
|
11
|
+
const translate = useTranslate();
|
|
12
|
+
const { defaultTitle } = useEditContext();
|
|
13
|
+
|
|
14
|
+
const headerTitle = React.useMemo(() => {
|
|
15
|
+
if (title !== undefined) {
|
|
16
|
+
return typeof title === 'string' ? translate(title, { _: title }) : title;
|
|
17
|
+
}
|
|
18
|
+
return defaultTitle;
|
|
19
|
+
}, [title, defaultTitle, translate]);
|
|
20
|
+
|
|
21
|
+
const headerActions = actions || (
|
|
22
|
+
<SpaceBetween direction="horizontal" size="xs">
|
|
23
|
+
{/* Add default edit actions here if needed */}
|
|
24
|
+
</SpaceBetween>
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Header variant="h2" {...props} actions={headerActions}>
|
|
29
|
+
{headerTitle}
|
|
30
|
+
</Header>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default EditHeader;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { useRecordContext, useList, ListContextProvider, ResourceContextProvider, type RaRecord } from '@strato-admin/core';
|
|
2
|
+
import { type FieldProps } from './types';
|
|
3
|
+
|
|
4
|
+
export interface ArrayFieldProps<RecordType extends RaRecord = RaRecord> extends FieldProps<RecordType> {
|
|
5
|
+
/**
|
|
6
|
+
* Thecomponents to render for each item in the array.
|
|
7
|
+
*/
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Number of items per page if pagination is used within the field.
|
|
11
|
+
*/
|
|
12
|
+
perPage?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* ArrayField component that wraps its children with a ListContextProvider
|
|
17
|
+
* and ResourceContextProvider initialized with the array data and resource name.
|
|
18
|
+
*
|
|
19
|
+
* This allows using components that expect a ListContext or ResourceContext
|
|
20
|
+
* (like DataTable) to display nested arrays within a record.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <ArrayField source="products" resource="products">
|
|
24
|
+
* <DataTable variant="embedded">
|
|
25
|
+
* <DataTable.Col source="title" label="Title" />
|
|
26
|
+
* <DataTable.NumberCol source="price" label="Price" />
|
|
27
|
+
* </DataTable>
|
|
28
|
+
* </ArrayField>
|
|
29
|
+
*/
|
|
30
|
+
export const ArrayField = <RecordType extends RaRecord = any>(props: ArrayFieldProps<RecordType>) => {
|
|
31
|
+
const { source, resource, children, perPage = 10 } = props;
|
|
32
|
+
|
|
33
|
+
const record = useRecordContext<RecordType>();
|
|
34
|
+
const data = (source && record?.[source]) || [];
|
|
35
|
+
|
|
36
|
+
const targetResource = resource || source || '';
|
|
37
|
+
|
|
38
|
+
const listContext = useList({
|
|
39
|
+
data,
|
|
40
|
+
resource: targetResource,
|
|
41
|
+
perPage,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ResourceContextProvider value={targetResource}>
|
|
46
|
+
<ListContextProvider value={listContext}>{children as any}</ListContextProvider>
|
|
47
|
+
</ResourceContextProvider>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default ArrayField;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import Badge, { type BadgeProps } from '@cloudscape-design/components/badge';
|
|
2
|
+
import { type RaRecord, useFieldValue, useRecordContext } from '@strato-admin/core';
|
|
3
|
+
import RecordLink from '../RecordLink';
|
|
4
|
+
import { type FieldProps } from './types';
|
|
5
|
+
|
|
6
|
+
export interface BadgeFieldProps<RecordType extends RaRecord = RaRecord>
|
|
7
|
+
extends FieldProps<RecordType> {
|
|
8
|
+
/**
|
|
9
|
+
* The color of the badge.
|
|
10
|
+
* @default "grey"
|
|
11
|
+
*/
|
|
12
|
+
color?: BadgeProps['color'];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BadgeField = <RecordType extends RaRecord = RaRecord>(props: BadgeFieldProps<RecordType>) => {
|
|
16
|
+
const { source, record: recordProp, emptyText, link, color = 'grey' } = props;
|
|
17
|
+
|
|
18
|
+
const record = useRecordContext<RecordType>({ record: recordProp });
|
|
19
|
+
const value = useFieldValue<RecordType>({ source: source as any, record });
|
|
20
|
+
const hasValue = value !== null && value !== undefined && value !== '';
|
|
21
|
+
|
|
22
|
+
if (!hasValue) {
|
|
23
|
+
return <>{emptyText ?? null}</>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<RecordLink link={link}>
|
|
28
|
+
<Badge color={color}>{String(value)}</Badge>
|
|
29
|
+
</RecordLink>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default BadgeField;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import BooleanField from './BooleanField';
|
|
3
|
+
import { withRaContext } from '../stories/RaStoryDecorator';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof BooleanField> = {
|
|
6
|
+
title: 'Fields/BooleanField',
|
|
7
|
+
component: BooleanField,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
decorators: [withRaContext({ id: 1, isTrue: true, isFalse: false, isUndefined: undefined })],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof BooleanField>;
|
|
14
|
+
|
|
15
|
+
export const TrueValue: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
source: 'isTrue',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const FalseValue: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
source: 'isFalse',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const UndefinedValue: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
source: 'isUndefined',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const WithLabel: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
source: 'isTrue',
|
|
36
|
+
showLabel: true,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const CustomLabels: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
source: 'isTrue',
|
|
43
|
+
showLabel: true,
|
|
44
|
+
trueLabel: 'Active',
|
|
45
|
+
falseLabel: 'Inactive',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const CustomLabelsFalse: Story = {
|
|
50
|
+
args: {
|
|
51
|
+
source: 'isFalse',
|
|
52
|
+
showLabel: true,
|
|
53
|
+
trueLabel: 'Active',
|
|
54
|
+
falseLabel: 'Inactive',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect } from 'vitest';
|
|
4
|
+
import { useFieldValue, useRecordContext } from '@strato-admin/core';
|
|
5
|
+
import BooleanField from './BooleanField';
|
|
6
|
+
|
|
7
|
+
// Mock ra-core
|
|
8
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
|
+
|
|
10
|
+
// Mock Cloudscape components
|
|
11
|
+
vi.mock('@cloudscape-design/components/status-indicator', () => ({
|
|
12
|
+
default: ({ children, type }: any) => (
|
|
13
|
+
<div data-testid="status-indicator" data-type={type}>
|
|
14
|
+
{children}
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
describe('BooleanField', () => {
|
|
20
|
+
it('should render check icon for true value', () => {
|
|
21
|
+
const record = { is_active: true };
|
|
22
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
23
|
+
(useFieldValue as any).mockReturnValue(true);
|
|
24
|
+
|
|
25
|
+
const { getByTestId } = render(<BooleanField source="is_active" />);
|
|
26
|
+
|
|
27
|
+
const indicator = getByTestId('status-indicator');
|
|
28
|
+
expect(indicator.getAttribute('data-type')).toBe('success');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should render close icon for false value', () => {
|
|
32
|
+
const record = { is_active: false };
|
|
33
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
34
|
+
(useFieldValue as any).mockReturnValue(false);
|
|
35
|
+
|
|
36
|
+
const { getByTestId } = render(<BooleanField source="is_active" />);
|
|
37
|
+
|
|
38
|
+
const indicator = getByTestId('status-indicator');
|
|
39
|
+
expect(indicator.getAttribute('data-type')).toBe('not-started');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render label when showLabel is true', () => {
|
|
43
|
+
const record = { is_active: true };
|
|
44
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
45
|
+
(useFieldValue as any).mockReturnValue(true);
|
|
46
|
+
|
|
47
|
+
const { getByText } = render(<BooleanField source="is_active" showLabel />);
|
|
48
|
+
|
|
49
|
+
expect(getByText('Yes')).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should render custom labels', () => {
|
|
53
|
+
const record = { is_active: false };
|
|
54
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
55
|
+
(useFieldValue as any).mockReturnValue(false);
|
|
56
|
+
|
|
57
|
+
const { getByText } = render(
|
|
58
|
+
<BooleanField source="is_active" showLabel trueLabel="Enabled" falseLabel="Disabled" />,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(getByText('Disabled')).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import StatusIndicator from '@cloudscape-design/components/status-indicator';
|
|
2
|
+
import { RaRecord, useFieldValue, useRecordContext, useTranslate } from '@strato-admin/core';
|
|
3
|
+
import RecordLink from '../RecordLink';
|
|
4
|
+
import { type FieldProps } from './types';
|
|
5
|
+
|
|
6
|
+
export type BooleanFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
|
|
7
|
+
/**
|
|
8
|
+
* The label to display when the value is true. Defaults to "Yes".
|
|
9
|
+
*/
|
|
10
|
+
trueLabel?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The label to display when the value is false. Defaults to "No".
|
|
13
|
+
*/
|
|
14
|
+
falseLabel?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to show the text label alongside the icon.
|
|
17
|
+
*/
|
|
18
|
+
showLabel?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const BooleanField = <RecordType extends RaRecord = RaRecord>(props: BooleanFieldProps<RecordType>) => {
|
|
22
|
+
const { source, record: recordProp, trueLabel, falseLabel, showLabel = false, link } = props;
|
|
23
|
+
const record = useRecordContext<RecordType>({ record: recordProp });
|
|
24
|
+
const value = useFieldValue<RecordType>({ source: source as any, record });
|
|
25
|
+
const translate = useTranslate();
|
|
26
|
+
|
|
27
|
+
const isTrue = !!value;
|
|
28
|
+
|
|
29
|
+
const content = isTrue ? (
|
|
30
|
+
<StatusIndicator type="success" colorOverride="green">
|
|
31
|
+
{showLabel ? (trueLabel ?? translate('ra.boolean.true', { _: 'Yes' })) : null}
|
|
32
|
+
</StatusIndicator>
|
|
33
|
+
) : (
|
|
34
|
+
<StatusIndicator type="not-started">
|
|
35
|
+
{showLabel ? (falseLabel ?? translate('ra.boolean.false', { _: 'No' })) : null}
|
|
36
|
+
</StatusIndicator>
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return <RecordLink link={link}>{content}</RecordLink>;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default BooleanField;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import CurrencyField from './CurrencyField';
|
|
3
|
+
import { withRaContext } from '../stories/RaStoryDecorator';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof CurrencyField> = {
|
|
6
|
+
title: 'Fields/CurrencyField',
|
|
7
|
+
component: CurrencyField,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
decorators: [
|
|
10
|
+
withRaContext({
|
|
11
|
+
id: 1,
|
|
12
|
+
price: 123.45,
|
|
13
|
+
quantity: 10,
|
|
14
|
+
score: 0.85,
|
|
15
|
+
empty: null,
|
|
16
|
+
}),
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
type Story = StoryObj<typeof CurrencyField>;
|
|
22
|
+
|
|
23
|
+
export const Basic: Story = {
|
|
24
|
+
args: {
|
|
25
|
+
source: 'price',
|
|
26
|
+
currency: 'USD',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const CustomLocale: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
source: 'price',
|
|
33
|
+
locales: 'de-DE',
|
|
34
|
+
currency: 'EUR',
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const WithOptions: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
source: 'price',
|
|
41
|
+
currency: 'USD',
|
|
42
|
+
options: { minimumFractionDigits: 2 },
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Empty: Story = {
|
|
47
|
+
args: {
|
|
48
|
+
source: 'empty',
|
|
49
|
+
currency: 'USD',
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const WithEmptyText: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
source: 'empty',
|
|
56
|
+
emptyText: 'N/A',
|
|
57
|
+
currency: 'USD',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const WithLink: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
source: 'price',
|
|
64
|
+
link: 'show',
|
|
65
|
+
currency: 'USD',
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type RaRecord, useFieldValue, useRecordContext, useLocale } from '@strato-admin/core';
|
|
2
|
+
import RecordLink from '../RecordLink';
|
|
3
|
+
import { type FieldProps } from './types';
|
|
4
|
+
|
|
5
|
+
export type CurrencyFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
|
|
6
|
+
/**
|
|
7
|
+
* Options for Intl.NumberFormat.
|
|
8
|
+
*/
|
|
9
|
+
options?: Intl.NumberFormatOptions;
|
|
10
|
+
/**
|
|
11
|
+
* Locale(s) to use for formatting. Defaults to the current app locale.
|
|
12
|
+
*/
|
|
13
|
+
locales?: string | string[];
|
|
14
|
+
/**
|
|
15
|
+
* The currency to use in currency formatting.
|
|
16
|
+
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currency_2
|
|
17
|
+
*/
|
|
18
|
+
currency: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CurrencyField = <RecordType extends RaRecord = RaRecord>(props: CurrencyFieldProps<RecordType>) => {
|
|
22
|
+
const { source, record: recordProp, emptyText, options, locales, link, currency } = props;
|
|
23
|
+
const record = useRecordContext<RecordType>({ record: recordProp });
|
|
24
|
+
const value = useFieldValue<RecordType>({ source: source as any, record });
|
|
25
|
+
const currentLocale = useLocale();
|
|
26
|
+
const hasValue = value !== null && value !== undefined && value !== '';
|
|
27
|
+
|
|
28
|
+
if (!hasValue) {
|
|
29
|
+
return <>{emptyText ?? null}</>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const numberValue = typeof value === 'string' ? parseFloat(value) : value;
|
|
33
|
+
const formatOptions: Intl.NumberFormatOptions = {
|
|
34
|
+
style: 'currency',
|
|
35
|
+
currency,
|
|
36
|
+
...options,
|
|
37
|
+
};
|
|
38
|
+
const formattedValue = new Intl.NumberFormat(locales || currentLocale, formatOptions).format(numberValue);
|
|
39
|
+
|
|
40
|
+
return <RecordLink link={link}>{formattedValue}</RecordLink>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
(CurrencyField as any).isNumberColumn = true;
|
|
44
|
+
|
|
45
|
+
export default CurrencyField;
|