@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,63 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useResourceContext } from '@strato-admin/core';
|
|
5
|
+
import { Create } from './Create';
|
|
6
|
+
|
|
7
|
+
// Mock strato-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
|
+
describe('Create', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should render content and title', () => {
|
|
39
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
40
|
+
|
|
41
|
+
const { getByTestId, getByText } = render(
|
|
42
|
+
<Create>
|
|
43
|
+
<div data-testid="content">Create New</div>
|
|
44
|
+
</Create>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(getByTestId('container')).toBeDefined();
|
|
48
|
+
expect(getByTestId('content').textContent).toBe('Create New');
|
|
49
|
+
expect(getByText('Products')).toBeDefined();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should use provided title', () => {
|
|
53
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
54
|
+
|
|
55
|
+
const { getByText } = render(
|
|
56
|
+
<Create title="Add New Product">
|
|
57
|
+
<div />
|
|
58
|
+
</Create>,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
expect(getByText('Add New Product')).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { CreateBase, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
|
|
3
|
+
import Container from '@cloudscape-design/components/container';
|
|
4
|
+
import { CreateHeader } from './CreateHeader';
|
|
5
|
+
import Form from '../form/Form';
|
|
6
|
+
|
|
7
|
+
export interface CreateProps<RecordType extends RaRecord = RaRecord> {
|
|
8
|
+
children?: React.ReactNode;
|
|
9
|
+
inputSchema?: React.ReactNode;
|
|
10
|
+
title?: React.ReactNode;
|
|
11
|
+
actions?: React.ReactNode;
|
|
12
|
+
resource?: string;
|
|
13
|
+
record?: Partial<RecordType>;
|
|
14
|
+
redirect?: any;
|
|
15
|
+
transform?: any;
|
|
16
|
+
mutationOptions?: any;
|
|
17
|
+
include?: string[];
|
|
18
|
+
exclude?: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CreateUI = ({
|
|
22
|
+
children,
|
|
23
|
+
resource,
|
|
24
|
+
inputSchema,
|
|
25
|
+
title,
|
|
26
|
+
actions,
|
|
27
|
+
include,
|
|
28
|
+
exclude,
|
|
29
|
+
}: {
|
|
30
|
+
children?: React.ReactNode;
|
|
31
|
+
resource?: string;
|
|
32
|
+
inputSchema?: React.ReactNode;
|
|
33
|
+
title?: React.ReactNode;
|
|
34
|
+
actions?: React.ReactNode;
|
|
35
|
+
include?: string[];
|
|
36
|
+
exclude?: string[];
|
|
37
|
+
}) => {
|
|
38
|
+
const finalChildren = children || <Form include={include} exclude={exclude} />;
|
|
39
|
+
return (
|
|
40
|
+
<ResourceSchemaProvider resource={resource} inputSchema={inputSchema}>
|
|
41
|
+
<Container header={<CreateHeader title={title} actions={actions} />}>{finalChildren}</Container>
|
|
42
|
+
</ResourceSchemaProvider>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A Create component that provides record context and a Cloudscape Container.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* <Create>
|
|
51
|
+
* <Form>
|
|
52
|
+
* <TextInput source="name" />
|
|
53
|
+
* </Form>
|
|
54
|
+
* </Create>
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* // Using InputSchema from context
|
|
58
|
+
* <Create include={['name', 'price']} />
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Passing a custom input schema
|
|
62
|
+
* <Create inputSchema={<InputSchema>...</InputSchema>}>
|
|
63
|
+
* <Form />
|
|
64
|
+
* </Create>
|
|
65
|
+
*/
|
|
66
|
+
export const Create = <RecordType extends RaRecord = RaRecord>({
|
|
67
|
+
children,
|
|
68
|
+
inputSchema,
|
|
69
|
+
title,
|
|
70
|
+
actions,
|
|
71
|
+
include,
|
|
72
|
+
exclude,
|
|
73
|
+
...props
|
|
74
|
+
}: CreateProps<RecordType>) => {
|
|
75
|
+
return (
|
|
76
|
+
<CreateBase {...props}>
|
|
77
|
+
<CreateUI
|
|
78
|
+
resource={props.resource}
|
|
79
|
+
title={title}
|
|
80
|
+
actions={actions}
|
|
81
|
+
include={include}
|
|
82
|
+
exclude={exclude}
|
|
83
|
+
inputSchema={inputSchema}
|
|
84
|
+
>
|
|
85
|
+
{children}
|
|
86
|
+
</CreateUI>
|
|
87
|
+
</CreateBase>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
Create.Header = CreateHeader;
|
|
92
|
+
|
|
93
|
+
export default Create;
|
|
@@ -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 { useCreateContext, useTranslate } from '@strato-admin/core';
|
|
5
|
+
|
|
6
|
+
export interface CreateHeaderProps extends Omit<HeaderProps, 'children'> {
|
|
7
|
+
title?: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const CreateHeader = ({ title, actions, ...props }: CreateHeaderProps) => {
|
|
11
|
+
const translate = useTranslate();
|
|
12
|
+
const { defaultTitle } = useCreateContext();
|
|
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 create 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 CreateHeader;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useResourceContext, useTranslate, useRecordContext } from '@strato-admin/core';
|
|
5
|
+
import KeyValuePairs from './KeyValuePairs';
|
|
6
|
+
import CloudscapeKeyValuePairs from '@cloudscape-design/components/key-value-pairs';
|
|
7
|
+
|
|
8
|
+
// Mock strato-core
|
|
9
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
10
|
+
|
|
11
|
+
// Mock react-router-dom
|
|
12
|
+
vi.mock('react-router-dom', () => ({
|
|
13
|
+
useNavigate: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
// Mock Cloudscape components
|
|
17
|
+
vi.mock('@cloudscape-design/components/key-value-pairs', () => ({
|
|
18
|
+
default: vi.fn(({ items }: any) => (
|
|
19
|
+
<div data-testid="cloudscape-kvp">
|
|
20
|
+
{items.map((item: any, index: number) => (
|
|
21
|
+
<div key={index} data-testid={`kv-item-${index}`}>
|
|
22
|
+
<span data-testid={`kv-label-${index}`}>{item.label}</span>
|
|
23
|
+
<div data-testid={`kv-value-${index}`}>{item.value}</div>
|
|
24
|
+
</div>
|
|
25
|
+
))}
|
|
26
|
+
</div>
|
|
27
|
+
)),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock('@cloudscape-design/components/box', () => ({
|
|
31
|
+
default: ({ children }: any) => <div>{children}</div>,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
describe('KeyValuePairs', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
38
|
+
(useRecordContext as any).mockReturnValue({ id: 1, name: 'Gadget', price: 100 });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should render children and automatically generate labels', () => {
|
|
42
|
+
render(
|
|
43
|
+
<KeyValuePairs>
|
|
44
|
+
<KeyValuePairs.Field source="name" />
|
|
45
|
+
<KeyValuePairs.Field source="price" label="Cost" />
|
|
46
|
+
</KeyValuePairs>,
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const kvpProps = (CloudscapeKeyValuePairs as any).mock.calls[0][0];
|
|
50
|
+
expect(kvpProps.items).toHaveLength(2);
|
|
51
|
+
// We check the source/label props of the FieldTitle component
|
|
52
|
+
expect(kvpProps.items[0].label.props.source).toBe('name');
|
|
53
|
+
expect(kvpProps.items[1].label.props.label).toBe('Cost');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should use translation for labels if available', () => {
|
|
57
|
+
const translate = vi.fn((key) => {
|
|
58
|
+
if (key === 'resources.products.fields.name') return 'Product Name';
|
|
59
|
+
return key;
|
|
60
|
+
});
|
|
61
|
+
(useTranslate as any).mockReturnValue(translate);
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<KeyValuePairs>
|
|
65
|
+
<KeyValuePairs.Field source="name" />
|
|
66
|
+
</KeyValuePairs>,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const kvpProps = (CloudscapeKeyValuePairs as any).mock.calls[0][0];
|
|
70
|
+
expect(kvpProps.items[0].label.props.source).toBe('name');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should pass columns and minColumnWidth props to CloudscapeKeyValuePairs', () => {
|
|
74
|
+
render(
|
|
75
|
+
<KeyValuePairs columns={3} minColumnWidth={200}>
|
|
76
|
+
<KeyValuePairs.Field source="name" />
|
|
77
|
+
</KeyValuePairs>,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const kvpProps = (CloudscapeKeyValuePairs as any).mock.calls[0][0];
|
|
81
|
+
expect(kvpProps.columns).toBe(3);
|
|
82
|
+
expect(kvpProps.minColumnWidth).toBe(200);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should provide record context to children', () => {
|
|
86
|
+
const record = { id: 1, name: 'Gadget' };
|
|
87
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
88
|
+
|
|
89
|
+
// TextField should render 'Gadget' because we mocked useFieldValue to return record[source]
|
|
90
|
+
const { getByTestId } = render(
|
|
91
|
+
<KeyValuePairs>
|
|
92
|
+
<KeyValuePairs.Field source="name" />
|
|
93
|
+
</KeyValuePairs>,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(getByTestId('kv-value-0').textContent).toBe('Gadget');
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import CloudscapeKeyValuePairs, {
|
|
3
|
+
type KeyValuePairsProps as CloudscapeKeyValuePairsProps,
|
|
4
|
+
} from '@cloudscape-design/components/key-value-pairs';
|
|
5
|
+
import {
|
|
6
|
+
useResourceContext,
|
|
7
|
+
useRecordContext,
|
|
8
|
+
FieldTitle,
|
|
9
|
+
RecordContextProvider,
|
|
10
|
+
type RaRecord,
|
|
11
|
+
useFieldSchema,
|
|
12
|
+
} from '@strato-admin/core';
|
|
13
|
+
import TextField from '../field/TextField';
|
|
14
|
+
|
|
15
|
+
export interface KeyValuePairsProps extends Partial<Omit<CloudscapeKeyValuePairsProps, 'items'>> {
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
include?: string[];
|
|
18
|
+
exclude?: string[];
|
|
19
|
+
columns?: number;
|
|
20
|
+
minColumnWidth?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface KeyValueFieldProps {
|
|
24
|
+
source?: string;
|
|
25
|
+
label?: string;
|
|
26
|
+
children?: React.ReactNode;
|
|
27
|
+
field?: React.ComponentType<any>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* KeyValuePairs.Field is a helper component to define a field in a KeyValuePairs component.
|
|
32
|
+
* It mirrors the DataTable.Col pattern.
|
|
33
|
+
*/
|
|
34
|
+
export const KeyValueField = ({ children, source, field: FieldComponent }: KeyValueFieldProps) => {
|
|
35
|
+
if (children) {
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
{React.Children.map(children, (child) =>
|
|
39
|
+
React.isValidElement(child) ? React.cloneElement(child, { source } as any) : child
|
|
40
|
+
)}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
if (FieldComponent) {
|
|
45
|
+
return <FieldComponent source={source} />;
|
|
46
|
+
}
|
|
47
|
+
return <TextField source={source} />;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A KeyValuePairs component that mirrors the Cloudscape KeyValuePairs component
|
|
52
|
+
* but automatically handles labels and record context for its children.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* <KeyValuePairs columns={2}>
|
|
56
|
+
* <TextField source="name" label="Full Name" />
|
|
57
|
+
* <KeyValuePairs.Field source="age" label="Age" />
|
|
58
|
+
* </KeyValuePairs>
|
|
59
|
+
*/
|
|
60
|
+
export const KeyValuePairs = <RecordType extends RaRecord = RaRecord>({
|
|
61
|
+
children,
|
|
62
|
+
include,
|
|
63
|
+
exclude,
|
|
64
|
+
columns = 3, // Default to 3 columns if not specified
|
|
65
|
+
minColumnWidth,
|
|
66
|
+
...props
|
|
67
|
+
}: KeyValuePairsProps) => {
|
|
68
|
+
const resource = useResourceContext();
|
|
69
|
+
const record = useRecordContext<RecordType>();
|
|
70
|
+
const schemaChildren = useFieldSchema();
|
|
71
|
+
|
|
72
|
+
const finalChildren = React.useMemo(() => {
|
|
73
|
+
const baseChildren = children || schemaChildren;
|
|
74
|
+
let result = React.Children.toArray(baseChildren);
|
|
75
|
+
|
|
76
|
+
if (include) {
|
|
77
|
+
result = result.filter(
|
|
78
|
+
(child) => React.isValidElement(child) && include.includes((child.props as any).source)
|
|
79
|
+
);
|
|
80
|
+
} else if (exclude) {
|
|
81
|
+
result = result.filter(
|
|
82
|
+
(child) => React.isValidElement(child) && !exclude.includes((child.props as any).source)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return result;
|
|
87
|
+
}, [children, schemaChildren, include, exclude]);
|
|
88
|
+
|
|
89
|
+
const items =
|
|
90
|
+
React.Children.map(finalChildren, (child) => {
|
|
91
|
+
if (!React.isValidElement(child)) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { source, label } = child.props as any;
|
|
96
|
+
return {
|
|
97
|
+
label: <FieldTitle source={source} resource={resource} label={label} />,
|
|
98
|
+
value: <RecordContextProvider value={record}>{child as any}</RecordContextProvider>,
|
|
99
|
+
};
|
|
100
|
+
})?.filter((item): item is Exclude<typeof item, null> => item !== null) || [];
|
|
101
|
+
|
|
102
|
+
return <CloudscapeKeyValuePairs {...props} items={items} columns={columns} minColumnWidth={minColumnWidth} />;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
KeyValuePairs.Field = KeyValueField;
|
|
106
|
+
|
|
107
|
+
export default KeyValuePairs;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useShowContext, useResourceContext } from '@strato-admin/core';
|
|
5
|
+
import Show from './Show';
|
|
6
|
+
|
|
7
|
+
// Mock strato-core
|
|
8
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
|
+
|
|
10
|
+
// Mock react-router-dom
|
|
11
|
+
vi.mock('react-router-dom', () => ({
|
|
12
|
+
useNavigate: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock Cloudscape components
|
|
16
|
+
vi.mock('@cloudscape-design/components/container', () => ({
|
|
17
|
+
default: ({ children, header }: any) => (
|
|
18
|
+
<div data-testid="container">
|
|
19
|
+
<div data-testid="container-header">{header}</div>
|
|
20
|
+
<div data-testid="container-content">{children}</div>
|
|
21
|
+
</div>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('@cloudscape-design/components/header', () => ({
|
|
26
|
+
default: ({ children, actions }: any) => (
|
|
27
|
+
<header>
|
|
28
|
+
<div>{children}</div>
|
|
29
|
+
<div>{actions}</div>
|
|
30
|
+
</header>
|
|
31
|
+
),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock('@cloudscape-design/components/space-between', () => ({
|
|
35
|
+
default: ({ children }: any) => <div>{children}</div>,
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock('@cloudscape-design/components/button', () => ({
|
|
39
|
+
default: ({ children }: any) => <button>{children}</button>,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('Show', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should render nothing when loading', () => {
|
|
48
|
+
(useShowContext as any).mockReturnValue({
|
|
49
|
+
isLoading: true,
|
|
50
|
+
record: undefined,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const { queryByTestId } = render(
|
|
54
|
+
<Show>
|
|
55
|
+
<div data-testid="content" />
|
|
56
|
+
</Show>,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(queryByTestId('container')).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should render content and title when record is loaded', () => {
|
|
63
|
+
(useShowContext as any).mockReturnValue({
|
|
64
|
+
isLoading: false,
|
|
65
|
+
record: { id: 1 }, defaultTitle: 'Products',
|
|
66
|
+
resource: 'products',
|
|
67
|
+
});
|
|
68
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
69
|
+
|
|
70
|
+
const { getByTestId, getByText } = render(
|
|
71
|
+
<Show>
|
|
72
|
+
<div data-testid="content">Hello World</div>
|
|
73
|
+
</Show>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
expect(getByTestId('container')).toBeDefined();
|
|
77
|
+
expect(getByTestId('content').textContent).toBe('Hello World');
|
|
78
|
+
expect(getByText('Products')).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should use provided title', () => {
|
|
82
|
+
(useShowContext as any).mockReturnValue({
|
|
83
|
+
isLoading: false,
|
|
84
|
+
record: { id: 1 }, defaultTitle: 'Products',
|
|
85
|
+
resource: 'products',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const { getByText } = render(
|
|
89
|
+
<Show title="My Product">
|
|
90
|
+
<div />
|
|
91
|
+
</Show>,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(getByText('My Product')).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ShowBase, useShowContext, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
|
|
3
|
+
import Container from '@cloudscape-design/components/container';
|
|
4
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
5
|
+
import { ShowHeader } from './ShowHeader';
|
|
6
|
+
import KeyValuePairs from './KeyValuePairs';
|
|
7
|
+
|
|
8
|
+
export interface ShowProps<_RecordType extends RaRecord = RaRecord> {
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
fieldSchema?: React.ReactNode;
|
|
11
|
+
title?: React.ReactNode;
|
|
12
|
+
actions?: React.ReactNode;
|
|
13
|
+
resource?: string;
|
|
14
|
+
id?: any;
|
|
15
|
+
queryOptions?: any;
|
|
16
|
+
include?: string[];
|
|
17
|
+
exclude?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ShowUI = ({
|
|
21
|
+
children,
|
|
22
|
+
resource,
|
|
23
|
+
fieldSchema,
|
|
24
|
+
title,
|
|
25
|
+
actions,
|
|
26
|
+
include,
|
|
27
|
+
exclude,
|
|
28
|
+
}: {
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
resource?: string;
|
|
31
|
+
fieldSchema?: React.ReactNode;
|
|
32
|
+
title?: React.ReactNode;
|
|
33
|
+
actions?: React.ReactNode;
|
|
34
|
+
include?: string[];
|
|
35
|
+
exclude?: string[];
|
|
36
|
+
}) => {
|
|
37
|
+
const { record, isLoading } = useShowContext();
|
|
38
|
+
|
|
39
|
+
if (isLoading || !record) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const finalChildren = children || <KeyValuePairs include={include} exclude={exclude} />;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ResourceSchemaProvider resource={resource} fieldSchema={fieldSchema}>
|
|
47
|
+
<Container header={<ShowHeader title={title} actions={actions} />}>
|
|
48
|
+
<SpaceBetween direction="vertical" size="l">
|
|
49
|
+
{finalChildren}
|
|
50
|
+
</SpaceBetween>
|
|
51
|
+
</Container>
|
|
52
|
+
</ResourceSchemaProvider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A Show component that provides record context and a Cloudscape Container.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* <Show>
|
|
61
|
+
* <KeyValuePairs>
|
|
62
|
+
* <TextField source="name" />
|
|
63
|
+
* <NumberField source="price" />
|
|
64
|
+
* </KeyValuePairs>
|
|
65
|
+
* </Show>
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Using FieldSchema from context
|
|
69
|
+
* <Show include={['name', 'price']} />
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* // Passing a custom field schema
|
|
73
|
+
* <Show fieldSchema={<FieldSchema>...</FieldSchema>}>
|
|
74
|
+
* <KeyValuePairs />
|
|
75
|
+
* </Show>
|
|
76
|
+
*/
|
|
77
|
+
export const Show = <RecordType extends RaRecord = RaRecord>({
|
|
78
|
+
children,
|
|
79
|
+
fieldSchema,
|
|
80
|
+
title,
|
|
81
|
+
actions,
|
|
82
|
+
include,
|
|
83
|
+
exclude,
|
|
84
|
+
...props
|
|
85
|
+
}: ShowProps<RecordType>) => {
|
|
86
|
+
return (
|
|
87
|
+
<ShowBase {...props}>
|
|
88
|
+
<ShowUI
|
|
89
|
+
resource={props.resource}
|
|
90
|
+
title={title}
|
|
91
|
+
actions={actions}
|
|
92
|
+
include={include}
|
|
93
|
+
exclude={exclude}
|
|
94
|
+
fieldSchema={fieldSchema}
|
|
95
|
+
>
|
|
96
|
+
{children}
|
|
97
|
+
</ShowUI>
|
|
98
|
+
</ShowBase>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
Show.Header = ShowHeader;
|
|
103
|
+
|
|
104
|
+
export default Show;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useResourceContext, useShowContext } from '@strato-admin/core';
|
|
5
|
+
import { ShowHeader } from './ShowHeader';
|
|
6
|
+
|
|
7
|
+
// Mock strato-core
|
|
8
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
|
+
|
|
10
|
+
// Mock react-router-dom
|
|
11
|
+
vi.mock('react-router-dom', () => ({
|
|
12
|
+
useNavigate: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock Cloudscape components
|
|
16
|
+
vi.mock('@cloudscape-design/components/header', () => ({
|
|
17
|
+
default: ({ children, actions }: any) => (
|
|
18
|
+
<header>
|
|
19
|
+
<div data-testid="header-title">{children}</div>
|
|
20
|
+
<div data-testid="header-actions">{actions}</div>
|
|
21
|
+
</header>
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('@cloudscape-design/components/space-between', () => ({
|
|
26
|
+
default: ({ children }: any) => <div data-testid="space-between">{children}</div>,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('@cloudscape-design/components/button', () => ({
|
|
30
|
+
default: ({ children, variant }: any) => (
|
|
31
|
+
<button data-testid="button" data-variant={variant}>
|
|
32
|
+
{children}
|
|
33
|
+
</button>
|
|
34
|
+
),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
describe('ShowHeader', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.clearAllMocks();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should render title from resource', () => {
|
|
43
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
44
|
+
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
45
|
+
|
|
46
|
+
const { getByTestId } = render(<ShowHeader />);
|
|
47
|
+
|
|
48
|
+
expect(getByTestId('header-title').textContent).toBe('Products');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render provided title', () => {
|
|
52
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
53
|
+
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
54
|
+
|
|
55
|
+
const { getByTestId } = render(<ShowHeader title="My Product" />);
|
|
56
|
+
|
|
57
|
+
expect(getByTestId('header-title').textContent).toBe('My Product');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should render EditButton by default as primary', () => {
|
|
61
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
62
|
+
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
63
|
+
|
|
64
|
+
const { getByText } = render(<ShowHeader />);
|
|
65
|
+
|
|
66
|
+
const editButton = getByText('Edit');
|
|
67
|
+
expect(editButton).toBeDefined();
|
|
68
|
+
expect(editButton.getAttribute('data-variant')).toBe('primary');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should render custom actions if provided', () => {
|
|
72
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
73
|
+
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
74
|
+
|
|
75
|
+
const { getByTestId, queryByText } = render(<ShowHeader actions={<div data-testid="custom-action">Custom</div>} />);
|
|
76
|
+
|
|
77
|
+
expect(getByTestId('custom-action')).toBeDefined();
|
|
78
|
+
expect(queryByText('ra.action.edit')).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
});
|