@strato-admin/cloudscape 0.1.1 → 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.
Files changed (231) hide show
  1. package/dist/Admin.d.ts +6 -2
  2. package/dist/Admin.js +14 -8
  3. package/dist/RecordLink.js +5 -4
  4. package/dist/Settings.d.ts +17 -0
  5. package/dist/Settings.js +14 -0
  6. package/dist/button/BulkDeleteButton.d.ts +2 -1
  7. package/dist/button/BulkDeleteButton.js +17 -11
  8. package/dist/button/Button.d.ts +2 -1
  9. package/dist/button/CancelButton.d.ts +6 -0
  10. package/dist/button/CancelButton.js +10 -0
  11. package/dist/button/CreateButton.js +9 -8
  12. package/dist/button/DeleteButton.d.ts +13 -0
  13. package/dist/button/DeleteButton.js +36 -0
  14. package/dist/button/EditButton.d.ts +1 -1
  15. package/dist/button/EditButton.js +10 -10
  16. package/dist/button/SaveButton.js +2 -2
  17. package/dist/button/index.d.ts +2 -0
  18. package/dist/button/index.js +2 -0
  19. package/dist/collection-hooks/interfaces.d.ts +7 -3
  20. package/dist/collection-hooks/useCollection.d.ts +1 -1
  21. package/dist/collection-hooks/useCollection.js +15 -10
  22. package/dist/create/Create.d.ts +9 -17
  23. package/dist/create/Create.js +40 -12
  24. package/dist/create/CreateHeader.d.ts +2 -2
  25. package/dist/create/CreateHeader.js +4 -5
  26. package/dist/defaults.d.ts +6 -0
  27. package/dist/defaults.js +21 -0
  28. package/dist/detail/Detail.d.ts +33 -0
  29. package/dist/detail/Detail.js +22 -0
  30. package/dist/detail/DetailHeader.d.ts +11 -0
  31. package/dist/detail/{ShowHeader.js → DetailHeader.js} +7 -5
  32. package/dist/detail/DetailHub.d.ts +27 -0
  33. package/dist/detail/DetailHub.js +63 -0
  34. package/dist/detail/KeyValuePairs.d.ts +7 -1
  35. package/dist/detail/KeyValuePairs.js +14 -8
  36. package/dist/detail/index.d.ts +3 -2
  37. package/dist/detail/index.js +3 -2
  38. package/dist/edit/Edit.d.ts +8 -19
  39. package/dist/edit/Edit.js +48 -12
  40. package/dist/edit/EditHeader.d.ts +2 -2
  41. package/dist/edit/EditHeader.js +5 -4
  42. package/dist/field/ArrayField.d.ts +26 -10
  43. package/dist/field/ArrayField.js +38 -10
  44. package/dist/field/BadgeField.d.ts +1 -1
  45. package/dist/field/BadgeField.js +1 -1
  46. package/dist/field/BooleanField.d.ts +1 -1
  47. package/dist/field/BooleanField.js +2 -2
  48. package/dist/field/CurrencyField.d.ts +1 -1
  49. package/dist/field/CurrencyField.js +1 -1
  50. package/dist/field/DateField.d.ts +1 -1
  51. package/dist/field/DateField.js +1 -1
  52. package/dist/field/IdField.d.ts +1 -1
  53. package/dist/field/IdField.js +3 -3
  54. package/dist/field/NumberField.d.ts +1 -1
  55. package/dist/field/NumberField.js +1 -1
  56. package/dist/field/ReferenceField.d.ts +1 -1
  57. package/dist/field/ReferenceField.js +4 -2
  58. package/dist/field/ReferenceManyField.d.ts +35 -4
  59. package/dist/field/ReferenceManyField.js +17 -4
  60. package/dist/field/StatusIndicatorField.d.ts +1 -1
  61. package/dist/field/StatusIndicatorField.js +6 -5
  62. package/dist/field/TextField.d.ts +1 -1
  63. package/dist/field/TextField.js +1 -1
  64. package/dist/field/types.d.ts +9 -9
  65. package/dist/form/Form.d.ts +12 -2
  66. package/dist/form/Form.js +10 -16
  67. package/dist/form/index.d.ts +1 -1
  68. package/dist/form/index.js +1 -1
  69. package/dist/hooks/useSchemaFields.d.ts +22 -0
  70. package/dist/hooks/useSchemaFields.js +45 -0
  71. package/dist/i18n/Message.d.ts +15 -0
  72. package/dist/i18n/Message.js +19 -0
  73. package/dist/i18n/RecordMessage.d.ts +14 -0
  74. package/dist/i18n/RecordMessage.js +16 -0
  75. package/dist/i18n/index.d.ts +3 -0
  76. package/dist/i18n/index.js +2 -0
  77. package/dist/i18n/types.d.ts +19 -0
  78. package/dist/i18n/types.js +1 -0
  79. package/dist/index.d.ts +5 -1
  80. package/dist/index.js +5 -1
  81. package/dist/input/ArrayInput.d.ts +33 -0
  82. package/dist/input/{AttributeEditor.js → ArrayInput.js} +18 -11
  83. package/dist/input/AutocompleteInput.d.ts +1 -1
  84. package/dist/input/AutocompleteInput.js +3 -3
  85. package/dist/input/BooleanInput.d.ts +6 -0
  86. package/dist/input/BooleanInput.js +23 -0
  87. package/dist/input/CommonInputProps.d.ts +6 -0
  88. package/dist/input/CommonInputProps.js +6 -0
  89. package/dist/input/FieldTitle.js +4 -4
  90. package/dist/input/FormField.js +12 -3
  91. package/dist/input/FormFieldContext.d.ts +1 -1
  92. package/dist/input/NumberInput.d.ts +1 -1
  93. package/dist/input/NumberInput.js +3 -3
  94. package/dist/input/ReferenceInput.d.ts +1 -1
  95. package/dist/input/ReferenceInput.js +22 -12
  96. package/dist/input/SelectInput.d.ts +1 -1
  97. package/dist/input/SelectInput.js +3 -3
  98. package/dist/input/SliderInput.d.ts +1 -1
  99. package/dist/input/SliderInput.js +4 -4
  100. package/dist/input/TextAreaInput.d.ts +1 -1
  101. package/dist/input/TextAreaInput.js +3 -3
  102. package/dist/input/TextInput.d.ts +1 -1
  103. package/dist/input/TextInput.js +6 -12
  104. package/dist/input/index.d.ts +2 -1
  105. package/dist/input/index.js +2 -1
  106. package/dist/input/types.d.ts +33 -2
  107. package/dist/layout/AppLayout.js +6 -3
  108. package/dist/layout/Notifications.d.ts +1 -0
  109. package/dist/layout/Notifications.js +51 -0
  110. package/dist/layout/Ready.d.ts +6 -0
  111. package/dist/layout/Ready.js +24 -0
  112. package/dist/layout/TopNavigation.d.ts +4 -2
  113. package/dist/layout/TopNavigation.js +7 -7
  114. package/dist/layout/index.d.ts +2 -0
  115. package/dist/layout/index.js +2 -0
  116. package/dist/list/Cards.d.ts +31 -4
  117. package/dist/list/Cards.js +81 -10
  118. package/dist/list/List.d.ts +9 -12
  119. package/dist/list/List.js +41 -11
  120. package/dist/list/Table.d.ts +8 -4
  121. package/dist/list/Table.js +55 -55
  122. package/dist/list/TableHeader.d.ts +2 -2
  123. package/dist/list/TableHeader.js +4 -5
  124. package/dist/theme/ThemeManager.js +1 -1
  125. package/package.json +8 -5
  126. package/src/Admin.tsx +35 -18
  127. package/src/RecordLink.stories.tsx +1 -1
  128. package/src/RecordLink.tsx +5 -4
  129. package/src/Settings.tsx +16 -0
  130. package/src/__mocks__/ra-core.tsx +83 -0
  131. package/src/__mocks__/strato-core.tsx +36 -42
  132. package/src/button/BulkDeleteButton.test.tsx +17 -4
  133. package/src/button/BulkDeleteButton.tsx +24 -29
  134. package/src/button/Button.tsx +31 -2
  135. package/src/button/CancelButton.tsx +20 -0
  136. package/src/button/CreateButton.tsx +12 -10
  137. package/src/button/DeleteButton.tsx +96 -0
  138. package/src/button/EditButton.tsx +13 -12
  139. package/src/button/SaveButton.tsx +2 -3
  140. package/src/button/index.ts +2 -0
  141. package/src/collection-hooks/interfaces.ts +7 -3
  142. package/src/collection-hooks/useCollection.test.ts +115 -2
  143. package/src/collection-hooks/useCollection.ts +15 -10
  144. package/src/create/Create.test.tsx +3 -3
  145. package/src/create/Create.tsx +68 -37
  146. package/src/create/CreateHeader.tsx +6 -10
  147. package/src/defaults.tsx +28 -0
  148. package/src/detail/Detail-CollectionFields.test.tsx +84 -0
  149. package/src/detail/Detail.test.tsx +91 -0
  150. package/src/detail/Detail.tsx +48 -0
  151. package/src/detail/{ShowHeader.test.tsx → DetailHeader.test.tsx} +11 -9
  152. package/src/detail/DetailHeader.tsx +42 -0
  153. package/src/detail/DetailHub.tsx +88 -0
  154. package/src/detail/KeyValuePairs.test.tsx +2 -2
  155. package/src/detail/KeyValuePairs.tsx +25 -18
  156. package/src/detail/index.ts +3 -2
  157. package/src/edit/Edit.test.tsx +7 -5
  158. package/src/edit/Edit.tsx +92 -40
  159. package/src/edit/EditHeader.tsx +7 -5
  160. package/src/field/ArrayField.tsx +57 -11
  161. package/src/field/BadgeField.tsx +2 -3
  162. package/src/field/BooleanField.test.tsx +2 -3
  163. package/src/field/BooleanField.tsx +3 -3
  164. package/src/field/CurrencyField.tsx +1 -1
  165. package/src/field/DateField.tsx +1 -1
  166. package/src/field/IdField.test.tsx +8 -20
  167. package/src/field/IdField.tsx +5 -20
  168. package/src/field/NumberField.tsx +1 -1
  169. package/src/field/ReferenceField.test.tsx +15 -6
  170. package/src/field/ReferenceField.tsx +10 -7
  171. package/src/field/ReferenceManyField.test.tsx +55 -10
  172. package/src/field/ReferenceManyField.tsx +84 -13
  173. package/src/field/StatusIndicatorField.test.tsx +7 -21
  174. package/src/field/StatusIndicatorField.tsx +8 -20
  175. package/src/field/TextField.tsx +1 -1
  176. package/src/field/types.ts +12 -13
  177. package/src/form/Form.test.tsx +8 -4
  178. package/src/form/Form.tsx +24 -19
  179. package/src/form/index.ts +1 -1
  180. package/src/hooks/useSchemaFields.ts +89 -0
  181. package/src/i18n/Message.tsx +22 -0
  182. package/src/i18n/RecordMessage.tsx +22 -0
  183. package/src/i18n/index.ts +3 -0
  184. package/src/i18n/types.ts +19 -0
  185. package/src/index.ts +5 -1
  186. package/src/input/ArrayInput.test.tsx +81 -0
  187. package/src/input/{AttributeEditor.tsx → ArrayInput.tsx} +36 -18
  188. package/src/input/AutocompleteInput.test.tsx +2 -4
  189. package/src/input/AutocompleteInput.tsx +9 -11
  190. package/src/input/BooleanInput.tsx +42 -0
  191. package/src/input/CommonInputProps.tsx +8 -0
  192. package/src/input/FieldTitle.tsx +3 -15
  193. package/src/input/FormField.tsx +78 -67
  194. package/src/input/FormFieldContext.ts +1 -1
  195. package/src/input/NumberInput.tsx +10 -7
  196. package/src/input/ReferenceInput.test.tsx +12 -2
  197. package/src/input/ReferenceInput.tsx +32 -14
  198. package/src/input/SelectInput.tsx +14 -17
  199. package/src/input/SliderInput.test.tsx +2 -3
  200. package/src/input/SliderInput.tsx +48 -38
  201. package/src/input/TextAreaInput.tsx +10 -6
  202. package/src/input/TextInput.test.tsx +2 -4
  203. package/src/input/TextInput.tsx +35 -20
  204. package/src/input/index.ts +2 -1
  205. package/src/input/types.ts +40 -8
  206. package/src/layout/AppLayout.test.tsx +23 -3
  207. package/src/layout/AppLayout.tsx +11 -8
  208. package/src/layout/Notifications.test.tsx +102 -0
  209. package/src/layout/Notifications.tsx +61 -0
  210. package/src/layout/Ready.tsx +123 -0
  211. package/src/layout/TopNavigation.test.tsx +2 -3
  212. package/src/layout/TopNavigation.tsx +9 -8
  213. package/src/layout/index.ts +2 -0
  214. package/src/list/Cards.test.tsx +320 -0
  215. package/src/list/Cards.tsx +146 -16
  216. package/src/list/List.tsx +87 -26
  217. package/src/list/Table.test.tsx +40 -5
  218. package/src/list/Table.tsx +89 -98
  219. package/src/list/TableHeader.test.tsx +15 -11
  220. package/src/list/TableHeader.tsx +6 -8
  221. package/src/theme/ThemeManager.tsx +1 -1
  222. package/dist/__mocks__/strato-core.js +0 -50
  223. package/dist/__mocks__to__delete/strato-core.js +0 -50
  224. package/dist/detail/Show.d.ts +0 -39
  225. package/dist/detail/Show.js +0 -40
  226. package/dist/detail/ShowHeader.d.ts +0 -7
  227. package/dist/input/AttributeEditor.d.ts +0 -25
  228. package/src/detail/Show.test.tsx +0 -96
  229. package/src/detail/Show.tsx +0 -104
  230. package/src/detail/ShowHeader.tsx +0 -35
  231. 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 { ShowHeader } from './ShowHeader';
4
+ import { useResourceContext, useShowContext } from '@strato-admin/ra-core';
5
+ import { DetailHeader } from './DetailHeader';
6
6
 
7
- // Mock strato-core
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('ShowHeader', () => {
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(<ShowHeader />);
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(<ShowHeader title="My Product" />);
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(<ShowHeader />);
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(<ShowHeader actions={<div data-testid="custom-action">Custom</div>} />);
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('ra.action.edit')).toBeNull();
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
- // Mock strato-core
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
- useResourceContext,
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 = useFieldSchema();
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
- if (include) {
77
- result = result.filter(
78
- (child) => React.isValidElement(child) && include.includes((child.props as any).source)
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) && !exclude.includes((child.props as any).source)
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) => {
@@ -1,3 +1,4 @@
1
1
  export * from './KeyValuePairs';
2
- export * from './Show';
3
- export * from './ShowHeader';
2
+ export * from './Detail';
3
+ export * from './DetailHub';
4
+ export * from './DetailHeader';
@@ -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
- // Mock ra-core
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 }, defaultTitle: 'Products',
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('Products')).toBeDefined();
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 }, defaultTitle: 'Products',
80
+ record: { id: 1 },
81
+ defaultTitle: 'Products',
80
82
  resource: 'products',
81
83
  });
82
84