@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
@@ -1,22 +1,39 @@
1
-
2
1
  import React from 'react';
3
2
  import { render, screen, cleanup } from '@testing-library/react';
4
3
  import { vi, describe, it, expect, beforeEach } from 'vitest';
5
4
  import ReferenceManyField from './ReferenceManyField';
5
+ import Table from '../list/Table';
6
6
 
7
- // Mock ra-core (via strato-core)
7
+ // Mock ra-core
8
+ vi.mock('@strato-admin/ra-core', () => ({
9
+ ReferenceManyFieldBase: vi.fn(({ children }: any) => (
10
+ <div data-testid="ra-reference-many-field-base">{children}</div>
11
+ )),
12
+ ResourceContextProvider: ({ children }: any) => <div data-testid="resource-context-provider">{children}</div>,
13
+ }));
14
+
15
+ // Mock strato-core
8
16
  vi.mock('@strato-admin/core', () => ({
9
- ReferenceManyFieldBase: vi.fn(({ children }: any) => <div data-testid="ra-reference-many-field-base">{children}</div>),
10
17
  ResourceSchemaProvider: vi.fn(({ children }: any) => <div data-testid="resource-schema-provider">{children}</div>),
11
- ResourceContextProvider: ({ children }: any) => <div data-testid="resource-context-provider">{children}</div>,
18
+ useResourceSchema: vi.fn((resource: string) => ({
19
+ listComponent: resource === 'with-custom-list' ? MockCustomList : undefined,
20
+ })),
12
21
  }));
13
22
 
23
+ // Mock Table
24
+ vi.mock('../list/Table', () => ({
25
+ default: vi.fn(({ title }: any) => <div data-testid="default-table">{title}</div>),
26
+ }));
27
+
28
+ const MockCustomList = vi.fn(({ title }: any) => <div data-testid="custom-list">{title}</div>);
29
+
14
30
  describe('ReferenceManyField', () => {
15
31
  beforeEach(() => {
16
32
  cleanup();
33
+ vi.clearAllMocks();
17
34
  });
18
35
 
19
- it('should render children within providers', () => {
36
+ it('should render children within providers if provided', () => {
20
37
  render(
21
38
  <ReferenceManyField reference="comments" target="post_id">
22
39
  <div data-testid="child">Child Content</div>
@@ -29,13 +46,41 @@ describe('ReferenceManyField', () => {
29
46
  expect(screen.getByTestId('child').textContent).toBe('Child Content');
30
47
  });
31
48
 
32
- it('should pass correct resource to ResourceSchemaProvider', () => {
49
+ it('should render default Table if no children provided', () => {
50
+ render(<ReferenceManyField reference="comments" target="post_id" title="Default Table Title" />);
51
+
52
+ expect(screen.getByTestId('default-table')).toBeDefined();
53
+ expect(screen.getByTestId('default-table').textContent).toBe('Default Table Title');
54
+ });
55
+
56
+ it('should render custom listComponent from schema if no children provided', () => {
57
+ render(<ReferenceManyField reference="with-custom-list" target="post_id" title="Custom List Title" />);
58
+
59
+ expect(screen.getByTestId('custom-list')).toBeDefined();
60
+ expect(screen.getByTestId('custom-list').textContent).toBe('Custom List Title');
61
+ });
62
+
63
+ it('should pass list props to the default component', () => {
33
64
  render(
34
- <ReferenceManyField reference="reviews" target="product_id">
35
- <div data-testid="child">Child Content</div>
36
- </ReferenceManyField>,
65
+ <ReferenceManyField
66
+ reference="comments"
67
+ target="post_id"
68
+ include={['id', 'name']}
69
+ exclude={['date']}
70
+ display={['id']}
71
+ filtering={false}
72
+ preferences={true}
73
+ />,
37
74
  );
38
75
 
39
- expect(screen.getByTestId('resource-schema-provider')).toBeDefined();
76
+ expect(Table).toHaveBeenCalled();
77
+ const props = (Table as any).mock.calls[0][0];
78
+ expect(props).toMatchObject({
79
+ include: ['id', 'name'],
80
+ exclude: ['date'],
81
+ display: ['id'],
82
+ filtering: false,
83
+ preferences: true,
84
+ });
40
85
  });
41
86
  });
@@ -1,11 +1,10 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { ReferenceManyFieldBase, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
2
+ import { ReferenceManyFieldBase, type RaRecord } from '@strato-admin/ra-core';
3
+ import { ResourceSchemaProvider, useResourceSchema } from '@strato-admin/core';
3
4
  import { type FieldProps } from './types';
5
+ import Table from '../list/Table';
4
6
 
5
- export interface ReferenceManyFieldProps<
6
- RecordType extends RaRecord = RaRecord,
7
- ReferenceRecordType extends RaRecord = RaRecord,
8
- > extends FieldProps<RecordType> {
7
+ export interface ReferenceManyFieldProps<RecordType extends RaRecord = RaRecord> extends FieldProps<RecordType> {
9
8
  children?: ReactNode;
10
9
  reference: string;
11
10
  target: string;
@@ -39,6 +38,30 @@ export interface ReferenceManyFieldProps<
39
38
  * @default false
40
39
  */
41
40
  synchronizeWithLocation?: boolean;
41
+ /**
42
+ * Include only these fields from the schema.
43
+ */
44
+ include?: string[];
45
+ /**
46
+ * Exclude these fields from the schema.
47
+ */
48
+ exclude?: string[];
49
+ /**
50
+ * The fields to display by default.
51
+ */
52
+ display?: string[];
53
+ /**
54
+ * Whether to enable text filtering.
55
+ */
56
+ filtering?: boolean;
57
+ /**
58
+ * Whether to show the preferences button or custom preferences content.
59
+ */
60
+ preferences?: boolean | React.ReactNode;
61
+ /**
62
+ * The title of the list.
63
+ */
64
+ title?: React.ReactNode;
42
65
  }
43
66
 
44
67
  /**
@@ -52,22 +75,70 @@ export interface ReferenceManyFieldProps<
52
75
  * <Table.Column source="created_at" />
53
76
  * </Table>
54
77
  * </ReferenceManyField>
78
+ *
79
+ * @example
80
+ * // Honors the default listComponent from the reviews resource schema
81
+ * <ReferenceManyField reference="reviews" target="product_id" />
55
82
  */
56
- export const ReferenceManyField = <
57
- RecordType extends RaRecord = RaRecord,
58
- ReferenceRecordType extends RaRecord = RaRecord,
59
- >(
60
- props: ReferenceManyFieldProps<RecordType, ReferenceRecordType>
83
+ export const ReferenceManyField = <RecordType extends RaRecord = RaRecord>(
84
+ props: ReferenceManyFieldProps<RecordType>,
61
85
  ) => {
62
- const { children, reference, fieldSchema, ...rest } = props;
86
+ const { children, reference, fieldSchema, include, exclude, display, filtering, preferences, title, ...rest } = props;
87
+ const { queryOptions } = useResourceSchema(reference);
63
88
 
64
89
  return (
65
- <ReferenceManyFieldBase reference={reference} {...rest}>
90
+ <ReferenceManyFieldBase reference={reference} queryOptions={queryOptions} {...rest}>
66
91
  <ResourceSchemaProvider resource={reference} fieldSchema={fieldSchema}>
67
- {children}
92
+ <ReferenceManyFieldUI
93
+ reference={reference}
94
+ include={include}
95
+ exclude={exclude}
96
+ display={display}
97
+ filtering={filtering}
98
+ preferences={preferences}
99
+ title={title}
100
+ >
101
+ {children}
102
+ </ReferenceManyFieldUI>
68
103
  </ResourceSchemaProvider>
69
104
  </ReferenceManyFieldBase>
70
105
  );
71
106
  };
72
107
 
108
+ const ReferenceManyFieldUI = ({
109
+ children,
110
+ reference,
111
+ include,
112
+ exclude,
113
+ display,
114
+ filtering,
115
+ preferences,
116
+ title,
117
+ }: {
118
+ children?: ReactNode;
119
+ reference: string;
120
+ include?: string[];
121
+ exclude?: string[];
122
+ display?: string[];
123
+ filtering?: boolean;
124
+ preferences?: boolean | React.ReactNode;
125
+ title?: React.ReactNode;
126
+ }) => {
127
+ const { listComponent: ListComponent = Table } = useResourceSchema(reference);
128
+
129
+ const finalChildren = children || (
130
+ <ListComponent
131
+ include={include}
132
+ exclude={exclude}
133
+ display={display}
134
+ filtering={filtering}
135
+ preferences={preferences}
136
+ title={title}
137
+ />
138
+ );
139
+
140
+ return <>{finalChildren}</>;
141
+ };
142
+
73
143
  export default ReferenceManyField;
144
+ ReferenceManyField.isCollectionField = true;
@@ -1,20 +1,15 @@
1
1
  import { render, cleanup } from '@testing-library/react';
2
2
  import { vi, describe, it, expect, afterEach } from 'vitest';
3
- import { useFieldValue, useRecordContext } from '@strato-admin/core';
3
+ import { useFieldValue, useRecordContext } from '@strato-admin/ra-core';
4
4
  import StatusIndicatorField from './StatusIndicatorField';
5
5
 
6
- // Mock ra-core
6
+ vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
7
7
  vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
8
8
 
9
9
  // Mock Cloudscape components
10
10
  vi.mock('@cloudscape-design/components/status-indicator', () => ({
11
11
  default: ({ children, type, colorOverride, iconAriaLabel }: any) => (
12
- <div
13
- data-testid="status-indicator"
14
- data-type={type}
15
- data-color={colorOverride}
16
- data-aria={iconAriaLabel}
17
- >
12
+ <div data-testid="status-indicator" data-type={type} data-color={colorOverride} data-aria={iconAriaLabel}>
18
13
  {children}
19
14
  </div>
20
15
  ),
@@ -67,10 +62,7 @@ describe('StatusIndicatorField', () => {
67
62
  (useFieldValue as any).mockReturnValue('error');
68
63
 
69
64
  const { getByTestId } = render(
70
- <StatusIndicatorField
71
- source="status"
72
- type={(val) => (val === 'error' ? 'error' : 'info')}
73
- />,
65
+ <StatusIndicatorField source="status" type={(val) => (val === 'error' ? 'error' : 'info')} />,
74
66
  );
75
67
 
76
68
  const indicator = getByTestId('status-indicator');
@@ -90,9 +82,7 @@ describe('StatusIndicatorField', () => {
90
82
  (useRecordContext as any).mockReturnValue({});
91
83
  (useFieldValue as any).mockReturnValue(undefined);
92
84
 
93
- const { getByText } = render(
94
- <StatusIndicatorField source="status" emptyText="No Status" />,
95
- );
85
+ const { getByText } = render(<StatusIndicatorField source="status" emptyText="No Status" />);
96
86
 
97
87
  expect(getByText('No Status')).toBeDefined();
98
88
  });
@@ -102,9 +92,7 @@ describe('StatusIndicatorField', () => {
102
92
  (useRecordContext as any).mockReturnValue(record);
103
93
  (useFieldValue as any).mockReturnValue('custom');
104
94
 
105
- const { getByTestId } = render(
106
- <StatusIndicatorField source="status" type="success" colorOverride="blue" />,
107
- );
95
+ const { getByTestId } = render(<StatusIndicatorField source="status" type="success" colorOverride="blue" />);
108
96
 
109
97
  const indicator = getByTestId('status-indicator');
110
98
  expect(indicator.getAttribute('data-color')).toBe('blue');
@@ -115,9 +103,7 @@ describe('StatusIndicatorField', () => {
115
103
  (useRecordContext as any).mockReturnValue(record);
116
104
  (useFieldValue as any).mockReturnValue('active');
117
105
 
118
- const { getByTestId } = render(
119
- <StatusIndicatorField source="status" iconAriaLabel="Active status" />,
120
- );
106
+ const { getByTestId } = render(<StatusIndicatorField source="status" iconAriaLabel="Active status" />);
121
107
 
122
108
  const indicator = getByTestId('status-indicator');
123
109
  expect(indicator.getAttribute('data-aria')).toBe('Active status');
@@ -1,6 +1,6 @@
1
1
  import StatusIndicator, { type StatusIndicatorProps } from '@cloudscape-design/components/status-indicator';
2
2
  import React, { type ReactElement } from 'react';
3
- import { type RaRecord, useFieldValue, useRecordContext, useTranslate } from '@strato-admin/core';
3
+ import { type RaRecord, useFieldValue, useRecordContext, useTranslate } from '@strato-admin/ra-core';
4
4
  import RecordLink from '../RecordLink';
5
5
  import { type FieldProps } from './types';
6
6
 
@@ -28,8 +28,7 @@ export interface StatusIndicatorLabelProps {
28
28
  */
29
29
  export const StatusIndicatorLabel = (_: StatusIndicatorLabelProps) => null;
30
30
 
31
- export interface StatusIndicatorFieldProps<RecordType extends RaRecord = RaRecord>
32
- extends FieldProps<RecordType> {
31
+ export interface StatusIndicatorFieldProps<RecordType extends RaRecord = RaRecord> extends FieldProps<RecordType> {
33
32
  /**
34
33
  * The type of the status indicator.
35
34
  * If provided as a string, it will be used for all values.
@@ -55,20 +54,8 @@ export interface StatusIndicatorFieldProps<RecordType extends RaRecord = RaRecor
55
54
  children?: React.ReactNode;
56
55
  }
57
56
 
58
- const StatusIndicatorField = <RecordType extends RaRecord = RaRecord>(
59
- props: StatusIndicatorFieldProps<RecordType>
60
- ) => {
61
- const {
62
- source,
63
- record: recordProp,
64
- emptyText,
65
- link,
66
- type,
67
- mapping,
68
- iconAriaLabel,
69
- colorOverride,
70
- children,
71
- } = props;
57
+ const StatusIndicatorField = <RecordType extends RaRecord = RaRecord>(props: StatusIndicatorFieldProps<RecordType>) => {
58
+ const { source, record: recordProp, emptyText, link, type, mapping, iconAriaLabel, colorOverride, children } = props;
72
59
 
73
60
  const record = useRecordContext<RecordType>({ record: recordProp });
74
61
  const value = useFieldValue<RecordType>({ source: source as any, record });
@@ -93,9 +80,10 @@ const StatusIndicatorField = <RecordType extends RaRecord = RaRecord>(
93
80
  finalColorOverride = matchingLabel.props.color;
94
81
  }
95
82
  if (matchingLabel.props.label) {
96
- label = typeof matchingLabel.props.label === 'string'
97
- ? translate(matchingLabel.props.label)
98
- : matchingLabel.props.label;
83
+ label =
84
+ typeof matchingLabel.props.label === 'string'
85
+ ? translate(matchingLabel.props.label)
86
+ : matchingLabel.props.label;
99
87
  }
100
88
  } else if (typeof type === 'function') {
101
89
  statusType = type(value, record as RecordType);
@@ -1,4 +1,4 @@
1
- import { type RaRecord, useFieldValue, useRecordContext } from '@strato-admin/core';
1
+ import { type RaRecord, useFieldValue, useRecordContext } from '@strato-admin/ra-core';
2
2
  import RecordLink from '../RecordLink';
3
3
  import { type FieldProps } from './types';
4
4
 
@@ -1,20 +1,19 @@
1
1
  import { type ReactNode } from 'react';
2
- import { type BaseFieldProps, type RaRecord } from '@strato-admin/core';
2
+ import { ExtractRecordPaths, type BaseFieldProps, type RaRecord } from '@strato-admin/ra-core';
3
3
  import { type RecordLinkType } from '../RecordLink';
4
4
 
5
5
  /**
6
6
  * Common props for all field components in strato-cloudscape.
7
7
  */
8
- export interface FieldProps<RecordType extends RaRecord = RaRecord>
9
- extends Omit<BaseFieldProps<RecordType>, 'source'> {
8
+ export interface FieldProps<RecordType extends RaRecord = RaRecord> extends Omit<BaseFieldProps<RecordType>, 'source'> {
10
9
  /**
11
10
  * The property name in the record that should be displayed.
12
11
  */
13
- source?: string;
12
+ source?: ExtractRecordPaths<RecordType>;
14
13
  /**
15
- * The label to display for this field. Usually inferred from the source.
14
+ * The label to display for this field. If not provided, it will be inferred from the `source`.
16
15
  */
17
- label?: ReactNode;
16
+ label?: string | ReactNode;
18
17
  /**
19
18
  * Whether the field is sortable in a table.
20
19
  * @default true
@@ -22,8 +21,8 @@ export interface FieldProps<RecordType extends RaRecord = RaRecord>
22
21
  sortable?: boolean;
23
22
  /**
24
23
  * Whether to link the field to another page.
25
- * - true: links to the 'edit' page of the current resource
26
- * - 'edit' | 'show': links to the specified page type
24
+ * - true: links to the 'detail' page of the current resource
25
+ * - 'edit' | 'detail': links to the specified page type
27
26
  * - string: a custom URL
28
27
  * - function: (record, resource) => string
29
28
  */
@@ -31,7 +30,7 @@ export interface FieldProps<RecordType extends RaRecord = RaRecord>
31
30
  /**
32
31
  * The text to display if the value is empty or null.
33
32
  */
34
- emptyText?: ReactNode;
33
+ emptyText?: string | ReactNode;
35
34
  /**
36
35
  * Configuration for the inferred form input.
37
36
  * - object: Props passed to the inferred Input component.
@@ -40,8 +39,8 @@ export interface FieldProps<RecordType extends RaRecord = RaRecord>
40
39
  */
41
40
  input?: Record<string, any> | React.ReactElement | false;
42
41
  /**
43
- * Whether the field is required.
44
- * This is used to automatically add validation to the inferred input
42
+ * Whether the field is required.
43
+ * This is used to automatically add validation to the inferred input
45
44
  * and potentially show warnings in display views.
46
45
  */
47
46
  isRequired?: boolean;
@@ -49,10 +48,10 @@ export interface FieldProps<RecordType extends RaRecord = RaRecord>
49
48
  * Additional text to help the user fill in the field.
50
49
  * Passed to the inferred Input component's FormField.
51
50
  */
52
- description?: ReactNode;
51
+ description?: string | ReactNode;
53
52
  /**
54
53
  * Text describing constraints (e.g., "Must be between 1 and 100").
55
54
  * Passed to the inferred Input component's FormField.
56
55
  */
57
- constraintText?: ReactNode;
56
+ constraintText?: string | ReactNode;
58
57
  }
@@ -1,9 +1,10 @@
1
1
  import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
3
4
  import { vi, describe, it, expect, beforeEach } from 'vitest';
4
5
  import { Form } from './Form';
5
6
 
6
- // Mock ra-core
7
+ vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
7
8
  vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
8
9
 
9
10
  // Mock Cloudscape components
@@ -31,14 +32,17 @@ describe('Form', () => {
31
32
 
32
33
  it('should render children and default toolbar', () => {
33
34
  const { getByTestId, getByText } = render(
34
- <Form>
35
- <div data-testid="child" />
36
- </Form>,
35
+ <MemoryRouter>
36
+ <Form>
37
+ <div data-testid="child" />
38
+ </Form>
39
+ </MemoryRouter>,
37
40
  );
38
41
 
39
42
  expect(getByTestId('ra-form')).toBeDefined();
40
43
  expect(getByTestId('cloudscape-form')).toBeDefined();
41
44
  expect(getByTestId('child')).toBeDefined();
45
+ expect(getByText('Cancel')).toBeDefined();
42
46
  expect(getByText('Save')).toBeDefined();
43
47
  });
44
48
 
package/src/form/Form.tsx CHANGED
@@ -1,37 +1,41 @@
1
1
  import React from 'react';
2
- import { Form as RaForm, type FormProps as RaFormProps, useSaveContext, useInputSchema } from '@strato-admin/core';
2
+ import { Form as RaForm, type FormProps as RaFormProps, useSaveContext, useRecordContext } from '@strato-admin/ra-core';
3
+ import { useSchemaFields } from '../hooks/useSchemaFields';
3
4
  import CloudscapeForm from '@cloudscape-design/components/form';
4
5
  import SpaceBetween from '@cloudscape-design/components/space-between';
5
6
  import { SaveButton } from '../button/SaveButton';
7
+ import { CancelButton } from '../button/CancelButton';
6
8
  import { FormField } from '../input/FormField';
7
9
 
8
10
  export interface FormProps extends Omit<RaFormProps, 'children'> {
9
11
  children?: React.ReactNode;
12
+ /**
13
+ * List of field sources to include in the form.
14
+ */
10
15
  include?: string[];
16
+ /**
17
+ * List of field sources to exclude from the form.
18
+ */
11
19
  exclude?: string[];
12
20
  toolbar?: React.ReactNode;
21
+ /**
22
+ * Label for the save button.
23
+ */
24
+ saveButtonLabel?: string;
13
25
  }
14
26
 
15
- export const Form = ({ children, include, exclude, toolbar, ...props }: FormProps) => {
27
+ export const Form = ({ children, include, exclude, toolbar, saveButtonLabel, ...props }: FormProps) => {
16
28
  const saveContext = useSaveContext();
17
- const schemaChildren = useInputSchema();
29
+ const record = useRecordContext();
30
+ const isEditMode = record?.id != null;
18
31
 
19
- const finalChildren = React.useMemo(() => {
20
- const baseChildren = children || schemaChildren;
21
- let result = React.Children.toArray(baseChildren);
32
+ const { getEditFields, getCreateFields } = useSchemaFields();
22
33
 
23
- if (include) {
24
- result = result.filter(
25
- (child) => React.isValidElement(child) && include.includes((child.props as any).source)
26
- );
27
- } else if (exclude) {
28
- result = result.filter(
29
- (child) => React.isValidElement(child) && !exclude.includes((child.props as any).source)
30
- );
31
- }
32
-
33
- return result;
34
- }, [children, schemaChildren, include, exclude]);
34
+ const finalChildren = React.useMemo(
35
+ () =>
36
+ isEditMode ? getEditFields(children, { include, exclude }) : getCreateFields(children, { include, exclude }),
37
+ [isEditMode, getEditFields, getCreateFields, children, include, exclude],
38
+ );
35
39
 
36
40
  const handleSubmit = async (values: any, event: any) => {
37
41
  if (props.onSubmit) {
@@ -48,7 +52,8 @@ export const Form = ({ children, include, exclude, toolbar, ...props }: FormProp
48
52
  actions={
49
53
  toolbar || (
50
54
  <SpaceBetween direction="horizontal" size="xs">
51
- <SaveButton />
55
+ <CancelButton />
56
+ <SaveButton label={saveButtonLabel} />
52
57
  </SpaceBetween>
53
58
  )
54
59
  }
package/src/form/index.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from './Form';
2
- export { ValidationError } from '@strato-admin/core';
2
+ export { ValidationError } from '@strato-admin/ra-core';
@@ -0,0 +1,89 @@
1
+ import React from 'react';
2
+ import { useResourceSchema } from '@strato-admin/core';
3
+
4
+ const noCollectionFields = (child: React.ReactElement) => !(child.type as any).isCollectionField;
5
+
6
+ function filterFields(
7
+ base: React.ReactNode,
8
+ options: {
9
+ include?: string[];
10
+ exclude?: string[];
11
+ defaultFilter?: (child: React.ReactElement) => boolean;
12
+ } = {},
13
+ ): React.ReactElement[] {
14
+ const { include, exclude, defaultFilter } = options;
15
+ let result = React.Children.toArray(base) as React.ReactElement[];
16
+
17
+ if (include) {
18
+ return result.filter((child) => React.isValidElement(child) && include.includes((child.props as any).source));
19
+ }
20
+ if (defaultFilter) {
21
+ result = result.filter((child) => React.isValidElement(child) && defaultFilter(child));
22
+ }
23
+ if (exclude) {
24
+ result = result.filter((child) => React.isValidElement(child) && !exclude.includes((child.props as any).source));
25
+ }
26
+ return result;
27
+ }
28
+
29
+ export function useSchemaFields() {
30
+ const {
31
+ fieldSchema,
32
+ inputSchema,
33
+ listInclude,
34
+ listExclude,
35
+ detailInclude,
36
+ detailExclude,
37
+ formInclude,
38
+ formExclude,
39
+ editInclude,
40
+ editExclude,
41
+ createInclude,
42
+ createExclude,
43
+ } = useResourceSchema();
44
+
45
+ const getListFields = React.useCallback(
46
+ (children: React.ReactNode, opts: { include?: string[]; exclude?: string[] } = {}) =>
47
+ filterFields(children || fieldSchema, {
48
+ include: opts.include || listInclude,
49
+ exclude: opts.exclude || listExclude,
50
+ defaultFilter: noCollectionFields,
51
+ }),
52
+ [fieldSchema, listInclude, listExclude],
53
+ );
54
+
55
+ const getDetailFields = React.useCallback(
56
+ (children: React.ReactNode, opts: { include?: string[]; exclude?: string[] } = {}) => {
57
+ const hasExplicit = React.Children.count(children) > 0;
58
+ const all = filterFields(children || fieldSchema, {
59
+ include: hasExplicit ? undefined : opts.include || detailInclude,
60
+ exclude: hasExplicit ? undefined : opts.exclude || detailExclude,
61
+ });
62
+ return {
63
+ scalarFields: all.filter((c) => !(c.type as any).isCollectionField),
64
+ collectionFields: all.filter((c) => (c.type as any).isCollectionField),
65
+ };
66
+ },
67
+ [fieldSchema, detailInclude, detailExclude],
68
+ );
69
+
70
+ const getEditFields = React.useCallback(
71
+ (children: React.ReactNode, opts: { include?: string[]; exclude?: string[] } = {}) =>
72
+ filterFields(children || inputSchema, {
73
+ include: opts.include || editInclude || formInclude,
74
+ exclude: opts.exclude || editExclude || formExclude,
75
+ }),
76
+ [inputSchema, editInclude, editExclude, formInclude, formExclude],
77
+ );
78
+
79
+ const getCreateFields = React.useCallback(
80
+ (children: React.ReactNode, opts: { include?: string[]; exclude?: string[] } = {}) =>
81
+ filterFields(children || inputSchema, {
82
+ include: opts.include || createInclude || formInclude,
83
+ exclude: opts.exclude || createExclude || formExclude,
84
+ }),
85
+ [inputSchema, createInclude, createExclude, formInclude, formExclude],
86
+ );
87
+
88
+ return { getListFields, getDetailFields, getEditFields, getCreateFields };
89
+ }
@@ -0,0 +1,22 @@
1
+ import { useTranslate } from '@strato-admin/ra-core';
2
+ import { MessageProps } from './types';
3
+
4
+ export type { MessageProps };
5
+
6
+ /**
7
+ * Renders a translatable string inline. The children text is the English source
8
+ * string and serves as both the translation key and the fallback value.
9
+ *
10
+ * @example
11
+ * <Message>Product Name</Message>
12
+ * <Message id="resources.products.actions.archive">Archive</Message>
13
+ * <Message context="verb">Archive</Message>
14
+ * <Message context="noun">Archive</Message>
15
+ * <Message comment="Appears in the confirmation dialog">Are you sure?</Message>
16
+ * <Message count={n}>{'Found {count, plural, one {# result} other {# results}}'}</Message>
17
+ */
18
+ export const Message = ({ children, id, context, comment: _comment, vars }: MessageProps) => {
19
+ const translate = useTranslate();
20
+ const key = id ?? (context ? `${context}\x04${children}` : children);
21
+ return <>{translate(key, { ...vars, _: children })}</>;
22
+ };
@@ -0,0 +1,22 @@
1
+ import { useTranslate, useRecordContext } from '@strato-admin/ra-core';
2
+ import { MessageProps } from './types';
3
+
4
+ export interface RecordMessageProps extends MessageProps {
5
+ /** Explicit record to use instead of the one from context. */
6
+ record?: Record<string, any>;
7
+ }
8
+
9
+ /**
10
+ * Renders a translatable string with all fields of the current record available
11
+ * as ICU variables. Must be used inside a record context (Detail, Edit, etc.).
12
+ *
13
+ * @example
14
+ * <RecordMessage>{'Product: {name}'}</RecordMessage>
15
+ * <RecordMessage>{'Order #{id} — {status}'}</RecordMessage>
16
+ */
17
+ export const RecordMessage = ({ children, id, context, comment: _comment, record, vars }: RecordMessageProps) => {
18
+ const translate = useTranslate();
19
+ const resolvedRecord = useRecordContext({ record }) ?? {};
20
+ const key = id ?? (context ? `${context}\x04${children}` : children);
21
+ return <>{translate(key, { ...resolvedRecord, ...vars, _: children })}</>;
22
+ };
@@ -0,0 +1,3 @@
1
+ export { type MessageProps } from './types';
2
+ export { Message } from './Message';
3
+ export { RecordMessage, type RecordMessageProps } from './RecordMessage';