@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.
Files changed (255) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/Admin.d.ts +17 -0
  4. package/dist/Admin.js +69 -0
  5. package/dist/RecordLink.d.ts +9 -0
  6. package/dist/RecordLink.js +43 -0
  7. package/dist/__mocks__/strato-core.js +50 -0
  8. package/dist/__mocks__to__delete/strato-core.js +50 -0
  9. package/dist/button/BulkDeleteButton.d.ts +7 -0
  10. package/dist/button/BulkDeleteButton.js +17 -0
  11. package/dist/button/Button.d.ts +6 -0
  12. package/dist/button/Button.js +6 -0
  13. package/dist/button/CreateButton.d.ts +6 -0
  14. package/dist/button/CreateButton.js +24 -0
  15. package/dist/button/EditButton.d.ts +8 -0
  16. package/dist/button/EditButton.js +24 -0
  17. package/dist/button/SaveButton.d.ts +6 -0
  18. package/dist/button/SaveButton.js +8 -0
  19. package/dist/button/index.d.ts +5 -0
  20. package/dist/button/index.js +5 -0
  21. package/dist/collection-hooks/index.d.ts +2 -0
  22. package/dist/collection-hooks/index.js +2 -0
  23. package/dist/collection-hooks/interfaces.d.ts +93 -0
  24. package/dist/collection-hooks/interfaces.js +1 -0
  25. package/dist/collection-hooks/useCollection.d.ts +3 -0
  26. package/dist/collection-hooks/useCollection.js +102 -0
  27. package/dist/create/Create.d.ts +40 -0
  28. package/dist/create/Create.js +34 -0
  29. package/dist/create/CreateHeader.d.ts +7 -0
  30. package/dist/create/CreateHeader.js +18 -0
  31. package/dist/create/index.d.ts +2 -0
  32. package/dist/create/index.js +2 -0
  33. package/dist/detail/KeyValuePairs.d.ts +36 -0
  34. package/dist/detail/KeyValuePairs.js +58 -0
  35. package/dist/detail/Show.d.ts +39 -0
  36. package/dist/detail/Show.js +40 -0
  37. package/dist/detail/ShowHeader.d.ts +7 -0
  38. package/dist/detail/ShowHeader.js +19 -0
  39. package/dist/detail/index.d.ts +3 -0
  40. package/dist/detail/index.js +3 -0
  41. package/dist/edit/Edit.d.ts +42 -0
  42. package/dist/edit/Edit.js +38 -0
  43. package/dist/edit/EditHeader.d.ts +7 -0
  44. package/dist/edit/EditHeader.js +18 -0
  45. package/dist/edit/index.d.ts +2 -0
  46. package/dist/edit/index.js +2 -0
  47. package/dist/field/ArrayField.d.ts +29 -0
  48. package/dist/field/ArrayField.js +30 -0
  49. package/dist/field/BadgeField.d.ts +12 -0
  50. package/dist/field/BadgeField.js +15 -0
  51. package/dist/field/BooleanField.d.ts +18 -0
  52. package/dist/field/BooleanField.js +14 -0
  53. package/dist/field/CurrencyField.d.ts +19 -0
  54. package/dist/field/CurrencyField.js +23 -0
  55. package/dist/field/DateField.d.ts +14 -0
  56. package/dist/field/DateField.js +17 -0
  57. package/dist/field/IdField.d.ts +17 -0
  58. package/dist/field/IdField.js +21 -0
  59. package/dist/field/NumberField.d.ts +14 -0
  60. package/dist/field/NumberField.js +18 -0
  61. package/dist/field/ReferenceField.d.ts +16 -0
  62. package/dist/field/ReferenceField.js +23 -0
  63. package/dist/field/ReferenceManyField.d.ts +55 -0
  64. package/dist/field/ReferenceManyField.js +19 -0
  65. package/dist/field/StatusIndicatorField.d.ts +56 -0
  66. package/dist/field/StatusIndicatorField.js +48 -0
  67. package/dist/field/TextField.d.ts +5 -0
  68. package/dist/field/TextField.js +11 -0
  69. package/dist/field/index.d.ts +23 -0
  70. package/dist/field/index.js +23 -0
  71. package/dist/field/types.d.ts +56 -0
  72. package/dist/field/types.js +1 -0
  73. package/dist/form/Form.d.ts +13 -0
  74. package/dist/form/Form.js +33 -0
  75. package/dist/form/index.d.ts +2 -0
  76. package/dist/form/index.js +2 -0
  77. package/dist/index.d.ts +22 -0
  78. package/dist/index.js +22 -0
  79. package/dist/input/AttributeEditor.d.ts +25 -0
  80. package/dist/input/AttributeEditor.js +80 -0
  81. package/dist/input/AutocompleteInput.d.ts +10 -0
  82. package/dist/input/AutocompleteInput.js +67 -0
  83. package/dist/input/FieldTitle.d.ts +8 -0
  84. package/dist/input/FieldTitle.js +29 -0
  85. package/dist/input/FormField.d.ts +7 -0
  86. package/dist/input/FormField.js +35 -0
  87. package/dist/input/FormFieldContext.d.ts +6 -0
  88. package/dist/input/FormFieldContext.js +3 -0
  89. package/dist/input/NumberInput.d.ts +7 -0
  90. package/dist/input/NumberInput.js +27 -0
  91. package/dist/input/ReferenceInput.d.ts +3 -0
  92. package/dist/input/ReferenceInput.js +25 -0
  93. package/dist/input/SelectInput.d.ts +15 -0
  94. package/dist/input/SelectInput.js +47 -0
  95. package/dist/input/SliderInput.d.ts +6 -0
  96. package/dist/input/SliderInput.js +25 -0
  97. package/dist/input/TextAreaInput.d.ts +6 -0
  98. package/dist/input/TextAreaInput.js +23 -0
  99. package/dist/input/TextInput.d.ts +7 -0
  100. package/dist/input/TextInput.js +23 -0
  101. package/dist/input/index.d.ts +11 -0
  102. package/dist/input/index.js +11 -0
  103. package/dist/input/types.d.ts +6 -0
  104. package/dist/input/types.js +1 -0
  105. package/dist/layout/AppLayout.d.ts +8 -0
  106. package/dist/layout/AppLayout.js +38 -0
  107. package/dist/layout/TopNavigation.d.ts +6 -0
  108. package/dist/layout/TopNavigation.js +53 -0
  109. package/dist/layout/index.d.ts +2 -0
  110. package/dist/layout/index.js +2 -0
  111. package/dist/list/Cards.d.ts +11 -0
  112. package/dist/list/Cards.js +27 -0
  113. package/dist/list/List.d.ts +43 -0
  114. package/dist/list/List.js +28 -0
  115. package/dist/list/Table.d.ts +112 -0
  116. package/dist/list/Table.examples.d.ts +1 -0
  117. package/dist/list/Table.examples.js +3 -0
  118. package/dist/list/Table.js +218 -0
  119. package/dist/list/TableHeader.d.ts +7 -0
  120. package/dist/list/TableHeader.js +22 -0
  121. package/dist/list/index.d.ts +4 -0
  122. package/dist/list/index.js +4 -0
  123. package/dist/preferences/index.d.ts +0 -0
  124. package/dist/preferences/index.js +1 -0
  125. package/dist/theme/ThemeManager.d.ts +2 -0
  126. package/dist/theme/ThemeManager.js +11 -0
  127. package/dist/theme/index.d.ts +2 -0
  128. package/dist/theme/index.js +2 -0
  129. package/package.json +73 -0
  130. package/src/Admin.test.tsx +32 -0
  131. package/src/Admin.tsx +123 -0
  132. package/src/RecordLink.stories.tsx +56 -0
  133. package/src/RecordLink.tsx +67 -0
  134. package/src/__mocks__/strato-core.tsx +52 -0
  135. package/src/button/BulkDeleteButton.stories.tsx +59 -0
  136. package/src/button/BulkDeleteButton.test.tsx +64 -0
  137. package/src/button/BulkDeleteButton.tsx +41 -0
  138. package/src/button/Button.stories.tsx +31 -0
  139. package/src/button/Button.tsx +12 -0
  140. package/src/button/CreateButton.stories.tsx +42 -0
  141. package/src/button/CreateButton.tsx +38 -0
  142. package/src/button/EditButton.stories.tsx +29 -0
  143. package/src/button/EditButton.tsx +38 -0
  144. package/src/button/SaveButton.stories.tsx +35 -0
  145. package/src/button/SaveButton.tsx +19 -0
  146. package/src/button/index.ts +5 -0
  147. package/src/collection-hooks/index.ts +2 -0
  148. package/src/collection-hooks/interfaces.ts +80 -0
  149. package/src/collection-hooks/useCollection.test.ts +413 -0
  150. package/src/collection-hooks/useCollection.ts +125 -0
  151. package/src/create/Create.test.tsx +63 -0
  152. package/src/create/Create.tsx +93 -0
  153. package/src/create/CreateHeader.tsx +34 -0
  154. package/src/create/index.ts +2 -0
  155. package/src/detail/KeyValuePairs.test.tsx +98 -0
  156. package/src/detail/KeyValuePairs.tsx +107 -0
  157. package/src/detail/Show.test.tsx +96 -0
  158. package/src/detail/Show.tsx +104 -0
  159. package/src/detail/ShowHeader.test.tsx +80 -0
  160. package/src/detail/ShowHeader.tsx +35 -0
  161. package/src/detail/index.ts +3 -0
  162. package/src/edit/Edit.test.tsx +91 -0
  163. package/src/edit/Edit.tsx +102 -0
  164. package/src/edit/EditHeader.tsx +34 -0
  165. package/src/edit/index.ts +2 -0
  166. package/src/field/ArrayField.tsx +51 -0
  167. package/src/field/BadgeField.tsx +33 -0
  168. package/src/field/BooleanField.stories.tsx +56 -0
  169. package/src/field/BooleanField.test.tsx +63 -0
  170. package/src/field/BooleanField.tsx +42 -0
  171. package/src/field/CurrencyField.stories.tsx +67 -0
  172. package/src/field/CurrencyField.tsx +45 -0
  173. package/src/field/DateField.stories.tsx +67 -0
  174. package/src/field/DateField.tsx +33 -0
  175. package/src/field/IdField.test.tsx +88 -0
  176. package/src/field/IdField.tsx +40 -0
  177. package/src/field/NumberField.stories.tsx +75 -0
  178. package/src/field/NumberField.tsx +35 -0
  179. package/src/field/ReferenceField.test.tsx +88 -0
  180. package/src/field/ReferenceField.tsx +64 -0
  181. package/src/field/ReferenceManyField.test.tsx +41 -0
  182. package/src/field/ReferenceManyField.tsx +73 -0
  183. package/src/field/StatusIndicatorField.stories.tsx +93 -0
  184. package/src/field/StatusIndicatorField.test.tsx +143 -0
  185. package/src/field/StatusIndicatorField.tsx +119 -0
  186. package/src/field/TextField.stories.tsx +45 -0
  187. package/src/field/TextField.tsx +17 -0
  188. package/src/field/index.ts +23 -0
  189. package/src/field/types.ts +58 -0
  190. package/src/form/Form.test.tsx +55 -0
  191. package/src/form/Form.tsx +66 -0
  192. package/src/form/index.ts +2 -0
  193. package/src/index.ts +25 -0
  194. package/src/input/AttributeEditor.test.tsx +147 -0
  195. package/src/input/AttributeEditor.tsx +185 -0
  196. package/src/input/AutocompleteInput.test.tsx +178 -0
  197. package/src/input/AutocompleteInput.tsx +116 -0
  198. package/src/input/FieldTitle.tsx +53 -0
  199. package/src/input/FormField.tsx +87 -0
  200. package/src/input/FormFieldContext.ts +9 -0
  201. package/src/input/NumberInput.tsx +56 -0
  202. package/src/input/ReferenceInput.test.tsx +35 -0
  203. package/src/input/ReferenceInput.tsx +36 -0
  204. package/src/input/SelectInput.tsx +91 -0
  205. package/src/input/SliderInput.test.tsx +103 -0
  206. package/src/input/SliderInput.tsx +49 -0
  207. package/src/input/TextAreaInput.tsx +48 -0
  208. package/src/input/TextInput.test.tsx +91 -0
  209. package/src/input/TextInput.tsx +51 -0
  210. package/src/input/index.ts +11 -0
  211. package/src/input/types.ts +14 -0
  212. package/src/layout/AppLayout.test.tsx +87 -0
  213. package/src/layout/AppLayout.tsx +60 -0
  214. package/src/layout/TopNavigation.test.tsx +78 -0
  215. package/src/layout/TopNavigation.tsx +84 -0
  216. package/src/layout/index.ts +2 -0
  217. package/src/list/Cards.tsx +58 -0
  218. package/src/list/List.tsx +76 -0
  219. package/src/list/Table.examples.tsx +11 -0
  220. package/src/list/Table.stories.tsx +73 -0
  221. package/src/list/Table.test.tsx +255 -0
  222. package/src/list/Table.tsx +438 -0
  223. package/src/list/TableHeader.test.tsx +114 -0
  224. package/src/list/TableHeader.tsx +44 -0
  225. package/src/list/index.ts +4 -0
  226. package/src/preferences/index.ts +0 -0
  227. package/src/stories/Button.stories.ts +54 -0
  228. package/src/stories/Button.tsx +31 -0
  229. package/src/stories/Configure.mdx +369 -0
  230. package/src/stories/Header.stories.ts +34 -0
  231. package/src/stories/Header.tsx +47 -0
  232. package/src/stories/Page.stories.ts +33 -0
  233. package/src/stories/Page.tsx +71 -0
  234. package/src/stories/RaStoryDecorator.tsx +38 -0
  235. package/src/stories/assets/accessibility.png +0 -0
  236. package/src/stories/assets/accessibility.svg +1 -0
  237. package/src/stories/assets/addon-library.png +0 -0
  238. package/src/stories/assets/assets.png +0 -0
  239. package/src/stories/assets/avif-test-image.avif +0 -0
  240. package/src/stories/assets/context.png +0 -0
  241. package/src/stories/assets/discord.svg +1 -0
  242. package/src/stories/assets/docs.png +0 -0
  243. package/src/stories/assets/figma-plugin.png +0 -0
  244. package/src/stories/assets/github.svg +1 -0
  245. package/src/stories/assets/share.png +0 -0
  246. package/src/stories/assets/styling.png +0 -0
  247. package/src/stories/assets/testing.png +0 -0
  248. package/src/stories/assets/theming.png +0 -0
  249. package/src/stories/assets/tutorials.svg +1 -0
  250. package/src/stories/assets/youtube.svg +1 -0
  251. package/src/stories/button.css +30 -0
  252. package/src/stories/header.css +32 -0
  253. package/src/stories/page.css +68 -0
  254. package/src/theme/ThemeManager.tsx +15 -0
  255. package/src/theme/index.ts +2 -0
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import { useResourceContext } from '@strato-admin/core';
5
+ import { Create } from './Create';
6
+
7
+ // Mock strato-core
8
+ vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
9
+
10
+ // Mock Cloudscape components
11
+ vi.mock('@cloudscape-design/components/container', () => ({
12
+ default: ({ children, header }: any) => (
13
+ <div data-testid="container">
14
+ <div data-testid="container-header">{header}</div>
15
+ <div data-testid="container-content">{children}</div>
16
+ </div>
17
+ ),
18
+ }));
19
+
20
+ vi.mock('@cloudscape-design/components/header', () => ({
21
+ default: ({ children, actions }: any) => (
22
+ <header>
23
+ <div>{children}</div>
24
+ <div>{actions}</div>
25
+ </header>
26
+ ),
27
+ }));
28
+
29
+ vi.mock('@cloudscape-design/components/space-between', () => ({
30
+ default: ({ children }: any) => <div>{children}</div>,
31
+ }));
32
+
33
+ describe('Create', () => {
34
+ beforeEach(() => {
35
+ vi.clearAllMocks();
36
+ });
37
+
38
+ it('should render content and title', () => {
39
+ (useResourceContext as any).mockReturnValue('products');
40
+
41
+ const { getByTestId, getByText } = render(
42
+ <Create>
43
+ <div data-testid="content">Create New</div>
44
+ </Create>,
45
+ );
46
+
47
+ expect(getByTestId('container')).toBeDefined();
48
+ expect(getByTestId('content').textContent).toBe('Create New');
49
+ expect(getByText('Products')).toBeDefined();
50
+ });
51
+
52
+ it('should use provided title', () => {
53
+ (useResourceContext as any).mockReturnValue('products');
54
+
55
+ const { getByText } = render(
56
+ <Create title="Add New Product">
57
+ <div />
58
+ </Create>,
59
+ );
60
+
61
+ expect(getByText('Add New Product')).toBeDefined();
62
+ });
63
+ });
@@ -0,0 +1,93 @@
1
+ import React from 'react';
2
+ import { CreateBase, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
3
+ import Container from '@cloudscape-design/components/container';
4
+ import { CreateHeader } from './CreateHeader';
5
+ import Form from '../form/Form';
6
+
7
+ export interface CreateProps<RecordType extends RaRecord = RaRecord> {
8
+ children?: React.ReactNode;
9
+ inputSchema?: React.ReactNode;
10
+ title?: React.ReactNode;
11
+ actions?: React.ReactNode;
12
+ resource?: string;
13
+ record?: Partial<RecordType>;
14
+ redirect?: any;
15
+ transform?: any;
16
+ mutationOptions?: any;
17
+ include?: string[];
18
+ exclude?: string[];
19
+ }
20
+
21
+ const CreateUI = ({
22
+ children,
23
+ resource,
24
+ inputSchema,
25
+ title,
26
+ actions,
27
+ include,
28
+ exclude,
29
+ }: {
30
+ children?: React.ReactNode;
31
+ resource?: string;
32
+ inputSchema?: React.ReactNode;
33
+ title?: React.ReactNode;
34
+ actions?: React.ReactNode;
35
+ include?: string[];
36
+ exclude?: string[];
37
+ }) => {
38
+ const finalChildren = children || <Form include={include} exclude={exclude} />;
39
+ return (
40
+ <ResourceSchemaProvider resource={resource} inputSchema={inputSchema}>
41
+ <Container header={<CreateHeader title={title} actions={actions} />}>{finalChildren}</Container>
42
+ </ResourceSchemaProvider>
43
+ );
44
+ };
45
+
46
+ /**
47
+ * A Create component that provides record context and a Cloudscape Container.
48
+ *
49
+ * @example
50
+ * <Create>
51
+ * <Form>
52
+ * <TextInput source="name" />
53
+ * </Form>
54
+ * </Create>
55
+ *
56
+ * @example
57
+ * // Using InputSchema from context
58
+ * <Create include={['name', 'price']} />
59
+ *
60
+ * @example
61
+ * // Passing a custom input schema
62
+ * <Create inputSchema={<InputSchema>...</InputSchema>}>
63
+ * <Form />
64
+ * </Create>
65
+ */
66
+ export const Create = <RecordType extends RaRecord = RaRecord>({
67
+ children,
68
+ inputSchema,
69
+ title,
70
+ actions,
71
+ include,
72
+ exclude,
73
+ ...props
74
+ }: CreateProps<RecordType>) => {
75
+ return (
76
+ <CreateBase {...props}>
77
+ <CreateUI
78
+ resource={props.resource}
79
+ title={title}
80
+ actions={actions}
81
+ include={include}
82
+ exclude={exclude}
83
+ inputSchema={inputSchema}
84
+ >
85
+ {children}
86
+ </CreateUI>
87
+ </CreateBase>
88
+ );
89
+ };
90
+
91
+ Create.Header = CreateHeader;
92
+
93
+ export default Create;
@@ -0,0 +1,34 @@
1
+ import React from 'react';
2
+ import Header, { HeaderProps } from '@cloudscape-design/components/header';
3
+ import SpaceBetween from '@cloudscape-design/components/space-between';
4
+ import { useCreateContext, useTranslate } from '@strato-admin/core';
5
+
6
+ export interface CreateHeaderProps extends Omit<HeaderProps, 'children'> {
7
+ title?: React.ReactNode;
8
+ }
9
+
10
+ export const CreateHeader = ({ title, actions, ...props }: CreateHeaderProps) => {
11
+ const translate = useTranslate();
12
+ const { defaultTitle } = useCreateContext();
13
+
14
+ const headerTitle = React.useMemo(() => {
15
+ if (title !== undefined) {
16
+ return typeof title === 'string' ? translate(title, { _: title }) : title;
17
+ }
18
+ return defaultTitle;
19
+ }, [title, defaultTitle, translate]);
20
+
21
+ const headerActions = actions || (
22
+ <SpaceBetween direction="horizontal" size="xs">
23
+ {/* Add default create actions here if needed */}
24
+ </SpaceBetween>
25
+ );
26
+
27
+ return (
28
+ <Header variant="h2" {...props} actions={headerActions}>
29
+ {headerTitle}
30
+ </Header>
31
+ );
32
+ };
33
+
34
+ export default CreateHeader;
@@ -0,0 +1,2 @@
1
+ export * from './Create';
2
+ export * from './CreateHeader';
@@ -0,0 +1,98 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import { useResourceContext, useTranslate, useRecordContext } from '@strato-admin/core';
5
+ import KeyValuePairs from './KeyValuePairs';
6
+ import CloudscapeKeyValuePairs from '@cloudscape-design/components/key-value-pairs';
7
+
8
+ // Mock strato-core
9
+ vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
10
+
11
+ // Mock react-router-dom
12
+ vi.mock('react-router-dom', () => ({
13
+ useNavigate: vi.fn(),
14
+ }));
15
+
16
+ // Mock Cloudscape components
17
+ vi.mock('@cloudscape-design/components/key-value-pairs', () => ({
18
+ default: vi.fn(({ items }: any) => (
19
+ <div data-testid="cloudscape-kvp">
20
+ {items.map((item: any, index: number) => (
21
+ <div key={index} data-testid={`kv-item-${index}`}>
22
+ <span data-testid={`kv-label-${index}`}>{item.label}</span>
23
+ <div data-testid={`kv-value-${index}`}>{item.value}</div>
24
+ </div>
25
+ ))}
26
+ </div>
27
+ )),
28
+ }));
29
+
30
+ vi.mock('@cloudscape-design/components/box', () => ({
31
+ default: ({ children }: any) => <div>{children}</div>,
32
+ }));
33
+
34
+ describe('KeyValuePairs', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks();
37
+ (useResourceContext as any).mockReturnValue('products');
38
+ (useRecordContext as any).mockReturnValue({ id: 1, name: 'Gadget', price: 100 });
39
+ });
40
+
41
+ it('should render children and automatically generate labels', () => {
42
+ render(
43
+ <KeyValuePairs>
44
+ <KeyValuePairs.Field source="name" />
45
+ <KeyValuePairs.Field source="price" label="Cost" />
46
+ </KeyValuePairs>,
47
+ );
48
+
49
+ const kvpProps = (CloudscapeKeyValuePairs as any).mock.calls[0][0];
50
+ expect(kvpProps.items).toHaveLength(2);
51
+ // We check the source/label props of the FieldTitle component
52
+ expect(kvpProps.items[0].label.props.source).toBe('name');
53
+ expect(kvpProps.items[1].label.props.label).toBe('Cost');
54
+ });
55
+
56
+ it('should use translation for labels if available', () => {
57
+ const translate = vi.fn((key) => {
58
+ if (key === 'resources.products.fields.name') return 'Product Name';
59
+ return key;
60
+ });
61
+ (useTranslate as any).mockReturnValue(translate);
62
+
63
+ render(
64
+ <KeyValuePairs>
65
+ <KeyValuePairs.Field source="name" />
66
+ </KeyValuePairs>,
67
+ );
68
+
69
+ const kvpProps = (CloudscapeKeyValuePairs as any).mock.calls[0][0];
70
+ expect(kvpProps.items[0].label.props.source).toBe('name');
71
+ });
72
+
73
+ it('should pass columns and minColumnWidth props to CloudscapeKeyValuePairs', () => {
74
+ render(
75
+ <KeyValuePairs columns={3} minColumnWidth={200}>
76
+ <KeyValuePairs.Field source="name" />
77
+ </KeyValuePairs>,
78
+ );
79
+
80
+ const kvpProps = (CloudscapeKeyValuePairs as any).mock.calls[0][0];
81
+ expect(kvpProps.columns).toBe(3);
82
+ expect(kvpProps.minColumnWidth).toBe(200);
83
+ });
84
+
85
+ it('should provide record context to children', () => {
86
+ const record = { id: 1, name: 'Gadget' };
87
+ (useRecordContext as any).mockReturnValue(record);
88
+
89
+ // TextField should render 'Gadget' because we mocked useFieldValue to return record[source]
90
+ const { getByTestId } = render(
91
+ <KeyValuePairs>
92
+ <KeyValuePairs.Field source="name" />
93
+ </KeyValuePairs>,
94
+ );
95
+
96
+ expect(getByTestId('kv-value-0').textContent).toBe('Gadget');
97
+ });
98
+ });
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import CloudscapeKeyValuePairs, {
3
+ type KeyValuePairsProps as CloudscapeKeyValuePairsProps,
4
+ } from '@cloudscape-design/components/key-value-pairs';
5
+ import {
6
+ useResourceContext,
7
+ useRecordContext,
8
+ FieldTitle,
9
+ RecordContextProvider,
10
+ type RaRecord,
11
+ useFieldSchema,
12
+ } from '@strato-admin/core';
13
+ import TextField from '../field/TextField';
14
+
15
+ export interface KeyValuePairsProps extends Partial<Omit<CloudscapeKeyValuePairsProps, 'items'>> {
16
+ children?: React.ReactNode;
17
+ include?: string[];
18
+ exclude?: string[];
19
+ columns?: number;
20
+ minColumnWidth?: number;
21
+ }
22
+
23
+ export interface KeyValueFieldProps {
24
+ source?: string;
25
+ label?: string;
26
+ children?: React.ReactNode;
27
+ field?: React.ComponentType<any>;
28
+ }
29
+
30
+ /**
31
+ * KeyValuePairs.Field is a helper component to define a field in a KeyValuePairs component.
32
+ * It mirrors the DataTable.Col pattern.
33
+ */
34
+ export const KeyValueField = ({ children, source, field: FieldComponent }: KeyValueFieldProps) => {
35
+ if (children) {
36
+ return (
37
+ <>
38
+ {React.Children.map(children, (child) =>
39
+ React.isValidElement(child) ? React.cloneElement(child, { source } as any) : child
40
+ )}
41
+ </>
42
+ );
43
+ }
44
+ if (FieldComponent) {
45
+ return <FieldComponent source={source} />;
46
+ }
47
+ return <TextField source={source} />;
48
+ };
49
+
50
+ /**
51
+ * A KeyValuePairs component that mirrors the Cloudscape KeyValuePairs component
52
+ * but automatically handles labels and record context for its children.
53
+ *
54
+ * @example
55
+ * <KeyValuePairs columns={2}>
56
+ * <TextField source="name" label="Full Name" />
57
+ * <KeyValuePairs.Field source="age" label="Age" />
58
+ * </KeyValuePairs>
59
+ */
60
+ export const KeyValuePairs = <RecordType extends RaRecord = RaRecord>({
61
+ children,
62
+ include,
63
+ exclude,
64
+ columns = 3, // Default to 3 columns if not specified
65
+ minColumnWidth,
66
+ ...props
67
+ }: KeyValuePairsProps) => {
68
+ const resource = useResourceContext();
69
+ const record = useRecordContext<RecordType>();
70
+ const schemaChildren = useFieldSchema();
71
+
72
+ const finalChildren = React.useMemo(() => {
73
+ const baseChildren = children || schemaChildren;
74
+ let result = React.Children.toArray(baseChildren);
75
+
76
+ if (include) {
77
+ result = result.filter(
78
+ (child) => React.isValidElement(child) && include.includes((child.props as any).source)
79
+ );
80
+ } else if (exclude) {
81
+ result = result.filter(
82
+ (child) => React.isValidElement(child) && !exclude.includes((child.props as any).source)
83
+ );
84
+ }
85
+
86
+ return result;
87
+ }, [children, schemaChildren, include, exclude]);
88
+
89
+ const items =
90
+ React.Children.map(finalChildren, (child) => {
91
+ if (!React.isValidElement(child)) {
92
+ return null;
93
+ }
94
+
95
+ const { source, label } = child.props as any;
96
+ return {
97
+ label: <FieldTitle source={source} resource={resource} label={label} />,
98
+ value: <RecordContextProvider value={record}>{child as any}</RecordContextProvider>,
99
+ };
100
+ })?.filter((item): item is Exclude<typeof item, null> => item !== null) || [];
101
+
102
+ return <CloudscapeKeyValuePairs {...props} items={items} columns={columns} minColumnWidth={minColumnWidth} />;
103
+ };
104
+
105
+ KeyValuePairs.Field = KeyValueField;
106
+
107
+ export default KeyValuePairs;
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import { useShowContext, useResourceContext } from '@strato-admin/core';
5
+ import Show from './Show';
6
+
7
+ // Mock strato-core
8
+ vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
9
+
10
+ // Mock react-router-dom
11
+ vi.mock('react-router-dom', () => ({
12
+ useNavigate: vi.fn(),
13
+ }));
14
+
15
+ // Mock Cloudscape components
16
+ vi.mock('@cloudscape-design/components/container', () => ({
17
+ default: ({ children, header }: any) => (
18
+ <div data-testid="container">
19
+ <div data-testid="container-header">{header}</div>
20
+ <div data-testid="container-content">{children}</div>
21
+ </div>
22
+ ),
23
+ }));
24
+
25
+ vi.mock('@cloudscape-design/components/header', () => ({
26
+ default: ({ children, actions }: any) => (
27
+ <header>
28
+ <div>{children}</div>
29
+ <div>{actions}</div>
30
+ </header>
31
+ ),
32
+ }));
33
+
34
+ vi.mock('@cloudscape-design/components/space-between', () => ({
35
+ default: ({ children }: any) => <div>{children}</div>,
36
+ }));
37
+
38
+ vi.mock('@cloudscape-design/components/button', () => ({
39
+ default: ({ children }: any) => <button>{children}</button>,
40
+ }));
41
+
42
+ describe('Show', () => {
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ });
46
+
47
+ it('should render nothing when loading', () => {
48
+ (useShowContext as any).mockReturnValue({
49
+ isLoading: true,
50
+ record: undefined,
51
+ });
52
+
53
+ const { queryByTestId } = render(
54
+ <Show>
55
+ <div data-testid="content" />
56
+ </Show>,
57
+ );
58
+
59
+ expect(queryByTestId('container')).toBeNull();
60
+ });
61
+
62
+ it('should render content and title when record is loaded', () => {
63
+ (useShowContext as any).mockReturnValue({
64
+ isLoading: false,
65
+ record: { id: 1 }, defaultTitle: 'Products',
66
+ resource: 'products',
67
+ });
68
+ (useResourceContext as any).mockReturnValue('products');
69
+
70
+ const { getByTestId, getByText } = render(
71
+ <Show>
72
+ <div data-testid="content">Hello World</div>
73
+ </Show>,
74
+ );
75
+
76
+ expect(getByTestId('container')).toBeDefined();
77
+ expect(getByTestId('content').textContent).toBe('Hello World');
78
+ expect(getByText('Products')).toBeDefined();
79
+ });
80
+
81
+ it('should use provided title', () => {
82
+ (useShowContext as any).mockReturnValue({
83
+ isLoading: false,
84
+ record: { id: 1 }, defaultTitle: 'Products',
85
+ resource: 'products',
86
+ });
87
+
88
+ const { getByText } = render(
89
+ <Show title="My Product">
90
+ <div />
91
+ </Show>,
92
+ );
93
+
94
+ expect(getByText('My Product')).toBeDefined();
95
+ });
96
+ });
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+ import { ShowBase, useShowContext, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
3
+ import Container from '@cloudscape-design/components/container';
4
+ import SpaceBetween from '@cloudscape-design/components/space-between';
5
+ import { ShowHeader } from './ShowHeader';
6
+ import KeyValuePairs from './KeyValuePairs';
7
+
8
+ export interface ShowProps<_RecordType extends RaRecord = RaRecord> {
9
+ children?: React.ReactNode;
10
+ fieldSchema?: React.ReactNode;
11
+ title?: React.ReactNode;
12
+ actions?: React.ReactNode;
13
+ resource?: string;
14
+ id?: any;
15
+ queryOptions?: any;
16
+ include?: string[];
17
+ exclude?: string[];
18
+ }
19
+
20
+ const ShowUI = ({
21
+ children,
22
+ resource,
23
+ fieldSchema,
24
+ title,
25
+ actions,
26
+ include,
27
+ exclude,
28
+ }: {
29
+ children?: React.ReactNode;
30
+ resource?: string;
31
+ fieldSchema?: React.ReactNode;
32
+ title?: React.ReactNode;
33
+ actions?: React.ReactNode;
34
+ include?: string[];
35
+ exclude?: string[];
36
+ }) => {
37
+ const { record, isLoading } = useShowContext();
38
+
39
+ if (isLoading || !record) {
40
+ return null;
41
+ }
42
+
43
+ const finalChildren = children || <KeyValuePairs include={include} exclude={exclude} />;
44
+
45
+ return (
46
+ <ResourceSchemaProvider resource={resource} fieldSchema={fieldSchema}>
47
+ <Container header={<ShowHeader title={title} actions={actions} />}>
48
+ <SpaceBetween direction="vertical" size="l">
49
+ {finalChildren}
50
+ </SpaceBetween>
51
+ </Container>
52
+ </ResourceSchemaProvider>
53
+ );
54
+ };
55
+
56
+ /**
57
+ * A Show component that provides record context and a Cloudscape Container.
58
+ *
59
+ * @example
60
+ * <Show>
61
+ * <KeyValuePairs>
62
+ * <TextField source="name" />
63
+ * <NumberField source="price" />
64
+ * </KeyValuePairs>
65
+ * </Show>
66
+ *
67
+ * @example
68
+ * // Using FieldSchema from context
69
+ * <Show include={['name', 'price']} />
70
+ *
71
+ * @example
72
+ * // Passing a custom field schema
73
+ * <Show fieldSchema={<FieldSchema>...</FieldSchema>}>
74
+ * <KeyValuePairs />
75
+ * </Show>
76
+ */
77
+ export const Show = <RecordType extends RaRecord = RaRecord>({
78
+ children,
79
+ fieldSchema,
80
+ title,
81
+ actions,
82
+ include,
83
+ exclude,
84
+ ...props
85
+ }: ShowProps<RecordType>) => {
86
+ return (
87
+ <ShowBase {...props}>
88
+ <ShowUI
89
+ resource={props.resource}
90
+ title={title}
91
+ actions={actions}
92
+ include={include}
93
+ exclude={exclude}
94
+ fieldSchema={fieldSchema}
95
+ >
96
+ {children}
97
+ </ShowUI>
98
+ </ShowBase>
99
+ );
100
+ };
101
+
102
+ Show.Header = ShowHeader;
103
+
104
+ export default Show;
@@ -0,0 +1,80 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import { useResourceContext, useShowContext } from '@strato-admin/core';
5
+ import { ShowHeader } from './ShowHeader';
6
+
7
+ // Mock strato-core
8
+ vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
9
+
10
+ // Mock react-router-dom
11
+ vi.mock('react-router-dom', () => ({
12
+ useNavigate: vi.fn(),
13
+ }));
14
+
15
+ // Mock Cloudscape components
16
+ vi.mock('@cloudscape-design/components/header', () => ({
17
+ default: ({ children, actions }: any) => (
18
+ <header>
19
+ <div data-testid="header-title">{children}</div>
20
+ <div data-testid="header-actions">{actions}</div>
21
+ </header>
22
+ ),
23
+ }));
24
+
25
+ vi.mock('@cloudscape-design/components/space-between', () => ({
26
+ default: ({ children }: any) => <div data-testid="space-between">{children}</div>,
27
+ }));
28
+
29
+ vi.mock('@cloudscape-design/components/button', () => ({
30
+ default: ({ children, variant }: any) => (
31
+ <button data-testid="button" data-variant={variant}>
32
+ {children}
33
+ </button>
34
+ ),
35
+ }));
36
+
37
+ describe('ShowHeader', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ it('should render title from resource', () => {
43
+ (useResourceContext as any).mockReturnValue('products');
44
+ (useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
45
+
46
+ const { getByTestId } = render(<ShowHeader />);
47
+
48
+ expect(getByTestId('header-title').textContent).toBe('Products');
49
+ });
50
+
51
+ it('should render provided title', () => {
52
+ (useResourceContext as any).mockReturnValue('products');
53
+ (useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
54
+
55
+ const { getByTestId } = render(<ShowHeader title="My Product" />);
56
+
57
+ expect(getByTestId('header-title').textContent).toBe('My Product');
58
+ });
59
+
60
+ it('should render EditButton by default as primary', () => {
61
+ (useResourceContext as any).mockReturnValue('products');
62
+ (useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
63
+
64
+ const { getByText } = render(<ShowHeader />);
65
+
66
+ const editButton = getByText('Edit');
67
+ expect(editButton).toBeDefined();
68
+ expect(editButton.getAttribute('data-variant')).toBe('primary');
69
+ });
70
+
71
+ it('should render custom actions if provided', () => {
72
+ (useResourceContext as any).mockReturnValue('products');
73
+ (useShowContext as any).mockReturnValue({ record: { id: 1 }, defaultTitle: 'Products' });
74
+
75
+ const { getByTestId, queryByText } = render(<ShowHeader actions={<div data-testid="custom-action">Custom</div>} />);
76
+
77
+ expect(getByTestId('custom-action')).toBeDefined();
78
+ expect(queryByText('ra.action.edit')).toBeNull();
79
+ });
80
+ });