@strato-admin/cloudscape 0.1.0 → 0.3.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/dist/Admin.d.ts +6 -2
- package/dist/Admin.js +14 -8
- package/dist/RecordLink.js +5 -4
- package/dist/Settings.d.ts +17 -0
- package/dist/Settings.js +14 -0
- package/dist/button/BulkDeleteButton.d.ts +4 -1
- package/dist/button/BulkDeleteButton.js +37 -5
- package/dist/button/Button.d.ts +2 -1
- package/dist/button/CancelButton.d.ts +6 -0
- package/dist/button/CancelButton.js +10 -0
- package/dist/button/CreateButton.js +9 -8
- package/dist/button/DeleteButton.d.ts +13 -0
- package/dist/button/DeleteButton.js +36 -0
- package/dist/button/EditButton.d.ts +1 -1
- package/dist/button/EditButton.js +10 -10
- package/dist/button/SaveButton.js +2 -2
- package/dist/button/index.d.ts +2 -0
- package/dist/button/index.js +2 -0
- package/dist/collection-hooks/interfaces.d.ts +7 -3
- package/dist/collection-hooks/useCollection.d.ts +1 -1
- package/dist/collection-hooks/useCollection.js +15 -10
- package/dist/create/Create.d.ts +9 -17
- package/dist/create/Create.js +40 -12
- package/dist/create/CreateHeader.d.ts +2 -2
- package/dist/create/CreateHeader.js +4 -5
- package/dist/defaults.d.ts +6 -0
- package/dist/defaults.js +21 -0
- package/dist/detail/Detail.d.ts +33 -0
- package/dist/detail/Detail.js +22 -0
- package/dist/detail/DetailHeader.d.ts +11 -0
- package/dist/detail/{ShowHeader.js → DetailHeader.js} +7 -5
- package/dist/detail/DetailHub.d.ts +27 -0
- package/dist/detail/DetailHub.js +63 -0
- package/dist/detail/KeyValuePairs.d.ts +7 -1
- package/dist/detail/KeyValuePairs.js +14 -8
- package/dist/detail/index.d.ts +3 -2
- package/dist/detail/index.js +3 -2
- package/dist/edit/Edit.d.ts +8 -19
- package/dist/edit/Edit.js +48 -12
- package/dist/edit/EditHeader.d.ts +2 -2
- package/dist/edit/EditHeader.js +5 -4
- package/dist/field/ArrayField.d.ts +26 -10
- package/dist/field/ArrayField.js +38 -10
- package/dist/field/BadgeField.d.ts +1 -1
- package/dist/field/BadgeField.js +1 -1
- package/dist/field/BooleanField.d.ts +1 -1
- package/dist/field/BooleanField.js +2 -2
- package/dist/field/CurrencyField.d.ts +1 -1
- package/dist/field/CurrencyField.js +1 -1
- package/dist/field/DateField.d.ts +1 -1
- package/dist/field/DateField.js +1 -1
- package/dist/field/IdField.d.ts +1 -1
- package/dist/field/IdField.js +3 -3
- package/dist/field/NumberField.d.ts +1 -1
- package/dist/field/NumberField.js +1 -1
- package/dist/field/ReferenceField.d.ts +1 -1
- package/dist/field/ReferenceField.js +4 -2
- package/dist/field/ReferenceManyField.d.ts +35 -4
- package/dist/field/ReferenceManyField.js +17 -4
- package/dist/field/StatusIndicatorField.d.ts +1 -1
- package/dist/field/StatusIndicatorField.js +6 -5
- package/dist/field/TextField.d.ts +1 -1
- package/dist/field/TextField.js +1 -1
- package/dist/field/types.d.ts +9 -9
- package/dist/form/Form.d.ts +12 -2
- package/dist/form/Form.js +10 -16
- package/dist/form/index.d.ts +1 -1
- package/dist/form/index.js +1 -1
- package/dist/hooks/useSchemaFields.d.ts +22 -0
- package/dist/hooks/useSchemaFields.js +45 -0
- package/dist/i18n/Message.d.ts +15 -0
- package/dist/i18n/Message.js +19 -0
- package/dist/i18n/RecordMessage.d.ts +14 -0
- package/dist/i18n/RecordMessage.js +16 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/types.d.ts +19 -0
- package/dist/i18n/types.js +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/input/ArrayInput.d.ts +33 -0
- package/dist/input/{AttributeEditor.js → ArrayInput.js} +18 -11
- package/dist/input/AutocompleteInput.d.ts +1 -1
- package/dist/input/AutocompleteInput.js +3 -3
- package/dist/input/BooleanInput.d.ts +6 -0
- package/dist/input/BooleanInput.js +23 -0
- package/dist/input/CommonInputProps.d.ts +6 -0
- package/dist/input/CommonInputProps.js +6 -0
- package/dist/input/FieldTitle.js +4 -4
- package/dist/input/FormField.js +12 -3
- package/dist/input/FormFieldContext.d.ts +1 -1
- package/dist/input/NumberInput.d.ts +1 -1
- package/dist/input/NumberInput.js +3 -3
- package/dist/input/ReferenceInput.d.ts +1 -1
- package/dist/input/ReferenceInput.js +22 -12
- package/dist/input/SelectInput.d.ts +1 -1
- package/dist/input/SelectInput.js +3 -3
- package/dist/input/SliderInput.d.ts +1 -1
- package/dist/input/SliderInput.js +4 -4
- package/dist/input/TextAreaInput.d.ts +1 -1
- package/dist/input/TextAreaInput.js +3 -3
- package/dist/input/TextInput.d.ts +1 -1
- package/dist/input/TextInput.js +6 -12
- package/dist/input/index.d.ts +2 -1
- package/dist/input/index.js +2 -1
- package/dist/input/types.d.ts +33 -2
- package/dist/layout/AppLayout.js +6 -3
- package/dist/layout/Notifications.d.ts +1 -0
- package/dist/layout/Notifications.js +51 -0
- package/dist/layout/Ready.d.ts +6 -0
- package/dist/layout/Ready.js +24 -0
- package/dist/layout/TopNavigation.d.ts +4 -2
- package/dist/layout/TopNavigation.js +7 -7
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +2 -0
- package/dist/list/Cards.d.ts +31 -4
- package/dist/list/Cards.js +81 -10
- package/dist/list/List.d.ts +9 -12
- package/dist/list/List.js +41 -11
- package/dist/list/Table.d.ts +8 -4
- package/dist/list/Table.js +55 -55
- package/dist/list/TableHeader.d.ts +2 -2
- package/dist/list/TableHeader.js +4 -5
- package/dist/theme/ThemeManager.js +1 -1
- package/package.json +9 -6
- package/src/Admin.tsx +35 -18
- package/src/RecordLink.stories.tsx +1 -1
- package/src/RecordLink.tsx +5 -4
- package/src/Settings.tsx +16 -0
- package/src/__mocks__/ra-core.tsx +83 -0
- package/src/__mocks__/strato-core.tsx +36 -42
- package/src/button/BulkDeleteButton.test.tsx +45 -8
- package/src/button/BulkDeleteButton.tsx +75 -12
- package/src/button/Button.tsx +31 -2
- package/src/button/CancelButton.tsx +20 -0
- package/src/button/CreateButton.tsx +12 -10
- package/src/button/DeleteButton.tsx +96 -0
- package/src/button/EditButton.tsx +13 -12
- package/src/button/SaveButton.tsx +2 -3
- package/src/button/index.ts +2 -0
- package/src/collection-hooks/interfaces.ts +7 -3
- package/src/collection-hooks/useCollection.test.ts +115 -2
- package/src/collection-hooks/useCollection.ts +15 -10
- package/src/create/Create.test.tsx +3 -3
- package/src/create/Create.tsx +68 -37
- package/src/create/CreateHeader.tsx +6 -10
- package/src/defaults.tsx +28 -0
- package/src/detail/Detail-CollectionFields.test.tsx +84 -0
- package/src/detail/Detail.test.tsx +91 -0
- package/src/detail/Detail.tsx +48 -0
- package/src/detail/{ShowHeader.test.tsx → DetailHeader.test.tsx} +11 -9
- package/src/detail/DetailHeader.tsx +42 -0
- package/src/detail/DetailHub.tsx +88 -0
- package/src/detail/KeyValuePairs.test.tsx +2 -2
- package/src/detail/KeyValuePairs.tsx +25 -18
- package/src/detail/index.ts +3 -2
- package/src/edit/Edit.test.tsx +7 -5
- package/src/edit/Edit.tsx +92 -40
- package/src/edit/EditHeader.tsx +7 -5
- package/src/field/ArrayField.tsx +57 -11
- package/src/field/BadgeField.tsx +2 -3
- package/src/field/BooleanField.test.tsx +2 -3
- package/src/field/BooleanField.tsx +3 -3
- package/src/field/CurrencyField.tsx +1 -1
- package/src/field/DateField.tsx +1 -1
- package/src/field/IdField.test.tsx +8 -20
- package/src/field/IdField.tsx +5 -20
- package/src/field/NumberField.tsx +1 -1
- package/src/field/ReferenceField.test.tsx +15 -6
- package/src/field/ReferenceField.tsx +10 -7
- package/src/field/ReferenceManyField.test.tsx +55 -10
- package/src/field/ReferenceManyField.tsx +84 -13
- package/src/field/StatusIndicatorField.test.tsx +7 -21
- package/src/field/StatusIndicatorField.tsx +8 -20
- package/src/field/TextField.tsx +1 -1
- package/src/field/types.ts +12 -13
- package/src/form/Form.test.tsx +8 -4
- package/src/form/Form.tsx +24 -19
- package/src/form/index.ts +1 -1
- package/src/hooks/useSchemaFields.ts +89 -0
- package/src/i18n/Message.tsx +22 -0
- package/src/i18n/RecordMessage.tsx +22 -0
- package/src/i18n/index.ts +3 -0
- package/src/i18n/types.ts +19 -0
- package/src/index.ts +5 -1
- package/src/input/ArrayInput.test.tsx +81 -0
- package/src/input/{AttributeEditor.tsx → ArrayInput.tsx} +36 -18
- package/src/input/AutocompleteInput.test.tsx +2 -4
- package/src/input/AutocompleteInput.tsx +9 -11
- package/src/input/BooleanInput.tsx +42 -0
- package/src/input/CommonInputProps.tsx +8 -0
- package/src/input/FieldTitle.tsx +3 -15
- package/src/input/FormField.tsx +78 -67
- package/src/input/FormFieldContext.ts +1 -1
- package/src/input/NumberInput.tsx +10 -7
- package/src/input/ReferenceInput.test.tsx +12 -2
- package/src/input/ReferenceInput.tsx +32 -14
- package/src/input/SelectInput.tsx +14 -17
- package/src/input/SliderInput.test.tsx +2 -3
- package/src/input/SliderInput.tsx +48 -38
- package/src/input/TextAreaInput.tsx +10 -6
- package/src/input/TextInput.test.tsx +2 -4
- package/src/input/TextInput.tsx +35 -20
- package/src/input/index.ts +2 -1
- package/src/input/types.ts +40 -8
- package/src/layout/AppLayout.test.tsx +23 -3
- package/src/layout/AppLayout.tsx +11 -8
- package/src/layout/Notifications.test.tsx +102 -0
- package/src/layout/Notifications.tsx +61 -0
- package/src/layout/Ready.tsx +123 -0
- package/src/layout/TopNavigation.test.tsx +2 -3
- package/src/layout/TopNavigation.tsx +9 -8
- package/src/layout/index.ts +2 -0
- package/src/list/Cards.test.tsx +320 -0
- package/src/list/Cards.tsx +146 -16
- package/src/list/List.tsx +87 -26
- package/src/list/Table.test.tsx +40 -5
- package/src/list/Table.tsx +89 -98
- package/src/list/TableHeader.test.tsx +15 -11
- package/src/list/TableHeader.tsx +6 -8
- package/src/theme/ThemeManager.tsx +1 -1
- package/dist/__mocks__/strato-core.js +0 -50
- package/dist/__mocks__to__delete/strato-core.js +0 -50
- package/dist/detail/Show.d.ts +0 -39
- package/dist/detail/Show.js +0 -40
- package/dist/detail/ShowHeader.d.ts +0 -7
- package/dist/input/AttributeEditor.d.ts +0 -25
- package/src/detail/Show.test.tsx +0 -96
- package/src/detail/Show.tsx +0 -104
- package/src/detail/ShowHeader.tsx +0 -35
- package/src/input/AttributeEditor.test.tsx +0 -147
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import { useShowController, useResourceContext, useShowContext } from '@strato-admin/ra-core';
|
|
5
|
+
import { useResourceSchema } from '@strato-admin/core';
|
|
6
|
+
import { Detail } from './Detail';
|
|
7
|
+
|
|
8
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
9
|
+
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
10
|
+
|
|
11
|
+
// Mock Cloudscape components
|
|
12
|
+
vi.mock('@cloudscape-design/components/container', () => ({
|
|
13
|
+
default: ({ children, header }: any) => (
|
|
14
|
+
<div data-testid="container">
|
|
15
|
+
{header && <div data-testid="container-header">{header}</div>}
|
|
16
|
+
<div data-testid="container-content">{children}</div>
|
|
17
|
+
</div>
|
|
18
|
+
),
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
vi.mock('@cloudscape-design/components/header', () => ({
|
|
22
|
+
default: ({ children }: any) => <header>{children}</header>,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock('@cloudscape-design/components/space-between', () => ({
|
|
26
|
+
default: ({ children }: any) => <div data-testid="space-between">{children}</div>,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
describe('Detail Collection Fields', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
33
|
+
const controllerProps = {
|
|
34
|
+
record: { id: 1, name: 'Test Product' },
|
|
35
|
+
isLoading: false,
|
|
36
|
+
resource: 'products',
|
|
37
|
+
defaultTitle: 'Products',
|
|
38
|
+
};
|
|
39
|
+
(useShowController as any).mockReturnValue(controllerProps);
|
|
40
|
+
(useShowContext as any).mockReturnValue(controllerProps);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
cleanup();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should render collection fields after scalar fields when no children are provided', () => {
|
|
48
|
+
const ScalarField = (props: any) => <div data-testid={`scalar-${props.source}`} />;
|
|
49
|
+
const CollectionField = (props: any) => <div data-testid={`collection-${props.source}`} />;
|
|
50
|
+
(CollectionField as any).isCollectionField = true;
|
|
51
|
+
|
|
52
|
+
(useResourceSchema as any).mockReturnValue({
|
|
53
|
+
label: 'Products',
|
|
54
|
+
fieldSchema: [<ScalarField key="scalar" source="name" />, <CollectionField key="collection" source="items" />],
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const { getByTestId, queryByTestId } = render(<Detail />);
|
|
58
|
+
|
|
59
|
+
// Scalar fields should be inside a container (handled by DetailHub -> KeyValuePairs)
|
|
60
|
+
// In this test, because we don't mock KeyValuePairs, DetailHub renders <KeyValuePairs />
|
|
61
|
+
// which in our mock is just the component itself or handled by schema.
|
|
62
|
+
|
|
63
|
+
// Collection field should be rendered
|
|
64
|
+
expect(getByTestId('collection-items')).toBeDefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should respect detailInclude for collection fields', () => {
|
|
68
|
+
const CollectionField1 = (props: any) => <div data-testid={`collection-${props.source}`} />;
|
|
69
|
+
(CollectionField1 as any).isCollectionField = true;
|
|
70
|
+
const CollectionField2 = (props: any) => <div data-testid={`collection-${props.source}`} />;
|
|
71
|
+
(CollectionField2 as any).isCollectionField = true;
|
|
72
|
+
|
|
73
|
+
(useResourceSchema as any).mockReturnValue({
|
|
74
|
+
label: 'Products',
|
|
75
|
+
fieldSchema: [<CollectionField1 key="c1" source="items1" />, <CollectionField2 key="c2" source="items2" />],
|
|
76
|
+
detailInclude: ['items1'],
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const { getByTestId, queryByTestId } = render(<Detail />);
|
|
80
|
+
|
|
81
|
+
expect(getByTestId('collection-items1')).toBeDefined();
|
|
82
|
+
expect(queryByTestId('collection-items2')).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import { useShowController, useResourceContext, useShowContext } from '@strato-admin/ra-core';
|
|
5
|
+
import { Detail } from './Detail';
|
|
6
|
+
|
|
7
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/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
|
+
{header && <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 }: any) => <header>{children}</header>,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
vi.mock('@cloudscape-design/components/space-between', () => ({
|
|
25
|
+
default: ({ children }: any) => <div>{children}</div>,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
// Mock KeyValuePairs
|
|
29
|
+
vi.mock('./KeyValuePairs', () => ({
|
|
30
|
+
default: ({ children }: any) => <div>{children}</div>,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
describe('Detail', () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
vi.clearAllMocks();
|
|
36
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
37
|
+
const controllerProps = {
|
|
38
|
+
record: { id: 1, name: 'Test Product' },
|
|
39
|
+
isLoading: false,
|
|
40
|
+
resource: 'products',
|
|
41
|
+
defaultTitle: 'Products',
|
|
42
|
+
};
|
|
43
|
+
(useShowController as any).mockReturnValue(controllerProps);
|
|
44
|
+
(useShowContext as any).mockReturnValue(controllerProps);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
cleanup();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should render nothing when loading', () => {
|
|
52
|
+
const controllerProps = {
|
|
53
|
+
isLoading: true,
|
|
54
|
+
record: undefined,
|
|
55
|
+
resource: 'products',
|
|
56
|
+
defaultTitle: 'Products',
|
|
57
|
+
};
|
|
58
|
+
(useShowController as any).mockReturnValue(controllerProps);
|
|
59
|
+
(useShowContext as any).mockReturnValue(controllerProps);
|
|
60
|
+
|
|
61
|
+
const { queryByTestId } = render(
|
|
62
|
+
<Detail>
|
|
63
|
+
<div />
|
|
64
|
+
</Detail>,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
expect(queryByTestId('container')).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should render content and title when record is loaded', () => {
|
|
71
|
+
const { getByTestId, getByText } = render(
|
|
72
|
+
<Detail>
|
|
73
|
+
<div data-testid="content">Product Content</div>
|
|
74
|
+
</Detail>,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(getByTestId('container')).toBeDefined();
|
|
78
|
+
expect(getByText('Product Content')).toBeDefined();
|
|
79
|
+
expect(getByText(/Products/)).toBeDefined();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should use provided title', () => {
|
|
83
|
+
const { getByText } = render(
|
|
84
|
+
<Detail title="Custom Title">
|
|
85
|
+
<div />
|
|
86
|
+
</Detail>,
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(getByText('Custom Title')).toBeDefined();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { ShowBaseProps, ShowContextProvider, useShowController } from '@strato-admin/ra-core';
|
|
3
|
+
import { useResourceSchema } from '@strato-admin/core';
|
|
4
|
+
import DetailHub from './DetailHub';
|
|
5
|
+
|
|
6
|
+
export interface DetailProps extends ShowBaseProps {
|
|
7
|
+
/**
|
|
8
|
+
* The title of the detail view.
|
|
9
|
+
*/
|
|
10
|
+
title?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The description of the detail view.
|
|
13
|
+
*/
|
|
14
|
+
description?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Custom fields or components to display in the detail view.
|
|
17
|
+
*/
|
|
18
|
+
children?: React.ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Custom actions to display in the header.
|
|
21
|
+
*/
|
|
22
|
+
actions?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A detail view component that displays a single record's details.
|
|
27
|
+
* It uses the detailComponent defined in the schema (defaults to DetailHub)
|
|
28
|
+
* to organize fields into sections.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* <Detail>
|
|
32
|
+
* <TextField source="name" />
|
|
33
|
+
* <TextField source="description" />
|
|
34
|
+
* </Detail>
|
|
35
|
+
*/
|
|
36
|
+
export const Detail = (props: DetailProps) => {
|
|
37
|
+
const { children, ...rest } = props;
|
|
38
|
+
const { queryOptions, detailComponent: DetailComponent = DetailHub } = useResourceSchema(props.resource);
|
|
39
|
+
const controllerProps = useShowController({ queryOptions, ...rest });
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<ShowContextProvider value={controllerProps}>
|
|
43
|
+
<DetailComponent {...props} />
|
|
44
|
+
</ShowContextProvider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default Detail;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
-
import { useResourceContext, useShowContext } from '@strato-admin/core';
|
|
5
|
-
import {
|
|
4
|
+
import { useResourceContext, useShowContext } from '@strato-admin/ra-core';
|
|
5
|
+
import { DetailHeader } from './DetailHeader';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
8
8
|
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
9
|
|
|
10
10
|
// Mock react-router-dom
|
|
@@ -34,7 +34,7 @@ vi.mock('@cloudscape-design/components/button', () => ({
|
|
|
34
34
|
),
|
|
35
35
|
}));
|
|
36
36
|
|
|
37
|
-
describe('
|
|
37
|
+
describe('DetailHeader', () => {
|
|
38
38
|
beforeEach(() => {
|
|
39
39
|
vi.clearAllMocks();
|
|
40
40
|
});
|
|
@@ -43,7 +43,7 @@ describe('ShowHeader', () => {
|
|
|
43
43
|
(useResourceContext as any).mockReturnValue('products');
|
|
44
44
|
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
45
45
|
|
|
46
|
-
const { getByTestId } = render(<
|
|
46
|
+
const { getByTestId } = render(<DetailHeader />);
|
|
47
47
|
|
|
48
48
|
expect(getByTestId('header-title').textContent).toBe('Products');
|
|
49
49
|
});
|
|
@@ -52,7 +52,7 @@ describe('ShowHeader', () => {
|
|
|
52
52
|
(useResourceContext as any).mockReturnValue('products');
|
|
53
53
|
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
54
54
|
|
|
55
|
-
const { getByTestId } = render(<
|
|
55
|
+
const { getByTestId } = render(<DetailHeader title="My Product" />);
|
|
56
56
|
|
|
57
57
|
expect(getByTestId('header-title').textContent).toBe('My Product');
|
|
58
58
|
});
|
|
@@ -61,7 +61,7 @@ describe('ShowHeader', () => {
|
|
|
61
61
|
(useResourceContext as any).mockReturnValue('products');
|
|
62
62
|
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
63
63
|
|
|
64
|
-
const { getByText } = render(<
|
|
64
|
+
const { getByText } = render(<DetailHeader />);
|
|
65
65
|
|
|
66
66
|
const editButton = getByText('Edit');
|
|
67
67
|
expect(editButton).toBeDefined();
|
|
@@ -72,9 +72,11 @@ describe('ShowHeader', () => {
|
|
|
72
72
|
(useResourceContext as any).mockReturnValue('products');
|
|
73
73
|
(useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
|
|
74
74
|
|
|
75
|
-
const { getByTestId, queryByText } = render(
|
|
75
|
+
const { getByTestId, queryByText } = render(
|
|
76
|
+
<DetailHeader actions={<div data-testid="custom-action">Custom</div>} />,
|
|
77
|
+
);
|
|
76
78
|
|
|
77
79
|
expect(getByTestId('custom-action')).toBeDefined();
|
|
78
|
-
expect(queryByText('
|
|
80
|
+
expect(queryByText('strato.action.edit')).toBeNull();
|
|
79
81
|
});
|
|
80
82
|
});
|
|
@@ -0,0 +1,42 @@
|
|
|
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 { useShowContext, useTranslate } from '@strato-admin/ra-core';
|
|
5
|
+
import { EditButton } from '../button/EditButton';
|
|
6
|
+
|
|
7
|
+
export interface DetailHeaderProps
|
|
8
|
+
extends Pick<HeaderProps, 'variant' | 'counter' | 'actions' | 'description' | 'info' | 'headingTagOverride'> {
|
|
9
|
+
title?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** @deprecated Use DetailHeader instead */
|
|
13
|
+
export type ShowHeaderProps = DetailHeaderProps;
|
|
14
|
+
|
|
15
|
+
/** @deprecated Use DetailHeader instead */
|
|
16
|
+
export const ShowHeader = (props: DetailHeaderProps) => <DetailHeader {...props} />;
|
|
17
|
+
|
|
18
|
+
export const DetailHeader = ({ title, actions, description, counter, info, variant = 'h2', headingTagOverride }: DetailHeaderProps) => {
|
|
19
|
+
const translate = useTranslate();
|
|
20
|
+
const { record, defaultTitle } = useShowContext();
|
|
21
|
+
|
|
22
|
+
const headerTitle = React.useMemo(() => {
|
|
23
|
+
if (title !== undefined) {
|
|
24
|
+
return typeof title === 'string' ? translate(title, { _: title }) : title;
|
|
25
|
+
}
|
|
26
|
+
return defaultTitle;
|
|
27
|
+
}, [title, defaultTitle, translate]);
|
|
28
|
+
|
|
29
|
+
const headerActions = actions || (
|
|
30
|
+
<SpaceBetween direction="horizontal" size="xs">
|
|
31
|
+
<EditButton record={record} variant="primary" />
|
|
32
|
+
</SpaceBetween>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Header variant={variant} actions={headerActions} description={description} counter={counter} info={info} headingTagOverride={headingTagOverride}>
|
|
37
|
+
{headerTitle}
|
|
38
|
+
</Header>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default DetailHeader;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import React, { useMemo, ReactNode, isValidElement } from 'react';
|
|
2
|
+
import Container from '@cloudscape-design/components/container';
|
|
3
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
4
|
+
import { useTranslate, useShowContext, RaRecord } from '@strato-admin/ra-core';
|
|
5
|
+
import { useResourceSchema } from '@strato-admin/core';
|
|
6
|
+
import { useSchemaFields } from '../hooks/useSchemaFields';
|
|
7
|
+
import KeyValuePairs from './KeyValuePairs';
|
|
8
|
+
import DetailHeader from './DetailHeader';
|
|
9
|
+
|
|
10
|
+
export interface DetailHubProps {
|
|
11
|
+
title?: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
actions?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A detail view component that organizes fields into sections based on their type.
|
|
19
|
+
* Scalar fields are grouped into a Container with KeyValuePairs, while
|
|
20
|
+
* collection fields (like ReferenceManyField) are displayed as separate sections
|
|
21
|
+
* below the main record details.
|
|
22
|
+
*
|
|
23
|
+
* It also renders the DetailHeader with the record title and actions (like Edit).
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <DetailHub />
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* <DetailHub title="Custom Title">
|
|
30
|
+
* <KeyValuePairs columns={3} />
|
|
31
|
+
* <ReferenceManyField reference="comments" target="post_id" />
|
|
32
|
+
* </DetailHub>
|
|
33
|
+
*/
|
|
34
|
+
export const DetailHub = <RecordType extends RaRecord = RaRecord>(props: DetailHubProps) => {
|
|
35
|
+
const { title, description, children, actions } = props;
|
|
36
|
+
const { record, resource, isLoading } = useShowContext<RecordType>();
|
|
37
|
+
const translate = useTranslate();
|
|
38
|
+
const { label, detailTitle, detailDescription } = useResourceSchema();
|
|
39
|
+
|
|
40
|
+
const { getDetailFields } = useSchemaFields();
|
|
41
|
+
|
|
42
|
+
const constructedTitle = useMemo(
|
|
43
|
+
() => label || translate(`resources.${resource}.name`, { smart_count: 1 }),
|
|
44
|
+
[label, translate, resource],
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const finalTitle = useMemo(() => {
|
|
48
|
+
if (title) return title;
|
|
49
|
+
if (typeof detailTitle === 'function') return detailTitle(record);
|
|
50
|
+
if (isValidElement(detailTitle)) return detailTitle;
|
|
51
|
+
if (detailTitle) return translate(detailTitle as string, record);
|
|
52
|
+
return constructedTitle;
|
|
53
|
+
}, [record, title, detailTitle, translate, constructedTitle]);
|
|
54
|
+
|
|
55
|
+
const finalDescription = useMemo(() => {
|
|
56
|
+
if (description) return description;
|
|
57
|
+
if (typeof detailDescription === 'function') return detailDescription(record);
|
|
58
|
+
if (isValidElement(detailDescription)) return detailDescription;
|
|
59
|
+
if (detailDescription) return translate(detailDescription as string, record);
|
|
60
|
+
return undefined;
|
|
61
|
+
}, [record, description, detailDescription, translate]);
|
|
62
|
+
|
|
63
|
+
const { scalarFields, collectionFields } = useMemo(() => getDetailFields(children), [getDetailFields, children]);
|
|
64
|
+
|
|
65
|
+
if (isLoading || !record) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const hasScalarFields = scalarFields.length > 0;
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<SpaceBetween size="l">
|
|
73
|
+
<DetailHeader title={finalTitle} description={finalDescription} actions={actions} />
|
|
74
|
+
{hasScalarFields && (
|
|
75
|
+
<Container>
|
|
76
|
+
{React.Children.count(children) > 0 ? (
|
|
77
|
+
<SpaceBetween size="l">{scalarFields}</SpaceBetween>
|
|
78
|
+
) : (
|
|
79
|
+
<KeyValuePairs />
|
|
80
|
+
)}
|
|
81
|
+
</Container>
|
|
82
|
+
)}
|
|
83
|
+
{collectionFields}
|
|
84
|
+
</SpaceBetween>
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export default DetailHub;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
-
import { useResourceContext, useTranslate, useRecordContext } from '@strato-admin/core';
|
|
4
|
+
import { useResourceContext, useTranslate, useRecordContext } from '@strato-admin/ra-core';
|
|
5
5
|
import KeyValuePairs from './KeyValuePairs';
|
|
6
6
|
import CloudscapeKeyValuePairs from '@cloudscape-design/components/key-value-pairs';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
9
9
|
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
10
10
|
|
|
11
11
|
// Mock react-router-dom
|
|
@@ -2,19 +2,19 @@ import React from 'react';
|
|
|
2
2
|
import CloudscapeKeyValuePairs, {
|
|
3
3
|
type KeyValuePairsProps as CloudscapeKeyValuePairsProps,
|
|
4
4
|
} from '@cloudscape-design/components/key-value-pairs';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
useRecordContext,
|
|
8
|
-
FieldTitle,
|
|
9
|
-
RecordContextProvider,
|
|
10
|
-
type RaRecord,
|
|
11
|
-
useFieldSchema,
|
|
12
|
-
} from '@strato-admin/core';
|
|
5
|
+
import { useRecordContext, FieldTitle, RecordContextProvider, type RaRecord } from '@strato-admin/ra-core';
|
|
6
|
+
import { useResourceSchema } from '@strato-admin/core';
|
|
13
7
|
import TextField from '../field/TextField';
|
|
14
8
|
|
|
15
9
|
export interface KeyValuePairsProps extends Partial<Omit<CloudscapeKeyValuePairsProps, 'items'>> {
|
|
16
10
|
children?: React.ReactNode;
|
|
11
|
+
/**
|
|
12
|
+
* List of field sources to include in the key-value pairs list.
|
|
13
|
+
*/
|
|
17
14
|
include?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* List of field sources to exclude from the key-value pairs list.
|
|
17
|
+
*/
|
|
18
18
|
exclude?: string[];
|
|
19
19
|
columns?: number;
|
|
20
20
|
minColumnWidth?: number;
|
|
@@ -36,7 +36,7 @@ export const KeyValueField = ({ children, source, field: FieldComponent }: KeyVa
|
|
|
36
36
|
return (
|
|
37
37
|
<>
|
|
38
38
|
{React.Children.map(children, (child) =>
|
|
39
|
-
React.isValidElement(child) ? React.cloneElement(child, { source } as any) : child
|
|
39
|
+
React.isValidElement(child) ? React.cloneElement(child, { source } as any) : child,
|
|
40
40
|
)}
|
|
41
41
|
</>
|
|
42
42
|
);
|
|
@@ -65,26 +65,33 @@ export const KeyValuePairs = <RecordType extends RaRecord = RaRecord>({
|
|
|
65
65
|
minColumnWidth,
|
|
66
66
|
...props
|
|
67
67
|
}: KeyValuePairsProps) => {
|
|
68
|
-
const resource = useResourceContext();
|
|
69
68
|
const record = useRecordContext<RecordType>();
|
|
70
|
-
const schemaChildren =
|
|
69
|
+
const { resource, fieldSchema: schemaChildren, detailInclude, detailExclude } = useResourceSchema();
|
|
71
70
|
|
|
72
71
|
const finalChildren = React.useMemo(() => {
|
|
73
72
|
const baseChildren = children || schemaChildren;
|
|
74
73
|
let result = React.Children.toArray(baseChildren);
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
} else if (exclude) {
|
|
75
|
+
const finalInclude = include || detailInclude;
|
|
76
|
+
const finalExclude = exclude || detailExclude;
|
|
77
|
+
|
|
78
|
+
if (finalInclude) {
|
|
81
79
|
result = result.filter(
|
|
82
|
-
(child) => React.isValidElement(child) &&
|
|
80
|
+
(child) => React.isValidElement(child) && finalInclude.includes((child.props as any).source),
|
|
83
81
|
);
|
|
82
|
+
} else {
|
|
83
|
+
// Filter out fields marked as collection fields by default
|
|
84
|
+
result = result.filter((child) => React.isValidElement(child) && !(child.type as any).isCollectionField);
|
|
85
|
+
|
|
86
|
+
if (finalExclude) {
|
|
87
|
+
result = result.filter(
|
|
88
|
+
(child) => React.isValidElement(child) && !finalExclude.includes((child.props as any).source),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
84
91
|
}
|
|
85
92
|
|
|
86
93
|
return result;
|
|
87
|
-
}, [children, schemaChildren, include, exclude]);
|
|
94
|
+
}, [children, schemaChildren, include, exclude, detailInclude, detailExclude]);
|
|
88
95
|
|
|
89
96
|
const items =
|
|
90
97
|
React.Children.map(finalChildren, (child) => {
|
package/src/detail/index.ts
CHANGED
package/src/edit/Edit.test.tsx
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
-
import { useEditContext, useResourceContext } from '@strato-admin/core';
|
|
4
|
+
import { useEditContext, useResourceContext } from '@strato-admin/ra-core';
|
|
5
5
|
import { Edit } from './Edit';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
8
8
|
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
9
|
|
|
10
10
|
// Mock Cloudscape components
|
|
@@ -57,7 +57,8 @@ describe('Edit', () => {
|
|
|
57
57
|
it('should render content and title when record is loaded', () => {
|
|
58
58
|
(useEditContext as any).mockReturnValue({
|
|
59
59
|
isLoading: false,
|
|
60
|
-
record: { id: 1 },
|
|
60
|
+
record: { id: 1 },
|
|
61
|
+
defaultTitle: 'Products',
|
|
61
62
|
resource: 'products',
|
|
62
63
|
});
|
|
63
64
|
(useResourceContext as any).mockReturnValue('products');
|
|
@@ -70,13 +71,14 @@ describe('Edit', () => {
|
|
|
70
71
|
|
|
71
72
|
expect(getByTestId('container')).toBeDefined();
|
|
72
73
|
expect(getByTestId('content').textContent).toBe('Hello World');
|
|
73
|
-
expect(getByText(
|
|
74
|
+
expect(getByText(/Products/)).toBeDefined();
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
it('should use provided title', () => {
|
|
77
78
|
(useEditContext as any).mockReturnValue({
|
|
78
79
|
isLoading: false,
|
|
79
|
-
record: { id: 1 },
|
|
80
|
+
record: { id: 1 },
|
|
81
|
+
defaultTitle: 'Products',
|
|
80
82
|
resource: 'products',
|
|
81
83
|
});
|
|
82
84
|
|