@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,35 @@
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 { useResourceContext, useShowContext, useTranslate, useResourceDefinitions } from '@strato-admin/core';
5
+ import { EditButton } from '../button/EditButton';
6
+
7
+ export interface ShowHeaderProps extends Omit<HeaderProps, 'children'> {
8
+ title?: React.ReactNode;
9
+ }
10
+
11
+ export const ShowHeader = ({ title, actions, ...props }: ShowHeaderProps) => {
12
+ const translate = useTranslate();
13
+ const { record, defaultTitle } = useShowContext();
14
+
15
+ const headerTitle = React.useMemo(() => {
16
+ if (title !== undefined) {
17
+ return typeof title === 'string' ? translate(title, { _: title }) : title;
18
+ }
19
+ return defaultTitle;
20
+ }, [title, defaultTitle, translate]);
21
+
22
+ const headerActions = actions || (
23
+ <SpaceBetween direction="horizontal" size="xs">
24
+ <EditButton record={record} />
25
+ </SpaceBetween>
26
+ );
27
+
28
+ return (
29
+ <Header variant="h2" {...props} actions={headerActions}>
30
+ {headerTitle}
31
+ </Header>
32
+ );
33
+ };
34
+
35
+ export default ShowHeader;
@@ -0,0 +1,3 @@
1
+ export * from './KeyValuePairs';
2
+ export * from './Show';
3
+ export * from './ShowHeader';
@@ -0,0 +1,91 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import { useEditContext, useResourceContext } from '@strato-admin/core';
5
+ import { Edit } from './Edit';
6
+
7
+ // Mock 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
+ <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
+ vi.mock('@cloudscape-design/components/button', () => ({
34
+ default: ({ children }: any) => <button>{children}</button>,
35
+ }));
36
+
37
+ describe('Edit', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ it('should render nothing when loading', () => {
43
+ (useEditContext as any).mockReturnValue({
44
+ isLoading: true,
45
+ record: undefined,
46
+ });
47
+
48
+ const { queryByTestId } = render(
49
+ <Edit>
50
+ <div data-testid="content" />
51
+ </Edit>,
52
+ );
53
+
54
+ expect(queryByTestId('container')).toBeNull();
55
+ });
56
+
57
+ it('should render content and title when record is loaded', () => {
58
+ (useEditContext as any).mockReturnValue({
59
+ isLoading: false,
60
+ record: { id: 1 }, defaultTitle: 'Products',
61
+ resource: 'products',
62
+ });
63
+ (useResourceContext as any).mockReturnValue('products');
64
+
65
+ const { getByTestId, getByText } = render(
66
+ <Edit>
67
+ <div data-testid="content">Hello World</div>
68
+ </Edit>,
69
+ );
70
+
71
+ expect(getByTestId('container')).toBeDefined();
72
+ expect(getByTestId('content').textContent).toBe('Hello World');
73
+ expect(getByText('Products')).toBeDefined();
74
+ });
75
+
76
+ it('should use provided title', () => {
77
+ (useEditContext as any).mockReturnValue({
78
+ isLoading: false,
79
+ record: { id: 1 }, defaultTitle: 'Products',
80
+ resource: 'products',
81
+ });
82
+
83
+ const { getByText } = render(
84
+ <Edit title="My Product">
85
+ <div />
86
+ </Edit>,
87
+ );
88
+
89
+ expect(getByText('My Product')).toBeDefined();
90
+ });
91
+ });
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+ import { EditBase, useEditContext, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
3
+ import Container from '@cloudscape-design/components/container';
4
+ import { EditHeader } from './EditHeader';
5
+ import Form from '../form/Form';
6
+
7
+ export interface EditProps<_RecordType extends RaRecord = RaRecord> {
8
+ children?: React.ReactNode;
9
+ inputSchema?: React.ReactNode;
10
+ title?: React.ReactNode;
11
+ actions?: React.ReactNode;
12
+ resource?: string;
13
+ id?: any;
14
+ mutationMode?: 'pessimistic' | 'optimistic' | 'undoable';
15
+ mutationOptions?: any;
16
+ queryOptions?: any;
17
+ redirect?: any;
18
+ transform?: any;
19
+ include?: string[];
20
+ exclude?: string[];
21
+ }
22
+
23
+ const EditUI = ({
24
+ children,
25
+ resource,
26
+ inputSchema,
27
+ title,
28
+ actions,
29
+ include,
30
+ exclude,
31
+ }: {
32
+ children?: React.ReactNode;
33
+ resource?: string;
34
+ inputSchema?: React.ReactNode;
35
+ title?: React.ReactNode;
36
+ actions?: React.ReactNode;
37
+ include?: string[];
38
+ exclude?: string[];
39
+ }) => {
40
+ const { record, isLoading } = useEditContext();
41
+
42
+ if (isLoading || !record) {
43
+ return null;
44
+ }
45
+
46
+ const finalChildren = children || <Form include={include} exclude={exclude} />;
47
+
48
+ return (
49
+ <ResourceSchemaProvider resource={resource} inputSchema={inputSchema}>
50
+ <Container header={<EditHeader title={title} actions={actions} />}>{finalChildren}</Container>
51
+ </ResourceSchemaProvider>
52
+ );
53
+ };
54
+
55
+ /**
56
+ * An Edit component that provides record context and a Cloudscape Container.
57
+ *
58
+ * @example
59
+ * <Edit>
60
+ * <Form>
61
+ * <TextInput source="name" />
62
+ * </Form>
63
+ * </Edit>
64
+ *
65
+ * @example
66
+ * // Using InputSchema from context
67
+ * <Edit include={['name', 'price']} />
68
+ *
69
+ * @example
70
+ * // Passing a custom input schema
71
+ * <Edit inputSchema={<InputSchema>...</InputSchema>}>
72
+ * <Form />
73
+ * </Edit>
74
+ */
75
+ export const Edit = <RecordType extends RaRecord = any>({
76
+ children,
77
+ inputSchema,
78
+ title,
79
+ actions,
80
+ include,
81
+ exclude,
82
+ ...props
83
+ }: EditProps<RecordType>) => {
84
+ return (
85
+ <EditBase {...props}>
86
+ <EditUI
87
+ resource={props.resource}
88
+ title={title}
89
+ actions={actions}
90
+ include={include}
91
+ exclude={exclude}
92
+ inputSchema={inputSchema}
93
+ >
94
+ {children}
95
+ </EditUI>
96
+ </EditBase>
97
+ );
98
+ };
99
+
100
+ Edit.Header = EditHeader;
101
+
102
+ export default Edit;
@@ -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 { useResourceContext, useEditContext, useTranslate, useResourceDefinitions } from '@strato-admin/core';
5
+
6
+ export interface EditHeaderProps extends Omit<HeaderProps, 'children'> {
7
+ title?: React.ReactNode;
8
+ }
9
+
10
+ export const EditHeader = ({ title, actions, ...props }: EditHeaderProps) => {
11
+ const translate = useTranslate();
12
+ const { defaultTitle } = useEditContext();
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 edit 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 EditHeader;
@@ -0,0 +1,2 @@
1
+ export * from './Edit';
2
+ export * from './EditHeader';
@@ -0,0 +1,51 @@
1
+ import { useRecordContext, useList, ListContextProvider, ResourceContextProvider, type RaRecord } from '@strato-admin/core';
2
+ import { type FieldProps } from './types';
3
+
4
+ export interface ArrayFieldProps<RecordType extends RaRecord = RaRecord> extends FieldProps<RecordType> {
5
+ /**
6
+ * Thecomponents to render for each item in the array.
7
+ */
8
+ children: React.ReactNode;
9
+ /**
10
+ * Number of items per page if pagination is used within the field.
11
+ */
12
+ perPage?: number;
13
+ }
14
+
15
+ /**
16
+ * ArrayField component that wraps its children with a ListContextProvider
17
+ * and ResourceContextProvider initialized with the array data and resource name.
18
+ *
19
+ * This allows using components that expect a ListContext or ResourceContext
20
+ * (like DataTable) to display nested arrays within a record.
21
+ *
22
+ * @example
23
+ * <ArrayField source="products" resource="products">
24
+ * <DataTable variant="embedded">
25
+ * <DataTable.Col source="title" label="Title" />
26
+ * <DataTable.NumberCol source="price" label="Price" />
27
+ * </DataTable>
28
+ * </ArrayField>
29
+ */
30
+ export const ArrayField = <RecordType extends RaRecord = any>(props: ArrayFieldProps<RecordType>) => {
31
+ const { source, resource, children, perPage = 10 } = props;
32
+
33
+ const record = useRecordContext<RecordType>();
34
+ const data = (source && record?.[source]) || [];
35
+
36
+ const targetResource = resource || source || '';
37
+
38
+ const listContext = useList({
39
+ data,
40
+ resource: targetResource,
41
+ perPage,
42
+ });
43
+
44
+ return (
45
+ <ResourceContextProvider value={targetResource}>
46
+ <ListContextProvider value={listContext}>{children as any}</ListContextProvider>
47
+ </ResourceContextProvider>
48
+ );
49
+ };
50
+
51
+ export default ArrayField;
@@ -0,0 +1,33 @@
1
+ import Badge, { type BadgeProps } from '@cloudscape-design/components/badge';
2
+ import { type RaRecord, useFieldValue, useRecordContext } from '@strato-admin/core';
3
+ import RecordLink from '../RecordLink';
4
+ import { type FieldProps } from './types';
5
+
6
+ export interface BadgeFieldProps<RecordType extends RaRecord = RaRecord>
7
+ extends FieldProps<RecordType> {
8
+ /**
9
+ * The color of the badge.
10
+ * @default "grey"
11
+ */
12
+ color?: BadgeProps['color'];
13
+ }
14
+
15
+ const BadgeField = <RecordType extends RaRecord = RaRecord>(props: BadgeFieldProps<RecordType>) => {
16
+ const { source, record: recordProp, emptyText, link, color = 'grey' } = props;
17
+
18
+ const record = useRecordContext<RecordType>({ record: recordProp });
19
+ const value = useFieldValue<RecordType>({ source: source as any, record });
20
+ const hasValue = value !== null && value !== undefined && value !== '';
21
+
22
+ if (!hasValue) {
23
+ return <>{emptyText ?? null}</>;
24
+ }
25
+
26
+ return (
27
+ <RecordLink link={link}>
28
+ <Badge color={color}>{String(value)}</Badge>
29
+ </RecordLink>
30
+ );
31
+ };
32
+
33
+ export default BadgeField;
@@ -0,0 +1,56 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import BooleanField from './BooleanField';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof BooleanField> = {
6
+ title: 'Fields/BooleanField',
7
+ component: BooleanField,
8
+ tags: ['autodocs'],
9
+ decorators: [withRaContext({ id: 1, isTrue: true, isFalse: false, isUndefined: undefined })],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof BooleanField>;
14
+
15
+ export const TrueValue: Story = {
16
+ args: {
17
+ source: 'isTrue',
18
+ },
19
+ };
20
+
21
+ export const FalseValue: Story = {
22
+ args: {
23
+ source: 'isFalse',
24
+ },
25
+ };
26
+
27
+ export const UndefinedValue: Story = {
28
+ args: {
29
+ source: 'isUndefined',
30
+ },
31
+ };
32
+
33
+ export const WithLabel: Story = {
34
+ args: {
35
+ source: 'isTrue',
36
+ showLabel: true,
37
+ },
38
+ };
39
+
40
+ export const CustomLabels: Story = {
41
+ args: {
42
+ source: 'isTrue',
43
+ showLabel: true,
44
+ trueLabel: 'Active',
45
+ falseLabel: 'Inactive',
46
+ },
47
+ };
48
+
49
+ export const CustomLabelsFalse: Story = {
50
+ args: {
51
+ source: 'isFalse',
52
+ showLabel: true,
53
+ trueLabel: 'Active',
54
+ falseLabel: 'Inactive',
55
+ },
56
+ };
@@ -0,0 +1,63 @@
1
+
2
+ import { render } from '@testing-library/react';
3
+ import { vi, describe, it, expect } from 'vitest';
4
+ import { useFieldValue, useRecordContext } from '@strato-admin/core';
5
+ import BooleanField from './BooleanField';
6
+
7
+ // Mock ra-core
8
+ vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
9
+
10
+ // Mock Cloudscape components
11
+ vi.mock('@cloudscape-design/components/status-indicator', () => ({
12
+ default: ({ children, type }: any) => (
13
+ <div data-testid="status-indicator" data-type={type}>
14
+ {children}
15
+ </div>
16
+ ),
17
+ }));
18
+
19
+ describe('BooleanField', () => {
20
+ it('should render check icon for true value', () => {
21
+ const record = { is_active: true };
22
+ (useRecordContext as any).mockReturnValue(record);
23
+ (useFieldValue as any).mockReturnValue(true);
24
+
25
+ const { getByTestId } = render(<BooleanField source="is_active" />);
26
+
27
+ const indicator = getByTestId('status-indicator');
28
+ expect(indicator.getAttribute('data-type')).toBe('success');
29
+ });
30
+
31
+ it('should render close icon for false value', () => {
32
+ const record = { is_active: false };
33
+ (useRecordContext as any).mockReturnValue(record);
34
+ (useFieldValue as any).mockReturnValue(false);
35
+
36
+ const { getByTestId } = render(<BooleanField source="is_active" />);
37
+
38
+ const indicator = getByTestId('status-indicator');
39
+ expect(indicator.getAttribute('data-type')).toBe('not-started');
40
+ });
41
+
42
+ it('should render label when showLabel is true', () => {
43
+ const record = { is_active: true };
44
+ (useRecordContext as any).mockReturnValue(record);
45
+ (useFieldValue as any).mockReturnValue(true);
46
+
47
+ const { getByText } = render(<BooleanField source="is_active" showLabel />);
48
+
49
+ expect(getByText('Yes')).toBeDefined();
50
+ });
51
+
52
+ it('should render custom labels', () => {
53
+ const record = { is_active: false };
54
+ (useRecordContext as any).mockReturnValue(record);
55
+ (useFieldValue as any).mockReturnValue(false);
56
+
57
+ const { getByText } = render(
58
+ <BooleanField source="is_active" showLabel trueLabel="Enabled" falseLabel="Disabled" />,
59
+ );
60
+
61
+ expect(getByText('Disabled')).toBeDefined();
62
+ });
63
+ });
@@ -0,0 +1,42 @@
1
+ import StatusIndicator from '@cloudscape-design/components/status-indicator';
2
+ import { RaRecord, useFieldValue, useRecordContext, useTranslate } from '@strato-admin/core';
3
+ import RecordLink from '../RecordLink';
4
+ import { type FieldProps } from './types';
5
+
6
+ export type BooleanFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
7
+ /**
8
+ * The label to display when the value is true. Defaults to "Yes".
9
+ */
10
+ trueLabel?: string;
11
+ /**
12
+ * The label to display when the value is false. Defaults to "No".
13
+ */
14
+ falseLabel?: string;
15
+ /**
16
+ * Whether to show the text label alongside the icon.
17
+ */
18
+ showLabel?: boolean;
19
+ };
20
+
21
+ const BooleanField = <RecordType extends RaRecord = RaRecord>(props: BooleanFieldProps<RecordType>) => {
22
+ const { source, record: recordProp, trueLabel, falseLabel, showLabel = false, link } = props;
23
+ const record = useRecordContext<RecordType>({ record: recordProp });
24
+ const value = useFieldValue<RecordType>({ source: source as any, record });
25
+ const translate = useTranslate();
26
+
27
+ const isTrue = !!value;
28
+
29
+ const content = isTrue ? (
30
+ <StatusIndicator type="success" colorOverride="green">
31
+ {showLabel ? (trueLabel ?? translate('ra.boolean.true', { _: 'Yes' })) : null}
32
+ </StatusIndicator>
33
+ ) : (
34
+ <StatusIndicator type="not-started">
35
+ {showLabel ? (falseLabel ?? translate('ra.boolean.false', { _: 'No' })) : null}
36
+ </StatusIndicator>
37
+ );
38
+
39
+ return <RecordLink link={link}>{content}</RecordLink>;
40
+ };
41
+
42
+ export default BooleanField;
@@ -0,0 +1,67 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import CurrencyField from './CurrencyField';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof CurrencyField> = {
6
+ title: 'Fields/CurrencyField',
7
+ component: CurrencyField,
8
+ tags: ['autodocs'],
9
+ decorators: [
10
+ withRaContext({
11
+ id: 1,
12
+ price: 123.45,
13
+ quantity: 10,
14
+ score: 0.85,
15
+ empty: null,
16
+ }),
17
+ ],
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof CurrencyField>;
22
+
23
+ export const Basic: Story = {
24
+ args: {
25
+ source: 'price',
26
+ currency: 'USD',
27
+ },
28
+ };
29
+
30
+ export const CustomLocale: Story = {
31
+ args: {
32
+ source: 'price',
33
+ locales: 'de-DE',
34
+ currency: 'EUR',
35
+ },
36
+ };
37
+
38
+ export const WithOptions: Story = {
39
+ args: {
40
+ source: 'price',
41
+ currency: 'USD',
42
+ options: { minimumFractionDigits: 2 },
43
+ },
44
+ };
45
+
46
+ export const Empty: Story = {
47
+ args: {
48
+ source: 'empty',
49
+ currency: 'USD',
50
+ },
51
+ };
52
+
53
+ export const WithEmptyText: Story = {
54
+ args: {
55
+ source: 'empty',
56
+ emptyText: 'N/A',
57
+ currency: 'USD',
58
+ },
59
+ };
60
+
61
+ export const WithLink: Story = {
62
+ args: {
63
+ source: 'price',
64
+ link: 'show',
65
+ currency: 'USD',
66
+ },
67
+ };
@@ -0,0 +1,45 @@
1
+ import { type RaRecord, useFieldValue, useRecordContext, useLocale } from '@strato-admin/core';
2
+ import RecordLink from '../RecordLink';
3
+ import { type FieldProps } from './types';
4
+
5
+ export type CurrencyFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
6
+ /**
7
+ * Options for Intl.NumberFormat.
8
+ */
9
+ options?: Intl.NumberFormatOptions;
10
+ /**
11
+ * Locale(s) to use for formatting. Defaults to the current app locale.
12
+ */
13
+ locales?: string | string[];
14
+ /**
15
+ * The currency to use in currency formatting.
16
+ * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#currency_2
17
+ */
18
+ currency: string;
19
+ };
20
+
21
+ const CurrencyField = <RecordType extends RaRecord = RaRecord>(props: CurrencyFieldProps<RecordType>) => {
22
+ const { source, record: recordProp, emptyText, options, locales, link, currency } = props;
23
+ const record = useRecordContext<RecordType>({ record: recordProp });
24
+ const value = useFieldValue<RecordType>({ source: source as any, record });
25
+ const currentLocale = useLocale();
26
+ const hasValue = value !== null && value !== undefined && value !== '';
27
+
28
+ if (!hasValue) {
29
+ return <>{emptyText ?? null}</>;
30
+ }
31
+
32
+ const numberValue = typeof value === 'string' ? parseFloat(value) : value;
33
+ const formatOptions: Intl.NumberFormatOptions = {
34
+ style: 'currency',
35
+ currency,
36
+ ...options,
37
+ };
38
+ const formattedValue = new Intl.NumberFormat(locales || currentLocale, formatOptions).format(numberValue);
39
+
40
+ return <RecordLink link={link}>{formattedValue}</RecordLink>;
41
+ };
42
+
43
+ (CurrencyField as any).isNumberColumn = true;
44
+
45
+ export default CurrencyField;