@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,93 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import StatusIndicatorField from './StatusIndicatorField';
|
|
3
|
+
import { withRaContext } from '../stories/RaStoryDecorator';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof StatusIndicatorField> = {
|
|
6
|
+
title: 'Fields/StatusIndicatorField',
|
|
7
|
+
component: StatusIndicatorField,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
decorators: [
|
|
10
|
+
withRaContext({
|
|
11
|
+
id: 1,
|
|
12
|
+
status: 'active',
|
|
13
|
+
error_status: 'error',
|
|
14
|
+
warning_status: 'warning',
|
|
15
|
+
loading_status: 'loading',
|
|
16
|
+
custom_value: 'Ready',
|
|
17
|
+
}),
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
type Story = StoryObj<typeof StatusIndicatorField>;
|
|
23
|
+
|
|
24
|
+
export const Basic: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
source: 'status',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const Success: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
source: 'status',
|
|
33
|
+
type: 'success',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Error: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
source: 'error_status',
|
|
40
|
+
type: 'error',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Warning: Story = {
|
|
45
|
+
args: {
|
|
46
|
+
source: 'warning_status',
|
|
47
|
+
type: 'warning',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const Loading: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
source: 'loading_status',
|
|
54
|
+
type: 'loading',
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const WithMapping: Story = {
|
|
59
|
+
args: {
|
|
60
|
+
source: 'status',
|
|
61
|
+
mapping: {
|
|
62
|
+
active: 'success',
|
|
63
|
+
inactive: 'stopped',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const DynamicType: Story = {
|
|
69
|
+
args: {
|
|
70
|
+
source: 'status',
|
|
71
|
+
type: (value) => (value === 'active' ? 'success' : 'error'),
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const ColorOverride: Story = {
|
|
76
|
+
args: {
|
|
77
|
+
source: 'custom_value',
|
|
78
|
+
type: 'success',
|
|
79
|
+
colorOverride: 'blue',
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const WithLabelChildren: Story = {
|
|
84
|
+
render: (args) => (
|
|
85
|
+
<StatusIndicatorField {...args}>
|
|
86
|
+
<StatusIndicatorField.Label value="active" type="success" label="Is Active" />
|
|
87
|
+
<StatusIndicatorField.Label value="inactive" type="stopped" label="Is Inactive" />
|
|
88
|
+
</StatusIndicatorField>
|
|
89
|
+
),
|
|
90
|
+
args: {
|
|
91
|
+
source: 'status',
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { render, cleanup } from '@testing-library/react';
|
|
2
|
+
import { vi, describe, it, expect, afterEach } from 'vitest';
|
|
3
|
+
import { useFieldValue, useRecordContext } from '@strato-admin/core';
|
|
4
|
+
import StatusIndicatorField from './StatusIndicatorField';
|
|
5
|
+
|
|
6
|
+
// Mock ra-core
|
|
7
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
8
|
+
|
|
9
|
+
// Mock Cloudscape components
|
|
10
|
+
vi.mock('@cloudscape-design/components/status-indicator', () => ({
|
|
11
|
+
default: ({ children, type, colorOverride, iconAriaLabel }: any) => (
|
|
12
|
+
<div
|
|
13
|
+
data-testid="status-indicator"
|
|
14
|
+
data-type={type}
|
|
15
|
+
data-color={colorOverride}
|
|
16
|
+
data-aria={iconAriaLabel}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('StatusIndicatorField', () => {
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
cleanup();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should render value with default info type', () => {
|
|
29
|
+
const record = { status: 'active' };
|
|
30
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
31
|
+
(useFieldValue as any).mockReturnValue('active');
|
|
32
|
+
|
|
33
|
+
const { getByTestId, getByText } = render(<StatusIndicatorField source="status" />);
|
|
34
|
+
|
|
35
|
+
const indicator = getByTestId('status-indicator');
|
|
36
|
+
expect(indicator.getAttribute('data-type')).toBe('info');
|
|
37
|
+
expect(getByText('active')).toBeDefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should render with specified type', () => {
|
|
41
|
+
const record = { status: 'active' };
|
|
42
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
43
|
+
(useFieldValue as any).mockReturnValue('active');
|
|
44
|
+
|
|
45
|
+
const { getByTestId } = render(<StatusIndicatorField source="status" type="success" />);
|
|
46
|
+
|
|
47
|
+
const indicator = getByTestId('status-indicator');
|
|
48
|
+
expect(indicator.getAttribute('data-type')).toBe('success');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render with mapping', () => {
|
|
52
|
+
const record = { status: 'inactive' };
|
|
53
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
54
|
+
(useFieldValue as any).mockReturnValue('inactive');
|
|
55
|
+
|
|
56
|
+
const { getByTestId } = render(
|
|
57
|
+
<StatusIndicatorField source="status" mapping={{ active: 'success', inactive: 'stopped' }} />,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const indicator = getByTestId('status-indicator');
|
|
61
|
+
expect(indicator.getAttribute('data-type')).toBe('stopped');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should render with dynamic type function', () => {
|
|
65
|
+
const record = { status: 'error' };
|
|
66
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
67
|
+
(useFieldValue as any).mockReturnValue('error');
|
|
68
|
+
|
|
69
|
+
const { getByTestId } = render(
|
|
70
|
+
<StatusIndicatorField
|
|
71
|
+
source="status"
|
|
72
|
+
type={(val) => (val === 'error' ? 'error' : 'info')}
|
|
73
|
+
/>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const indicator = getByTestId('status-indicator');
|
|
77
|
+
expect(indicator.getAttribute('data-type')).toBe('error');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should render nothing when value is missing', () => {
|
|
81
|
+
(useRecordContext as any).mockReturnValue({});
|
|
82
|
+
(useFieldValue as any).mockReturnValue(undefined);
|
|
83
|
+
|
|
84
|
+
const { queryByTestId } = render(<StatusIndicatorField source="status" />);
|
|
85
|
+
|
|
86
|
+
expect(queryByTestId('status-indicator')).toBeNull();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should render emptyText when value is missing', () => {
|
|
90
|
+
(useRecordContext as any).mockReturnValue({});
|
|
91
|
+
(useFieldValue as any).mockReturnValue(undefined);
|
|
92
|
+
|
|
93
|
+
const { getByText } = render(
|
|
94
|
+
<StatusIndicatorField source="status" emptyText="No Status" />,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(getByText('No Status')).toBeDefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('should render with colorOverride', () => {
|
|
101
|
+
const record = { status: 'custom' };
|
|
102
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
103
|
+
(useFieldValue as any).mockReturnValue('custom');
|
|
104
|
+
|
|
105
|
+
const { getByTestId } = render(
|
|
106
|
+
<StatusIndicatorField source="status" type="success" colorOverride="blue" />,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const indicator = getByTestId('status-indicator');
|
|
110
|
+
expect(indicator.getAttribute('data-color')).toBe('blue');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should render with iconAriaLabel', () => {
|
|
114
|
+
const record = { status: 'active' };
|
|
115
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
116
|
+
(useFieldValue as any).mockReturnValue('active');
|
|
117
|
+
|
|
118
|
+
const { getByTestId } = render(
|
|
119
|
+
<StatusIndicatorField source="status" iconAriaLabel="Active status" />,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const indicator = getByTestId('status-indicator');
|
|
123
|
+
expect(indicator.getAttribute('data-aria')).toBe('Active status');
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should render with Label children mapping', () => {
|
|
127
|
+
const record = { status: 'rejected' };
|
|
128
|
+
(useRecordContext as any).mockReturnValue(record);
|
|
129
|
+
(useFieldValue as any).mockReturnValue('rejected');
|
|
130
|
+
|
|
131
|
+
const { getByTestId, getByText } = render(
|
|
132
|
+
<StatusIndicatorField source="status">
|
|
133
|
+
<StatusIndicatorField.Label value="accepted" type="success" label="Accepted" />
|
|
134
|
+
<StatusIndicatorField.Label value="rejected" type="error" label="Rejected" color="red" />
|
|
135
|
+
</StatusIndicatorField>,
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
const indicator = getByTestId('status-indicator');
|
|
139
|
+
expect(indicator.getAttribute('data-type')).toBe('error');
|
|
140
|
+
expect(indicator.getAttribute('data-color')).toBe('red');
|
|
141
|
+
expect(getByText('Rejected')).toBeDefined();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import StatusIndicator, { type StatusIndicatorProps } from '@cloudscape-design/components/status-indicator';
|
|
2
|
+
import React, { type ReactElement } from 'react';
|
|
3
|
+
import { type RaRecord, useFieldValue, useRecordContext, useTranslate } from '@strato-admin/core';
|
|
4
|
+
import RecordLink from '../RecordLink';
|
|
5
|
+
import { type FieldProps } from './types';
|
|
6
|
+
|
|
7
|
+
export interface StatusIndicatorLabelProps {
|
|
8
|
+
/**
|
|
9
|
+
* The value to match against the field's value.
|
|
10
|
+
*/
|
|
11
|
+
value: any;
|
|
12
|
+
/**
|
|
13
|
+
* The type of the status indicator for this value.
|
|
14
|
+
*/
|
|
15
|
+
type: StatusIndicatorProps.Type;
|
|
16
|
+
/**
|
|
17
|
+
* Color override for the status indicator for this value.
|
|
18
|
+
*/
|
|
19
|
+
color?: StatusIndicatorProps.Color;
|
|
20
|
+
/**
|
|
21
|
+
* The label to display. If not provided, the value will be used.
|
|
22
|
+
*/
|
|
23
|
+
label?: React.ReactNode;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A declarative way to define status indicator mapping.
|
|
28
|
+
*/
|
|
29
|
+
export const StatusIndicatorLabel = (_: StatusIndicatorLabelProps) => null;
|
|
30
|
+
|
|
31
|
+
export interface StatusIndicatorFieldProps<RecordType extends RaRecord = RaRecord>
|
|
32
|
+
extends FieldProps<RecordType> {
|
|
33
|
+
/**
|
|
34
|
+
* The type of the status indicator.
|
|
35
|
+
* If provided as a string, it will be used for all values.
|
|
36
|
+
* If provided as a function, it will be called with the field value and the record.
|
|
37
|
+
* @default "info"
|
|
38
|
+
*/
|
|
39
|
+
type?: StatusIndicatorProps.Type | ((value: any, record: RecordType) => StatusIndicatorProps.Type);
|
|
40
|
+
/**
|
|
41
|
+
* A mapping from field values to status indicator types.
|
|
42
|
+
*/
|
|
43
|
+
mapping?: Record<string | number, StatusIndicatorProps.Type>;
|
|
44
|
+
/**
|
|
45
|
+
* Icon aria label for screen readers.
|
|
46
|
+
*/
|
|
47
|
+
iconAriaLabel?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Color override for the status indicator.
|
|
50
|
+
*/
|
|
51
|
+
colorOverride?: StatusIndicatorProps.Color;
|
|
52
|
+
/**
|
|
53
|
+
* Declarative mapping using StatusIndicatorLabel children.
|
|
54
|
+
*/
|
|
55
|
+
children?: React.ReactNode;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const StatusIndicatorField = <RecordType extends RaRecord = RaRecord>(
|
|
59
|
+
props: StatusIndicatorFieldProps<RecordType>
|
|
60
|
+
) => {
|
|
61
|
+
const {
|
|
62
|
+
source,
|
|
63
|
+
record: recordProp,
|
|
64
|
+
emptyText,
|
|
65
|
+
link,
|
|
66
|
+
type,
|
|
67
|
+
mapping,
|
|
68
|
+
iconAriaLabel,
|
|
69
|
+
colorOverride,
|
|
70
|
+
children,
|
|
71
|
+
} = props;
|
|
72
|
+
|
|
73
|
+
const record = useRecordContext<RecordType>({ record: recordProp });
|
|
74
|
+
const value = useFieldValue<RecordType>({ source: source as any, record });
|
|
75
|
+
const translate = useTranslate();
|
|
76
|
+
const hasValue = value !== null && value !== undefined && value !== '';
|
|
77
|
+
|
|
78
|
+
if (!hasValue) {
|
|
79
|
+
return <>{emptyText ?? null}</>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 1. Try to find mapping from children
|
|
83
|
+
const childrenArray = React.Children.toArray(children) as ReactElement<StatusIndicatorLabelProps>[];
|
|
84
|
+
const matchingLabel = childrenArray.find((child) => child.props?.value === value);
|
|
85
|
+
|
|
86
|
+
let statusType: StatusIndicatorProps.Type = 'info';
|
|
87
|
+
let finalColorOverride = colorOverride;
|
|
88
|
+
let label: React.ReactNode = String(value);
|
|
89
|
+
|
|
90
|
+
if (matchingLabel) {
|
|
91
|
+
statusType = matchingLabel.props.type;
|
|
92
|
+
if (matchingLabel.props.color) {
|
|
93
|
+
finalColorOverride = matchingLabel.props.color;
|
|
94
|
+
}
|
|
95
|
+
if (matchingLabel.props.label) {
|
|
96
|
+
label = typeof matchingLabel.props.label === 'string'
|
|
97
|
+
? translate(matchingLabel.props.label)
|
|
98
|
+
: matchingLabel.props.label;
|
|
99
|
+
}
|
|
100
|
+
} else if (typeof type === 'function') {
|
|
101
|
+
statusType = type(value, record as RecordType);
|
|
102
|
+
} else if (type) {
|
|
103
|
+
statusType = type;
|
|
104
|
+
} else if (mapping && (value as string | number) in mapping) {
|
|
105
|
+
statusType = mapping[value as string | number];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<RecordLink link={link}>
|
|
110
|
+
<StatusIndicator type={statusType} iconAriaLabel={iconAriaLabel} colorOverride={finalColorOverride}>
|
|
111
|
+
{label}
|
|
112
|
+
</StatusIndicator>
|
|
113
|
+
</RecordLink>
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
StatusIndicatorField.Label = StatusIndicatorLabel;
|
|
118
|
+
|
|
119
|
+
export default StatusIndicatorField;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import TextField from './TextField';
|
|
3
|
+
import { withRaContext } from '../stories/RaStoryDecorator';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof TextField> = {
|
|
6
|
+
title: 'Fields/TextField',
|
|
7
|
+
component: TextField,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
decorators: [withRaContext({ id: 1, title: 'Sample Title', description: 'This is a description', empty: '' })],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
type Story = StoryObj<typeof TextField>;
|
|
14
|
+
|
|
15
|
+
export const Basic: Story = {
|
|
16
|
+
args: {
|
|
17
|
+
source: 'title',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const WithDescription: Story = {
|
|
22
|
+
args: {
|
|
23
|
+
source: 'description',
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const Empty: Story = {
|
|
28
|
+
args: {
|
|
29
|
+
source: 'empty',
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const WithEmptyText: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
source: 'missing_field',
|
|
36
|
+
emptyText: 'No value provided',
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const WithLink: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
source: 'title',
|
|
43
|
+
link: 'show',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type RaRecord, useFieldValue, useRecordContext } from '@strato-admin/core';
|
|
2
|
+
import RecordLink from '../RecordLink';
|
|
3
|
+
import { type FieldProps } from './types';
|
|
4
|
+
|
|
5
|
+
export type TextFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType>;
|
|
6
|
+
|
|
7
|
+
const TextField = <RecordType extends RaRecord = RaRecord>(props: TextFieldProps<RecordType>) => {
|
|
8
|
+
const { source, record: recordProp, emptyText, link } = props;
|
|
9
|
+
|
|
10
|
+
const record = useRecordContext<RecordType>({ record: recordProp });
|
|
11
|
+
const value = useFieldValue<RecordType>({ source: source as any, record });
|
|
12
|
+
const hasValue = value !== null && value !== undefined && value !== '';
|
|
13
|
+
|
|
14
|
+
return <RecordLink link={link}>{hasValue ? String(value) : (emptyText ?? null)}</RecordLink>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default TextField;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export { default as StatusIndicatorField } from './StatusIndicatorField';
|
|
3
|
+
export * from './StatusIndicatorField';
|
|
4
|
+
export { default as TextField } from './TextField';
|
|
5
|
+
export { default as IdField } from './IdField';
|
|
6
|
+
export { default as NumberField } from './NumberField';
|
|
7
|
+
export { default as CurrencyField } from './CurrencyField';
|
|
8
|
+
export { default as DateField } from './DateField';
|
|
9
|
+
export { default as BooleanField } from './BooleanField';
|
|
10
|
+
export { default as BadgeField } from './BadgeField';
|
|
11
|
+
export { default as ReferenceField } from './ReferenceField';
|
|
12
|
+
export { default as ReferenceManyField } from './ReferenceManyField';
|
|
13
|
+
export { default as ArrayField } from './ArrayField';
|
|
14
|
+
export * from './TextField';
|
|
15
|
+
export * from './IdField';
|
|
16
|
+
export * from './NumberField';
|
|
17
|
+
export * from './DateField';
|
|
18
|
+
export * from './BooleanField';
|
|
19
|
+
export * from './BadgeField';
|
|
20
|
+
export * from './ReferenceField';
|
|
21
|
+
export * from './ReferenceManyField';
|
|
22
|
+
export * from './ArrayField';
|
|
23
|
+
export * from './CurrencyField';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { type BaseFieldProps, type RaRecord } from '@strato-admin/core';
|
|
3
|
+
import { type RecordLinkType } from '../RecordLink';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Common props for all field components in strato-cloudscape.
|
|
7
|
+
*/
|
|
8
|
+
export interface FieldProps<RecordType extends RaRecord = RaRecord>
|
|
9
|
+
extends Omit<BaseFieldProps<RecordType>, 'source'> {
|
|
10
|
+
/**
|
|
11
|
+
* The property name in the record that should be displayed.
|
|
12
|
+
*/
|
|
13
|
+
source?: string;
|
|
14
|
+
/**
|
|
15
|
+
* The label to display for this field. Usually inferred from the source.
|
|
16
|
+
*/
|
|
17
|
+
label?: ReactNode;
|
|
18
|
+
/**
|
|
19
|
+
* Whether the field is sortable in a table.
|
|
20
|
+
* @default true
|
|
21
|
+
*/
|
|
22
|
+
sortable?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Whether to link the field to another page.
|
|
25
|
+
* - true: links to the 'edit' page of the current resource
|
|
26
|
+
* - 'edit' | 'show': links to the specified page type
|
|
27
|
+
* - string: a custom URL
|
|
28
|
+
* - function: (record, resource) => string
|
|
29
|
+
*/
|
|
30
|
+
link?: RecordLinkType;
|
|
31
|
+
/**
|
|
32
|
+
* The text to display if the value is empty or null.
|
|
33
|
+
*/
|
|
34
|
+
emptyText?: ReactNode;
|
|
35
|
+
/**
|
|
36
|
+
* Configuration for the inferred form input.
|
|
37
|
+
* - object: Props passed to the inferred Input component.
|
|
38
|
+
* - ReactElement: A specific Input component to use (escape hatch).
|
|
39
|
+
* - false: Excludes this field from forms.
|
|
40
|
+
*/
|
|
41
|
+
input?: Record<string, any> | React.ReactElement | false;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the field is required.
|
|
44
|
+
* This is used to automatically add validation to the inferred input
|
|
45
|
+
* and potentially show warnings in display views.
|
|
46
|
+
*/
|
|
47
|
+
isRequired?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Additional text to help the user fill in the field.
|
|
50
|
+
* Passed to the inferred Input component's FormField.
|
|
51
|
+
*/
|
|
52
|
+
description?: ReactNode;
|
|
53
|
+
/**
|
|
54
|
+
* Text describing constraints (e.g., "Must be between 1 and 100").
|
|
55
|
+
* Passed to the inferred Input component's FormField.
|
|
56
|
+
*/
|
|
57
|
+
constraintText?: ReactNode;
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { Form } from './Form';
|
|
5
|
+
|
|
6
|
+
// Mock ra-core
|
|
7
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
8
|
+
|
|
9
|
+
// Mock Cloudscape components
|
|
10
|
+
vi.mock('@cloudscape-design/components/form', () => ({
|
|
11
|
+
default: ({ children, actions }: any) => (
|
|
12
|
+
<div data-testid="cloudscape-form">
|
|
13
|
+
<div data-testid="form-content">{children}</div>
|
|
14
|
+
<div data-testid="form-actions">{actions}</div>
|
|
15
|
+
</div>
|
|
16
|
+
),
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
vi.mock('@cloudscape-design/components/space-between', () => ({
|
|
20
|
+
default: ({ children }: any) => <div>{children}</div>,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('@cloudscape-design/components/button', () => ({
|
|
24
|
+
default: ({ children, type }: any) => <button type={type}>{children}</button>,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('Form', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.clearAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should render children and default toolbar', () => {
|
|
33
|
+
const { getByTestId, getByText } = render(
|
|
34
|
+
<Form>
|
|
35
|
+
<div data-testid="child" />
|
|
36
|
+
</Form>,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(getByTestId('ra-form')).toBeDefined();
|
|
40
|
+
expect(getByTestId('cloudscape-form')).toBeDefined();
|
|
41
|
+
expect(getByTestId('child')).toBeDefined();
|
|
42
|
+
expect(getByText('Save')).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should render custom toolbar', () => {
|
|
46
|
+
const { getByText, queryByText } = render(
|
|
47
|
+
<Form toolbar={<button>Custom Save</button>}>
|
|
48
|
+
<div />
|
|
49
|
+
</Form>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(getByText('Custom Save')).toBeDefined();
|
|
53
|
+
expect(queryByText('Save')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Form as RaForm, type FormProps as RaFormProps, useSaveContext, useInputSchema } from '@strato-admin/core';
|
|
3
|
+
import CloudscapeForm from '@cloudscape-design/components/form';
|
|
4
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
5
|
+
import { SaveButton } from '../button/SaveButton';
|
|
6
|
+
import { FormField } from '../input/FormField';
|
|
7
|
+
|
|
8
|
+
export interface FormProps extends Omit<RaFormProps, 'children'> {
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
include?: string[];
|
|
11
|
+
exclude?: string[];
|
|
12
|
+
toolbar?: React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Form = ({ children, include, exclude, toolbar, ...props }: FormProps) => {
|
|
16
|
+
const saveContext = useSaveContext();
|
|
17
|
+
const schemaChildren = useInputSchema();
|
|
18
|
+
|
|
19
|
+
const finalChildren = React.useMemo(() => {
|
|
20
|
+
const baseChildren = children || schemaChildren;
|
|
21
|
+
let result = React.Children.toArray(baseChildren);
|
|
22
|
+
|
|
23
|
+
if (include) {
|
|
24
|
+
result = result.filter(
|
|
25
|
+
(child) => React.isValidElement(child) && include.includes((child.props as any).source)
|
|
26
|
+
);
|
|
27
|
+
} else if (exclude) {
|
|
28
|
+
result = result.filter(
|
|
29
|
+
(child) => React.isValidElement(child) && !exclude.includes((child.props as any).source)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result;
|
|
34
|
+
}, [children, schemaChildren, include, exclude]);
|
|
35
|
+
|
|
36
|
+
const handleSubmit = async (values: any, event: any) => {
|
|
37
|
+
if (props.onSubmit) {
|
|
38
|
+
return props.onSubmit(values, event);
|
|
39
|
+
}
|
|
40
|
+
if (saveContext?.save) {
|
|
41
|
+
return saveContext.save(values, event);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<RaForm {...props} onSubmit={handleSubmit}>
|
|
47
|
+
<CloudscapeForm
|
|
48
|
+
actions={
|
|
49
|
+
toolbar || (
|
|
50
|
+
<SpaceBetween direction="horizontal" size="xs">
|
|
51
|
+
<SaveButton />
|
|
52
|
+
</SpaceBetween>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
>
|
|
56
|
+
<SpaceBetween direction="vertical" size="l">
|
|
57
|
+
{finalChildren}
|
|
58
|
+
</SpaceBetween>
|
|
59
|
+
</CloudscapeForm>
|
|
60
|
+
</RaForm>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
Form.Field = FormField;
|
|
65
|
+
|
|
66
|
+
export default Form;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export * from '@strato-admin/core';
|
|
2
|
+
|
|
3
|
+
export * from './collection-hooks';
|
|
4
|
+
export * from './list';
|
|
5
|
+
export * from './detail';
|
|
6
|
+
export * from './edit';
|
|
7
|
+
export * from './create';
|
|
8
|
+
export * from './field';
|
|
9
|
+
export * from './input';
|
|
10
|
+
export * from './form';
|
|
11
|
+
export * from './layout';
|
|
12
|
+
export * from './theme';
|
|
13
|
+
export * from './button';
|
|
14
|
+
export * from './Admin';
|
|
15
|
+
export { default as RecordLink } from './RecordLink';
|
|
16
|
+
export * from './RecordLink';
|
|
17
|
+
|
|
18
|
+
// Explicitly export themed components to resolve ambiguity with strato-core (ra-core) re-exports
|
|
19
|
+
export { Admin, type AdminProps } from './Admin';
|
|
20
|
+
export { Form, type FormProps } from './form';
|
|
21
|
+
export { List, type ListProps } from './list';
|
|
22
|
+
export { Create, type CreateProps } from './create';
|
|
23
|
+
export { Edit, type EditProps } from './edit';
|
|
24
|
+
export { Show, type ShowProps } from './detail';
|
|
25
|
+
export { type InputProps } from './input';
|