@strato-admin/cloudscape 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +4 -1
  7. package/dist/button/BulkDeleteButton.js +37 -5
  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 +9 -6
  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 +45 -8
  133. package/src/button/BulkDeleteButton.tsx +75 -12
  134. package/src/button/Button.tsx +31 -2
  135. package/src/button/CancelButton.tsx +20 -0
  136. package/src/button/CreateButton.tsx +12 -10
  137. package/src/button/DeleteButton.tsx +96 -0
  138. package/src/button/EditButton.tsx +13 -12
  139. package/src/button/SaveButton.tsx +2 -3
  140. package/src/button/index.ts +2 -0
  141. package/src/collection-hooks/interfaces.ts +7 -3
  142. package/src/collection-hooks/useCollection.test.ts +115 -2
  143. package/src/collection-hooks/useCollection.ts +15 -10
  144. package/src/create/Create.test.tsx +3 -3
  145. package/src/create/Create.tsx +68 -37
  146. package/src/create/CreateHeader.tsx +6 -10
  147. package/src/defaults.tsx +28 -0
  148. package/src/detail/Detail-CollectionFields.test.tsx +84 -0
  149. package/src/detail/Detail.test.tsx +91 -0
  150. package/src/detail/Detail.tsx +48 -0
  151. package/src/detail/{ShowHeader.test.tsx → DetailHeader.test.tsx} +11 -9
  152. package/src/detail/DetailHeader.tsx +42 -0
  153. package/src/detail/DetailHub.tsx +88 -0
  154. package/src/detail/KeyValuePairs.test.tsx +2 -2
  155. package/src/detail/KeyValuePairs.tsx +25 -18
  156. package/src/detail/index.ts +3 -2
  157. package/src/edit/Edit.test.tsx +7 -5
  158. package/src/edit/Edit.tsx +92 -40
  159. package/src/edit/EditHeader.tsx +7 -5
  160. package/src/field/ArrayField.tsx +57 -11
  161. package/src/field/BadgeField.tsx +2 -3
  162. package/src/field/BooleanField.test.tsx +2 -3
  163. package/src/field/BooleanField.tsx +3 -3
  164. package/src/field/CurrencyField.tsx +1 -1
  165. package/src/field/DateField.tsx +1 -1
  166. package/src/field/IdField.test.tsx +8 -20
  167. package/src/field/IdField.tsx +5 -20
  168. package/src/field/NumberField.tsx +1 -1
  169. package/src/field/ReferenceField.test.tsx +15 -6
  170. package/src/field/ReferenceField.tsx +10 -7
  171. package/src/field/ReferenceManyField.test.tsx +55 -10
  172. package/src/field/ReferenceManyField.tsx +84 -13
  173. package/src/field/StatusIndicatorField.test.tsx +7 -21
  174. package/src/field/StatusIndicatorField.tsx +8 -20
  175. package/src/field/TextField.tsx +1 -1
  176. package/src/field/types.ts +12 -13
  177. package/src/form/Form.test.tsx +8 -4
  178. package/src/form/Form.tsx +24 -19
  179. package/src/form/index.ts +1 -1
  180. package/src/hooks/useSchemaFields.ts +89 -0
  181. package/src/i18n/Message.tsx +22 -0
  182. package/src/i18n/RecordMessage.tsx +22 -0
  183. package/src/i18n/index.ts +3 -0
  184. package/src/i18n/types.ts +19 -0
  185. package/src/index.ts +5 -1
  186. package/src/input/ArrayInput.test.tsx +81 -0
  187. package/src/input/{AttributeEditor.tsx → ArrayInput.tsx} +36 -18
  188. package/src/input/AutocompleteInput.test.tsx +2 -4
  189. package/src/input/AutocompleteInput.tsx +9 -11
  190. package/src/input/BooleanInput.tsx +42 -0
  191. package/src/input/CommonInputProps.tsx +8 -0
  192. package/src/input/FieldTitle.tsx +3 -15
  193. package/src/input/FormField.tsx +78 -67
  194. package/src/input/FormFieldContext.ts +1 -1
  195. package/src/input/NumberInput.tsx +10 -7
  196. package/src/input/ReferenceInput.test.tsx +12 -2
  197. package/src/input/ReferenceInput.tsx +32 -14
  198. package/src/input/SelectInput.tsx +14 -17
  199. package/src/input/SliderInput.test.tsx +2 -3
  200. package/src/input/SliderInput.tsx +48 -38
  201. package/src/input/TextAreaInput.tsx +10 -6
  202. package/src/input/TextInput.test.tsx +2 -4
  203. package/src/input/TextInput.tsx +35 -20
  204. package/src/input/index.ts +2 -1
  205. package/src/input/types.ts +40 -8
  206. package/src/layout/AppLayout.test.tsx +23 -3
  207. package/src/layout/AppLayout.tsx +11 -8
  208. package/src/layout/Notifications.test.tsx +102 -0
  209. package/src/layout/Notifications.tsx +61 -0
  210. package/src/layout/Ready.tsx +123 -0
  211. package/src/layout/TopNavigation.test.tsx +2 -3
  212. package/src/layout/TopNavigation.tsx +9 -8
  213. package/src/layout/index.ts +2 -0
  214. package/src/list/Cards.test.tsx +320 -0
  215. package/src/list/Cards.tsx +146 -16
  216. package/src/list/List.tsx +87 -26
  217. package/src/list/Table.test.tsx +40 -5
  218. package/src/list/Table.tsx +89 -98
  219. package/src/list/TableHeader.test.tsx +15 -11
  220. package/src/list/TableHeader.tsx +6 -8
  221. package/src/theme/ThemeManager.tsx +1 -1
  222. package/dist/__mocks__/strato-core.js +0 -50
  223. package/dist/__mocks__to__delete/strato-core.js +0 -50
  224. package/dist/detail/Show.d.ts +0 -39
  225. package/dist/detail/Show.js +0 -40
  226. package/dist/detail/ShowHeader.d.ts +0 -7
  227. package/dist/input/AttributeEditor.d.ts +0 -25
  228. package/src/detail/Show.test.tsx +0 -96
  229. package/src/detail/Show.tsx +0 -104
  230. package/src/detail/ShowHeader.tsx +0 -35
  231. package/src/input/AttributeEditor.test.tsx +0 -147
@@ -0,0 +1,19 @@
1
+ export interface MessageProps {
2
+ children: string;
3
+ /**
4
+ * Explicit stable message ID. Used directly as the lookup key — bypasses
5
+ * hashing entirely. Takes priority over `context`. Written as `#. id: <id>`
6
+ * in PO files so translators see the stable identifier alongside the source text.
7
+ */
8
+ id?: string;
9
+ /**
10
+ * Disambiguation context. Prepended to the message before hashing so that
11
+ * the same string in different contexts produces different translation entries.
12
+ * Ignored when `id` is provided. Stored as `msgctxt` in PO files.
13
+ */
14
+ context?: string;
15
+ /** Translator note written into the PO file as a `#` comment. Ignored at runtime. */
16
+ comment?: string;
17
+ /** ICU variables substituted into the message at runtime. */
18
+ vars?: Record<string, any>;
19
+ }
package/src/index.ts CHANGED
@@ -11,7 +11,9 @@ export * from './form';
11
11
  export * from './layout';
12
12
  export * from './theme';
13
13
  export * from './button';
14
+ export * from './i18n';
14
15
  export * from './Admin';
16
+ export * from './Settings';
15
17
  export { default as RecordLink } from './RecordLink';
16
18
  export * from './RecordLink';
17
19
 
@@ -21,5 +23,7 @@ export { Form, type FormProps } from './form';
21
23
  export { List, type ListProps } from './list';
22
24
  export { Create, type CreateProps } from './create';
23
25
  export { Edit, type EditProps } from './edit';
24
- export { Show, type ShowProps } from './detail';
26
+ export { Detail, type DetailProps, Detail as Show, type DetailProps as ShowProps } from './detail';
25
27
  export { type InputProps } from './input';
28
+ export { Ready, AppLayout, TopNavigation } from './layout';
29
+ export { Button } from './button';
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { useForm, FormProvider } from 'react-hook-form';
5
+ import { ArrayInput } from './ArrayInput';
6
+ import TextInput from './TextInput';
7
+
8
+ const TestWrapper = ({ children }: { children: React.ReactNode }) => {
9
+ const methods = useForm({
10
+ defaultValues: {
11
+ items: [{ name: 'Item 1' }, { name: 'Item 2' }],
12
+ },
13
+ });
14
+ return <FormProvider {...methods}>{children}</FormProvider>;
15
+ };
16
+
17
+ describe('ArrayInput', () => {
18
+ it('should render items from default values', () => {
19
+ render(
20
+ <TestWrapper>
21
+ <ArrayInput source="items">
22
+ <TextInput source="name" label="Name" />
23
+ </ArrayInput>
24
+ </TestWrapper>,
25
+ );
26
+
27
+ expect(screen.getByDisplayValue('Item 1')).toBeDefined();
28
+ expect(screen.getByDisplayValue('Item 2')).toBeDefined();
29
+ });
30
+
31
+ it('should add a new item when clicking the add button', async () => {
32
+ render(
33
+ <TestWrapper>
34
+ <ArrayInput source="items" addButtonText="Add new one">
35
+ <TextInput source="name" label="Name" />
36
+ </ArrayInput>
37
+ </TestWrapper>,
38
+ );
39
+
40
+ const addButton = screen.getByText('Add new one');
41
+ fireEvent.click(addButton);
42
+
43
+ // AttributeEditor might have some internal delay or we need to wait for react-hook-form
44
+ await waitFor(() => {
45
+ const inputs = screen.getAllByRole('textbox');
46
+ expect(inputs).toHaveLength(3);
47
+ });
48
+ });
49
+
50
+ it('should remove an item when clicking the remove button', async () => {
51
+ render(
52
+ <TestWrapper>
53
+ <ArrayInput source="items" removeButtonText="Remove this">
54
+ <TextInput source="name" label="Name" />
55
+ </ArrayInput>
56
+ </TestWrapper>,
57
+ );
58
+
59
+ const removeButtons = screen.getAllByText('Remove this');
60
+ fireEvent.click(removeButtons[0]);
61
+
62
+ await waitFor(() => {
63
+ const inputs = screen.getAllByRole('textbox');
64
+ expect(inputs).toHaveLength(1);
65
+ });
66
+ });
67
+
68
+ it('should support gridLayout prop', () => {
69
+ // This is mostly a smoke test to ensure the prop doesn't crash
70
+ const gridLayout = [{ rows: [[1]] }];
71
+ render(
72
+ <TestWrapper>
73
+ <ArrayInput source="items" gridLayout={gridLayout}>
74
+ <TextInput source="name" label="Name" />
75
+ </ArrayInput>
76
+ </TestWrapper>,
77
+ );
78
+
79
+ expect(screen.getByDisplayValue('Item 1')).toBeDefined();
80
+ });
81
+ });
@@ -1,7 +1,9 @@
1
1
  import React, { useMemo } from 'react';
2
- import { useInput, RecordContextProvider, useResourceContext } from '@strato-admin/core';
2
+ import { useInput, RecordContextProvider, useResourceContext } from '@strato-admin/ra-core';
3
3
  import { useFieldArray, useFormContext } from 'react-hook-form';
4
- import CloudscapeAttributeEditor from '@cloudscape-design/components/attribute-editor';
4
+ import CloudscapeAttributeEditor, {
5
+ AttributeEditorProps as CloudscapeAttributeEditorProps,
6
+ } from '@cloudscape-design/components/attribute-editor';
5
7
  import Box from '@cloudscape-design/components/box';
6
8
  import { FieldTitle } from './FieldTitle';
7
9
  import TextInput from './TextInput';
@@ -9,7 +11,7 @@ import FormField from './FormField';
9
11
  import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
10
12
  import { InputProps } from './types';
11
13
 
12
- export interface AttributeEditorItemProps {
14
+ export interface ArrayInputItemProps {
13
15
  source: string;
14
16
  label?: string | false;
15
17
  field?: React.ComponentType<any>;
@@ -18,23 +20,30 @@ export interface AttributeEditorItemProps {
18
20
  children?: React.ReactNode;
19
21
  }
20
22
 
21
- export const Item = (_props: AttributeEditorItemProps) => {
23
+ export const Item = (_props: ArrayInputItemProps) => {
22
24
  // This is a placeholder component used to collect props.
23
- // The actual rendering is handled by the AttributeEditor.
25
+ // The actual rendering is handled by the ArrayInput.
24
26
  return null;
25
27
  };
26
28
 
27
- export interface AttributeEditorProps extends Omit<InputProps, 'source'> {
29
+ export interface ArrayInputProps extends Omit<InputProps, 'source'> {
28
30
  source?: string;
29
- children: React.ReactNode;
31
+ children?: React.ReactNode;
30
32
  addButtonText?: string;
31
33
  removeButtonText?: string;
32
34
  empty?: React.ReactNode;
33
35
  disableAddButton?: boolean;
34
36
  hideAddButton?: boolean;
37
+ /**
38
+ * Optionally specifies the layout of the attributes.
39
+ */
40
+ gridLayout?: ReadonlyArray<CloudscapeAttributeEditorProps.GridLayout>;
35
41
  }
36
42
 
37
- export const AttributeEditor = (props: AttributeEditorProps) => {
43
+ /**
44
+ * ArrayInput component that uses Cloudscape's AttributeEditor to edit arrays of objects.
45
+ */
46
+ export const ArrayInput = (props: ArrayInputProps) => {
38
47
  const {
39
48
  children,
40
49
  label,
@@ -46,6 +55,7 @@ export const AttributeEditor = (props: AttributeEditorProps) => {
46
55
  empty,
47
56
  disableAddButton,
48
57
  hideAddButton,
58
+ gridLayout,
49
59
  ...rest
50
60
  } = props;
51
61
 
@@ -86,14 +96,25 @@ export const AttributeEditor = (props: AttributeEditorProps) => {
86
96
  const childSource = childProps.source;
87
97
  const childLabel = childProps.label;
88
98
  const childValidate = childProps.validate;
99
+ const childDescription = childProps.description;
100
+ const childInfo = childProps.info;
89
101
 
90
102
  // Determine if the field is required by checking validators
91
103
  const isRequired = Array.isArray(childValidate)
92
- ? childValidate.some((v: any) => v.isRequired)
93
- : childValidate?.isRequired || childProps.isRequired;
104
+ ? childValidate.some((v: any) => v.isRequired || v.name === 'required')
105
+ : childValidate?.isRequired || childValidate?.name === 'required' || childProps.isRequired;
94
106
 
95
107
  return {
96
- label: <FieldTitle label={childLabel} source={childSource} resource={resource} isRequired={isRequired} />,
108
+ label: (
109
+ <FieldTitle
110
+ label={childLabel}
111
+ source={childSource}
112
+ resource={childProps.resource || resource}
113
+ isRequired={isRequired}
114
+ />
115
+ ),
116
+ description: childDescription,
117
+ info: childInfo,
97
118
  control: (item: any, index: number) => {
98
119
  const prefixedSource = `${source}.${index}.${childSource}`;
99
120
 
@@ -135,11 +156,7 @@ export const AttributeEditor = (props: AttributeEditorProps) => {
135
156
  );
136
157
  }
137
158
 
138
- return (
139
- <RecordContextProvider value={item}>
140
- <Box padding={{ top: 's' }}>{content}</Box>
141
- </RecordContextProvider>
142
- );
159
+ return <RecordContextProvider value={item}>{content}</RecordContextProvider>;
143
160
  },
144
161
  };
145
162
  })?.filter(Boolean) as any[];
@@ -163,6 +180,7 @@ export const AttributeEditor = (props: AttributeEditorProps) => {
163
180
  removeButtonText={removeButtonText}
164
181
  disableAddButton={disableAddButton}
165
182
  hideAddButton={hideAddButton}
183
+ gridLayout={gridLayout}
166
184
  />
167
185
  </FormFieldContext.Provider>
168
186
  );
@@ -180,6 +198,6 @@ export const AttributeEditor = (props: AttributeEditorProps) => {
180
198
  );
181
199
  };
182
200
 
183
- AttributeEditor.Item = Item;
201
+ ArrayInput.Item = Item;
184
202
 
185
- export default AttributeEditor;
203
+ export default ArrayInput;
@@ -1,11 +1,9 @@
1
-
2
1
  import { render, fireEvent } from '@testing-library/react';
3
2
  import { vi, describe, it, expect, beforeEach } from 'vitest';
4
- import { useInput, useResourceContext, useChoicesContext } from '@strato-admin/core';
3
+ import { useInput, useResourceContext, useChoicesContext } from '@strato-admin/ra-core';
5
4
  import { AutocompleteInput } from './AutocompleteInput';
6
5
 
7
- // Mock ra-core
8
- vi.mock('@strato-admin/core', () => ({
6
+ vi.mock('@strato-admin/ra-core', () => ({
9
7
  useInput: vi.fn(),
10
8
  useResourceContext: vi.fn(),
11
9
  useChoicesContext: vi.fn().mockReturnValue({ allChoices: [], isPending: false }),
@@ -1,10 +1,5 @@
1
1
  import React, { useState, useEffect, useMemo } from 'react';
2
- import {
3
- useInput,
4
- useResourceContext,
5
- useChoicesContext,
6
- useGetRecordRepresentation,
7
- } from '@strato-admin/core';
2
+ import { useInput, useResourceContext, useChoicesContext, useGetRecordRepresentation } from '@strato-admin/ra-core';
8
3
  import CloudscapeAutosuggest, {
9
4
  AutosuggestProps as CloudscapeAutosuggestProps,
10
5
  } from '@cloudscape-design/components/autosuggest';
@@ -13,13 +8,13 @@ import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
13
8
  import { InputProps } from './types';
14
9
 
15
10
  export interface AutocompleteInputProps
16
- extends Omit<CloudscapeAutosuggestProps, 'onChange' | 'value' | 'options' | 'onBlur'>,
17
- InputProps {
18
- choices?: Array<{ id: string | number; [key: string]: any }>;
11
+ extends InputProps,
12
+ Pick<CloudscapeAutosuggestProps, 'placeholder' | 'disabled' | 'readOnly' | 'enteredTextLabel'> {
13
+ choices?: Array<{ id: string | number; [key: string]: any }>;
19
14
  }
20
15
 
21
16
  export const AutocompleteInput = (props: AutocompleteInputProps) => {
22
- const { label, source, defaultValue, validate, choices: choicesProp, ...rest } = props;
17
+ const { label, source, defaultValue, validate, choices: choicesProp, placeholder, disabled, readOnly, enteredTextLabel, ...rest } = props;
23
18
  const resource = useResourceContext();
24
19
  const { allChoices, isPending, setFilters } = useChoicesContext(props);
25
20
  const getRecordRepresentation = useGetRecordRepresentation(resource);
@@ -90,8 +85,11 @@ export const AutocompleteInput = (props: AutocompleteInputProps) => {
90
85
 
91
86
  const inner = (
92
87
  <CloudscapeAutosuggest
93
- {...rest}
94
88
  id={id}
89
+ placeholder={placeholder}
90
+ disabled={disabled}
91
+ readOnly={readOnly}
92
+ enteredTextLabel={enteredTextLabel}
95
93
  options={options}
96
94
  value={filterValue}
97
95
  statusType={isPending ? 'loading' : 'finished'}
@@ -0,0 +1,42 @@
1
+ import { useInput } from '@strato-admin/ra-core';
2
+ import Toggle, { ToggleProps } from '@cloudscape-design/components/toggle';
3
+ import { FormField } from './FormField';
4
+ import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
5
+ import { InputProps } from './types';
6
+
7
+ export interface BooleanInputProps
8
+ extends InputProps,
9
+ Pick<ToggleProps, 'disabled' | 'readOnly' | 'children' | 'ariaLabel'> {}
10
+
11
+ export const BooleanInput = (props: BooleanInputProps) => {
12
+ const { label, source, defaultValue, validate, disabled, readOnly, children, ariaLabel, ...rest } = props;
13
+ const context = useFormFieldContext();
14
+ const inputState =
15
+ context ??
16
+ useInput({
17
+ source,
18
+ defaultValue: defaultValue ?? false,
19
+ validate,
20
+ ...rest,
21
+ });
22
+
23
+ const { field } = inputState;
24
+
25
+ const inner = (
26
+ <Toggle disabled={disabled} readOnly={readOnly} ariaLabel={ariaLabel} checked={!!field.value} onChange={(event) => field.onChange(event.detail.checked)}>
27
+ {children}
28
+ </Toggle>
29
+ );
30
+
31
+ if (context) {
32
+ return inner;
33
+ }
34
+
35
+ return (
36
+ <FormFieldContext.Provider value={inputState}>
37
+ <FormField {...props}>{inner}</FormField>
38
+ </FormFieldContext.Provider>
39
+ );
40
+ };
41
+
42
+ export default BooleanInput;
@@ -0,0 +1,8 @@
1
+ import { StratoCommonInputProps } from './types';
2
+
3
+ /**
4
+ * @internal Stub component for react-docgen-typescript to extract StratoCommonInputProps.
5
+ * Not intended for direct use.
6
+ */
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ export const CommonInputProps = (_props: StratoCommonInputProps) => null;
@@ -1,6 +1,5 @@
1
-
2
1
  import React from 'react';
3
- import { useTranslate, useResourceDefinitions } from '@strato-admin/core';
2
+ import { useTranslate, useResourceDefinitions } from '@strato-admin/ra-core';
4
3
  import { humanize } from 'inflection';
5
4
 
6
5
  export interface FieldTitleProps {
@@ -11,7 +10,7 @@ export interface FieldTitleProps {
11
10
  }
12
11
 
13
12
  export const FieldTitle = (props: FieldTitleProps) => {
14
- const { resource, source, label, isRequired } = props;
13
+ const { resource, source, label } = props;
15
14
  const translate = useTranslate();
16
15
  const definitions = useResourceDefinitions();
17
16
 
@@ -36,18 +35,7 @@ export const FieldTitle = (props: FieldTitleProps) => {
36
35
  });
37
36
  }, [label, translate, resource, source, definitions]);
38
37
 
39
- return (
40
- <span>
41
- {labelString}
42
- {!isRequired && (
43
- <span
44
- style={{ color: '#687078', fontWeight: 'normal', fontStyle: 'italic', fontSize: '12px', marginLeft: '4px' }}
45
- >
46
- (optional)
47
- </span>
48
- )}
49
- </span>
50
- );
38
+ return <span>{labelString}</span>;
51
39
  };
52
40
 
53
41
  export default FieldTitle;
@@ -1,87 +1,98 @@
1
-
2
1
  import React from 'react';
3
- import { useInput, useResourceContext, ValidationError } from '@strato-admin/core';
2
+ import { useInput, useResourceContext, ValidationError, useTranslate } from '@strato-admin/ra-core';
4
3
  import CloudscapeFormField from '@cloudscape-design/components/form-field';
5
4
  import { FieldTitle } from './FieldTitle';
6
5
  import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
7
6
  import { InputProps } from './types';
8
7
 
9
8
  export interface FormFieldProps extends InputProps {
10
- children: React.ReactNode;
9
+ children: React.ReactNode;
11
10
  }
12
11
 
13
12
  export const FormField = (props: FormFieldProps) => {
14
- const {
15
- children,
16
- label,
17
- source,
18
- defaultValue,
19
- validate,
20
- description,
21
- constraintText,
22
- info,
23
- secondaryControl,
24
- stretch,
25
- i18nStrings,
26
- ...rest
27
- } = props;
28
- const resource = useResourceContext();
29
- const context = useFormFieldContext();
30
- const inputState =
31
- context ??
32
- useInput({
33
- source,
34
- defaultValue,
35
- validate,
36
- ...rest,
37
- });
13
+ const {
14
+ children,
15
+ label,
16
+ source,
17
+ defaultValue,
18
+ validate,
19
+ description,
20
+ constraintText,
21
+ info,
22
+ secondaryControl,
23
+ stretch,
24
+ i18nStrings,
25
+ ...rest
26
+ } = props;
27
+ const resource = useResourceContext();
28
+ const translate = useTranslate();
29
+ const context = useFormFieldContext();
30
+ const inputState =
31
+ context ??
32
+ useInput({
33
+ source,
34
+ defaultValue,
35
+ validate,
36
+ ...rest,
37
+ });
38
38
 
39
- const contextValue = React.useMemo(() => {
40
- if (!inputState) return undefined;
41
- return {
42
- ...inputState,
43
- source: source || context?.source || '',
44
- };
45
- }, [inputState, source, context?.source]);
39
+ const contextValue = React.useMemo(() => {
40
+ if (!inputState) return undefined;
41
+ return {
42
+ ...inputState,
43
+ source: source || context?.source || '',
44
+ };
45
+ }, [inputState, source, context?.source]);
46
46
 
47
- const {
48
- id,
49
- fieldState: { isTouched, invalid, error },
50
- formState: { isSubmitted },
51
- isRequired,
52
- } = inputState;
47
+ const {
48
+ id,
49
+ fieldState: { isTouched, invalid, error },
50
+ formState: { isSubmitted },
51
+ isRequired,
52
+ } = inputState;
53
53
 
54
- const errorToProcess = (error as any)?.message || (error as any)?.root?.message;
55
- const errorText =
56
- (isTouched || isSubmitted) && invalid && typeof errorToProcess === 'string' ? (
57
- <ValidationError error={errorToProcess} />
58
- ) : undefined;
54
+ const errorToProcess = (error as any)?.message || (error as any)?.root?.message;
55
+ const errorText =
56
+ (isTouched || isSubmitted) && invalid && typeof errorToProcess === 'string' ? (
57
+ <ValidationError error={errorToProcess} />
58
+ ) : undefined;
59
59
 
60
- const content = (
61
- <CloudscapeFormField
62
- id={id}
63
- label={
64
- label === false ? undefined : (
65
- <FieldTitle label={label} source={source} resource={resource} isRequired={isRequired} />
66
- )
67
- }
68
- description={description}
69
- constraintText={constraintText}
70
- info={info}
71
- secondaryControl={secondaryControl}
72
- stretch={stretch}
73
- i18nStrings={i18nStrings}
74
- errorText={errorText}
75
- >
76
- {children}
77
- </CloudscapeFormField>
60
+ const finalConstraintText = React.useMemo(() => {
61
+ if (isRequired) return constraintText;
62
+ const optionalLabel = `(${translate('optional')})`;
63
+ if (!constraintText) return optionalLabel;
64
+ return (
65
+ <>
66
+ {constraintText} ({optionalLabel})
67
+ </>
78
68
  );
69
+ }, [isRequired, constraintText, translate]);
70
+
71
+ const content = (
72
+ <CloudscapeFormField
73
+ id={id}
74
+ label={
75
+ label === false ? undefined : (
76
+ <FieldTitle label={label} source={source} resource={resource} isRequired={isRequired} />
77
+ )
78
+ }
79
+ description={label === false ? undefined : description}
80
+ constraintText={label === false ? undefined : finalConstraintText}
81
+ info={info}
82
+ secondaryControl={secondaryControl}
83
+ stretch={stretch}
84
+ i18nStrings={i18nStrings}
85
+ errorText={errorText}
86
+ >
87
+ {children}
88
+ </CloudscapeFormField>
89
+ );
79
90
 
80
- if (context) {
81
- return content;
82
- }
91
+ if (context) {
92
+ return content;
93
+ }
83
94
 
84
- return <FormFieldContext.Provider value={contextValue}>{content}</FormFieldContext.Provider>;
95
+ return <FormFieldContext.Provider value={contextValue}>{content}</FormFieldContext.Provider>;
85
96
  };
86
97
 
87
98
  export default FormField;
@@ -1,5 +1,5 @@
1
1
  import { createContext, useContext } from 'react';
2
- import { UseInputValue } from '@strato-admin/core';
2
+ import { UseInputValue } from '@strato-admin/ra-core';
3
3
 
4
4
  export interface FormFieldContextValue extends UseInputValue {
5
5
  source?: string;
@@ -1,18 +1,17 @@
1
-
2
- import { useInput } from '@strato-admin/core';
1
+ import { useInput } from '@strato-admin/ra-core';
3
2
  import CloudscapeInput, { InputProps as CloudscapeInputProps } from '@cloudscape-design/components/input';
4
3
  import { FormField } from './FormField';
5
4
  import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
6
5
  import { InputProps } from './types';
7
6
 
8
7
  export interface NumberInputProps
9
- extends Omit<CloudscapeInputProps, 'onChange' | 'value' | 'onBlur' | 'type'>,
10
- InputProps {
11
- type?: CloudscapeInputProps['type'];
8
+ extends InputProps,
9
+ Pick<CloudscapeInputProps, 'placeholder' | 'disabled' | 'readOnly' | 'autoFocus' | 'step'> {
10
+ type?: CloudscapeInputProps['type'];
12
11
  }
13
12
 
14
13
  export const NumberInput = (props: NumberInputProps) => {
15
- const { label, source, defaultValue, validate, type = 'number', ...rest } = props;
14
+ const { label, source, defaultValue, validate, type = 'number', placeholder, disabled, readOnly, autoFocus, step, ...rest } = props;
16
15
  const context = useFormFieldContext();
17
16
  const inputState =
18
17
  context ??
@@ -32,10 +31,14 @@ export const NumberInput = (props: NumberInputProps) => {
32
31
 
33
32
  const inner = (
34
33
  <CloudscapeInput
35
- {...rest}
36
34
  {...field}
37
35
  id={id}
38
36
  type={type}
37
+ placeholder={placeholder}
38
+ disabled={disabled}
39
+ readOnly={readOnly}
40
+ autoFocus={autoFocus}
41
+ step={step}
39
42
  value={field.value?.toString() || ''}
40
43
  onChange={(event) => handleChange(event.detail.value)}
41
44
  onBlur={() => field.onBlur()}
@@ -1,11 +1,21 @@
1
-
2
1
  import { render } from '@testing-library/react';
3
2
  import { vi, describe, it, expect } from 'vitest';
4
3
  import { ReferenceInput } from './ReferenceInput';
5
4
 
6
5
  // Mock ra-core
7
- vi.mock('@strato-admin/core', () => ({
6
+ vi.mock('@strato-admin/ra-core', () => ({
8
7
  ReferenceInputBase: ({ children }: any) => <div data-testid="reference-input-base">{children}</div>,
8
+ useInput: vi.fn(() => ({
9
+ id: 'test',
10
+ field: { value: '', onChange: vi.fn(), onBlur: vi.fn() },
11
+ fieldState: { isTouched: false, invalid: false, error: null },
12
+ formState: { isSubmitted: false },
13
+ isRequired: false,
14
+ })),
15
+ useResourceContext: vi.fn(() => 'products'),
16
+ useTranslate: vi.fn(() => (key: string) => key),
17
+ useResourceDefinitions: vi.fn(() => ({})),
18
+ ValidationError: ({ error }: any) => <span>{error}</span>,
9
19
  }));
10
20
 
11
21
  describe('ReferenceInput', () => {