@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,11 +1,11 @@
1
-
2
1
  import React from 'react';
3
- import { ReferenceInputBase, type ReferenceInputBaseProps } from '@strato-admin/core';
4
- import { useFormFieldContext } from './FormFieldContext';
2
+ import { ReferenceInputBase, type ReferenceInputBaseProps, useInput } from '@strato-admin/ra-core';
3
+ import { FormField } from './FormField';
4
+ import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
5
5
  import { AutocompleteInput } from './AutocompleteInput';
6
6
 
7
7
  export const ReferenceInput = (props: ReferenceInputBaseProps) => {
8
- const { children, source: sourceProp, reference, ...rest } = props;
8
+ const { children, source: sourceProp, reference, isRequired, validate, defaultValue, label, ...rest } = props;
9
9
  const context = useFormFieldContext();
10
10
 
11
11
  // If we have a context, we use the source from it.
@@ -15,22 +15,40 @@ export const ReferenceInput = (props: ReferenceInputBaseProps) => {
15
15
  throw new Error('ReferenceInput requires a source prop or a parent FormField Master');
16
16
  }
17
17
 
18
+ const inputState =
19
+ context ??
20
+ useInput({
21
+ source,
22
+ defaultValue,
23
+ validate,
24
+ isRequired,
25
+ ...rest,
26
+ });
27
+
18
28
  const finalChildren = children || <AutocompleteInput source={source} />;
19
29
 
20
30
  const inner = (
21
- <ReferenceInputBase source={source} reference={reference} {...rest}>
22
- {React.isValidElement(finalChildren)
23
- ? React.cloneElement(finalChildren as React.ReactElement<any>, {
24
- source,
25
- })
26
- : (finalChildren as any)}
31
+ <ReferenceInputBase source={source} reference={reference} isRequired={isRequired} {...rest}>
32
+ <FormFieldContext.Provider value={inputState}>
33
+ {React.isValidElement(finalChildren)
34
+ ? React.cloneElement(finalChildren as React.ReactElement<any>, {
35
+ source,
36
+ isRequired,
37
+ })
38
+ : (finalChildren as any)}
39
+ </FormFieldContext.Provider>
27
40
  </ReferenceInputBase>
28
41
  );
29
42
 
30
- // ReferenceInput is unique because it's a wrapper.
31
- // It doesn't use FormFieldContext for its state directly (ReferenceInputBase does),
32
- // but it needs to ensure its children can consume the state it provides via ReferenceInputBase.
33
- return inner;
43
+ if (context) {
44
+ return inner;
45
+ }
46
+
47
+ return (
48
+ <FormFieldContext.Provider value={inputState}>
49
+ <FormField {...(props as any)}>{inner}</FormField>
50
+ </FormFieldContext.Provider>
51
+ );
34
52
  };
35
53
 
36
54
  export default ReferenceInput;
@@ -1,29 +1,23 @@
1
-
2
1
  import React from 'react';
3
- import {
4
- useInput,
5
- useResourceContext,
6
- useChoicesContext,
7
- useGetRecordRepresentation,
8
- } from '@strato-admin/core';
2
+ import { useInput, useResourceContext, useChoicesContext, useGetRecordRepresentation } from '@strato-admin/ra-core';
9
3
  import CloudscapeSelect, { SelectProps as CloudscapeSelectProps } from '@cloudscape-design/components/select';
10
4
  import { FormField } from './FormField';
11
5
  import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
12
6
  import { InputProps } from './types';
13
7
 
14
8
  export interface SelectInputProps
15
- extends Omit<CloudscapeSelectProps, 'onChange' | 'selectedOption' | 'options' | 'onBlur'>,
16
- InputProps {
17
- choices?: Array<{ id: string | number; [key: string]: any }>;
18
- /**
19
- * The text to display for the empty option when isRequired is false.
20
- * @default "-"
21
- */
22
- emptyText?: string;
9
+ extends InputProps,
10
+ Pick<CloudscapeSelectProps, 'filteringType' | 'placeholder' | 'disabled' | 'readOnly'> {
11
+ choices?: Array<{ id: string | number; [key: string]: any }>;
12
+ /**
13
+ * The text to display for the empty option when isRequired is false.
14
+ * @default "-"
15
+ */
16
+ emptyText?: string;
23
17
  }
24
18
 
25
19
  export const SelectInput = (props: SelectInputProps) => {
26
- const { label, source, defaultValue, validate, choices: choicesProp, emptyText = '-', ...rest } = props;
20
+ const { label, source, defaultValue, validate, choices: choicesProp, emptyText = '-', filteringType, placeholder, disabled, readOnly, ...rest } = props;
27
21
  const resource = useResourceContext();
28
22
  const { allChoices, isPending } = useChoicesContext(props);
29
23
  const getRecordRepresentation = useGetRecordRepresentation(resource);
@@ -63,8 +57,11 @@ export const SelectInput = (props: SelectInputProps) => {
63
57
 
64
58
  const inner = (
65
59
  <CloudscapeSelect
66
- {...rest}
67
60
  id={id}
61
+ filteringType={filteringType}
62
+ placeholder={placeholder}
63
+ disabled={disabled}
64
+ readOnly={readOnly}
68
65
  options={options}
69
66
  selectedOption={selectedOption}
70
67
  statusType={isPending ? 'loading' : 'finished'}
@@ -1,11 +1,10 @@
1
1
  import React from 'react';
2
2
  import { render, fireEvent, cleanup } from '@testing-library/react';
3
3
  import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
4
- import { useInput, useResourceContext } from '@strato-admin/core';
4
+ import { useInput, useResourceContext } from '@strato-admin/ra-core';
5
5
  import { SliderInput } from './SliderInput';
6
6
 
7
- // Mock strato-core
8
- vi.mock('@strato-admin/core', () => ({
7
+ vi.mock('@strato-admin/ra-core', () => ({
9
8
  useInput: vi.fn(),
10
9
  useResourceContext: vi.fn(),
11
10
  useTranslate: () => (key: string) => key,
@@ -1,49 +1,59 @@
1
- import React from 'react';
2
- import { useInput } from '@strato-admin/core';
1
+ import { useInput } from '@strato-admin/ra-core';
3
2
  import CloudscapeSlider, { SliderProps as CloudscapeSliderProps } from '@cloudscape-design/components/slider';
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 SliderInputProps
9
- extends Omit<CloudscapeSliderProps, 'onChange' | 'value' | 'i18nStrings'>,
10
- InputProps {}
8
+ extends InputProps,
9
+ Partial<Pick<CloudscapeSliderProps, 'min' | 'max'>>,
10
+ Pick<CloudscapeSliderProps, 'step' | 'disabled' | 'readOnly' | 'valueFormatter' | 'tickMarks' | 'referenceValues' | 'hideFillLine' | 'ariaLabel' | 'ariaDescription'> {}
11
11
 
12
12
  export const SliderInput = (props: SliderInputProps) => {
13
- const { label, source, defaultValue, validate, ...rest } = props;
14
- const context = useFormFieldContext();
15
- const inputState =
16
- context ??
17
- useInput({
18
- source,
19
- defaultValue,
20
- validate,
21
- ...rest,
22
- });
23
-
24
- const { id, field } = inputState;
25
-
26
- // Cloudscape Slider requires a number value
27
- const value = typeof field.value === 'number' ? field.value : props.min ?? 0;
28
-
29
- const inner = (
30
- <CloudscapeSlider
31
- {...(rest as any)}
32
- id={id}
33
- value={value}
34
- onChange={(event) => field.onChange(event.detail.value)}
35
- />
36
- );
37
-
38
- if (context) {
39
- return inner;
40
- }
41
-
42
- return (
43
- <FormFieldContext.Provider value={inputState}>
44
- <FormField {...props}>{inner}</FormField>
45
- </FormFieldContext.Provider>
46
- );
13
+ const { label, source, defaultValue, validate, min, max, step, disabled, readOnly, valueFormatter, tickMarks, referenceValues, hideFillLine, ariaLabel, ariaDescription, ...rest } = props;
14
+ const context = useFormFieldContext();
15
+ const inputState =
16
+ context ??
17
+ useInput({
18
+ source,
19
+ defaultValue,
20
+ validate,
21
+ ...rest,
22
+ });
23
+
24
+ const { id, field } = inputState;
25
+
26
+ // Cloudscape Slider requires a number value
27
+ const value = typeof field.value === 'number' ? field.value : (min ?? 0);
28
+
29
+ const inner = (
30
+ <CloudscapeSlider
31
+ id={id}
32
+ min={min ?? 0}
33
+ max={max ?? 100}
34
+ step={step}
35
+ disabled={disabled}
36
+ readOnly={readOnly}
37
+ valueFormatter={valueFormatter}
38
+ tickMarks={tickMarks}
39
+ referenceValues={referenceValues}
40
+ hideFillLine={hideFillLine}
41
+ ariaLabel={ariaLabel}
42
+ ariaDescription={ariaDescription}
43
+ value={value}
44
+ onChange={(event) => field.onChange(event.detail.value)}
45
+ />
46
+ );
47
+
48
+ if (context) {
49
+ return inner;
50
+ }
51
+
52
+ return (
53
+ <FormFieldContext.Provider value={inputState}>
54
+ <FormField {...props}>{inner}</FormField>
55
+ </FormFieldContext.Provider>
56
+ );
47
57
  };
48
58
 
49
59
  export default SliderInput;
@@ -1,16 +1,15 @@
1
-
2
- import { useInput } from '@strato-admin/core';
1
+ import { useInput } from '@strato-admin/ra-core';
3
2
  import CloudscapeTextarea, { TextareaProps as CloudscapeTextareaProps } from '@cloudscape-design/components/textarea';
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 TextAreaInputProps
9
- extends Omit<CloudscapeTextareaProps, 'onChange' | 'value' | 'onBlur'>,
10
- InputProps {}
8
+ extends InputProps,
9
+ Pick<CloudscapeTextareaProps, 'placeholder' | 'disabled' | 'readOnly' | 'rows' | 'autoFocus' | 'spellcheck'> {}
11
10
 
12
11
  export const TextAreaInput = (props: TextAreaInputProps) => {
13
- const { label, source, defaultValue, validate, ...rest } = props;
12
+ const { label, source, defaultValue, validate, placeholder, disabled, readOnly, rows, autoFocus, spellcheck, ...rest } = props;
14
13
  const context = useFormFieldContext();
15
14
  const inputState =
16
15
  context ??
@@ -25,9 +24,14 @@ export const TextAreaInput = (props: TextAreaInputProps) => {
25
24
 
26
25
  const inner = (
27
26
  <CloudscapeTextarea
28
- {...rest}
29
27
  {...field}
30
28
  id={id}
29
+ placeholder={placeholder}
30
+ disabled={disabled}
31
+ readOnly={readOnly}
32
+ rows={rows}
33
+ autoFocus={autoFocus}
34
+ spellcheck={spellcheck}
31
35
  value={field.value || ''}
32
36
  onChange={(event) => field.onChange(event.detail.value)}
33
37
  onBlur={() => field.onBlur()}
@@ -1,11 +1,9 @@
1
-
2
1
  import { render } from '@testing-library/react';
3
2
  import { vi, describe, it, expect, beforeEach } from 'vitest';
4
- import { useInput, useResourceContext } from '@strato-admin/core';
3
+ import { useInput, useResourceContext } from '@strato-admin/ra-core';
5
4
  import { TextInput } from './TextInput';
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
  useTranslate: () => (key: string) => key,
@@ -1,36 +1,49 @@
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 TextInputProps
9
- extends Omit<CloudscapeInputProps, 'onChange' | 'value' | 'onBlur' | 'type'>,
10
- InputProps {
11
- type?: CloudscapeInputProps['type'];
8
+ extends InputProps,
9
+ Pick<CloudscapeInputProps, 'placeholder' | 'disabled' | 'readOnly' | 'autoFocus' | 'autoComplete' | 'spellcheck' | 'inputMode'> {
10
+ type?: CloudscapeInputProps['type'];
12
11
  }
13
12
 
14
13
  export const TextInput = (props: TextInputProps) => {
15
- const { label, source, defaultValue, validate, type = 'text', ...rest } = props;
16
- const context = useFormFieldContext();
17
- const inputState =
18
- context ??
19
- useInput({
20
- source,
21
- defaultValue,
22
- validate,
23
- ...rest,
24
- });
25
-
26
- const { id, field } = inputState;
14
+ const {
15
+ label,
16
+ source,
17
+ defaultValue,
18
+ validate,
19
+ description,
20
+ constraintText,
21
+ info,
22
+ secondaryControl,
23
+ type = 'text',
24
+ placeholder,
25
+ disabled,
26
+ readOnly,
27
+ autoFocus,
28
+ autoComplete,
29
+ spellcheck,
30
+ inputMode,
27
31
 
32
+ } = props;
33
+ const context = useFormFieldContext();
34
+ const inputState = context ?? useInput({ source, defaultValue, validate });
35
+ const { field } = inputState;
28
36
  const inner = (
29
37
  <CloudscapeInput
30
- {...rest}
31
38
  {...field}
32
- id={id}
33
39
  type={type}
40
+ placeholder={placeholder}
41
+ disabled={disabled}
42
+ readOnly={readOnly}
43
+ autoFocus={autoFocus}
44
+ autoComplete={autoComplete}
45
+ spellcheck={spellcheck}
46
+ inputMode={inputMode}
34
47
  value={field.value || ''}
35
48
  onChange={(event) => field.onChange(event.detail.value)}
36
49
  onBlur={() => field.onBlur()}
@@ -43,7 +56,9 @@ export const TextInput = (props: TextInputProps) => {
43
56
 
44
57
  return (
45
58
  <FormFieldContext.Provider value={inputState}>
46
- <FormField {...props}>{inner}</FormField>
59
+ <FormField source={source} label={label} description={description} constraintText={constraintText} info={info} secondaryControl={secondaryControl}>
60
+ {inner}
61
+ </FormField>
47
62
  </FormFieldContext.Provider>
48
63
  );
49
64
  };
@@ -1,8 +1,9 @@
1
1
  export * from './types';
2
+ export * from './BooleanInput';
2
3
  export * from './TextInput';
3
4
  export * from './TextAreaInput';
4
5
  export * from './NumberInput';
5
- export * from './AttributeEditor';
6
+ export * from './ArrayInput';
6
7
  export * from './SelectInput';
7
8
  export * from './AutocompleteInput';
8
9
  export * from './ReferenceInput';
@@ -1,14 +1,46 @@
1
- import { ReactNode } from 'react';
2
- import { InputProps as InputPropsBase } from '@strato-admin/core';
1
+ import React from 'react';
2
+ import { InputProps as InputPropsBase, Validator } from '@strato-admin/ra-core';
3
3
  import { FormFieldProps as CloudscapeFormFieldProps } from '@cloudscape-design/components/form-field';
4
4
 
5
- export interface StratoInputProps<T = any>
6
- extends Omit<InputPropsBase<T>, 'label'>,
7
- Pick<
8
- CloudscapeFormFieldProps,
9
- 'description' | 'constraintText' | 'info' | 'secondaryControl' | 'stretch' | 'i18nStrings'
10
- > {
5
+ /**
6
+ * Common props shared by all Strato Admin input components.
7
+ */
8
+ export interface StratoCommonInputProps<T = any> {
9
+ /** The field name in the record. Used to read and write the value. */
10
+ source: string;
11
+ /** Override the auto-generated label. Pass `false` to hide the label entirely. */
11
12
  label?: string | false;
13
+ /** The initial value when no record value exists. */
14
+ defaultValue?: any;
15
+ /** Validation rule or array of rules. Use built-in validators (`required()`, `minLength()`, etc.) or provide a custom function. */
16
+ validate?: Validator | Validator[];
17
+ /** Transforms the record value before passing it to the input (record → input). */
18
+ format?: (value: T) => any;
19
+ /** Transforms the input value before saving to the record (input → record). */
20
+ parse?: (value: any) => T;
21
+ /** When `true`, the value is visible but cannot be changed. */
22
+ readOnly?: boolean;
23
+ /** When `true`, the input is non-interactive and grayed out. */
24
+ disabled?: boolean;
25
+ /** Helper text displayed below the label. */
26
+ description?: React.ReactNode;
27
+ /** Additional constraint text. Appended with "(optional)" when the field is not required. */
28
+ constraintText?: React.ReactNode;
29
+ /** Info link displayed next to the label (Cloudscape `info` slot). */
30
+ info?: React.ReactNode;
31
+ /** Secondary control displayed to the right of the input (Cloudscape `secondaryControl` slot). */
32
+ secondaryControl?: React.ReactNode;
33
+ /** When `true`, the form field stretches to fill its container width. */
34
+ stretch?: boolean;
12
35
  }
13
36
 
37
+ export interface StratoInputProps<T = any>
38
+ extends
39
+ StratoCommonInputProps<T>,
40
+ Omit<
41
+ InputPropsBase<T>,
42
+ 'label' | 'source' | 'defaultValue' | 'validate' | 'format' | 'parse' | 'readOnly' | 'disabled'
43
+ >,
44
+ Pick<CloudscapeFormFieldProps, 'i18nStrings'> {}
45
+
14
46
  export type InputProps<T = any> = StratoInputProps<T>;
@@ -2,10 +2,11 @@ import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import { vi, describe, it, expect, beforeEach } from 'vitest';
4
4
  import { AppLayout } from './AppLayout';
5
- import { useDefaultTitle, useResourceDefinitions } from '@strato-admin/core';
5
+ import { useDefaultTitle, useResourceDefinitions } from '@strato-admin/ra-core';
6
6
  import { TopNavigation } from './TopNavigation';
7
+ import SideNavigationMock from '@cloudscape-design/components/side-navigation';
7
8
 
8
- // Mock strato-core
9
+ vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
9
10
  vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
10
11
 
11
12
  // Mock global-styles
@@ -35,7 +36,7 @@ vi.mock('@cloudscape-design/components/app-layout', () => ({
35
36
  }));
36
37
 
37
38
  vi.mock('@cloudscape-design/components/side-navigation', () => ({
38
- default: () => <div data-testid="side-navigation" />,
39
+ default: vi.fn(() => <div data-testid="side-navigation" />),
39
40
  }));
40
41
 
41
42
  describe('AppLayout', () => {
@@ -43,6 +44,25 @@ describe('AppLayout', () => {
43
44
  vi.clearAllMocks();
44
45
  });
45
46
 
47
+ it('should only show resources with hasList: true in SideNavigation', () => {
48
+ (useResourceDefinitions as any).mockReturnValue({
49
+ posts: { name: 'posts', hasList: true },
50
+ comments: { name: 'comments', hasList: false },
51
+ users: { name: 'users', hasList: true },
52
+ });
53
+
54
+ render(
55
+ <AppLayout>
56
+ <div>Content</div>
57
+ </AppLayout>,
58
+ );
59
+
60
+ const sideNavProps = vi.mocked(SideNavigationMock).mock.calls[0][0];
61
+ expect(sideNavProps.items).toHaveLength(2);
62
+ expect(sideNavProps.items[0]).toMatchObject({ text: 'posts', href: '/posts' });
63
+ expect(sideNavProps.items[1]).toMatchObject({ text: 'users', href: '/users' });
64
+ });
65
+
46
66
  it('should use title from useDefaultTitle hook if no title prop provided', () => {
47
67
  (useDefaultTitle as any).mockReturnValue('Hook Title');
48
68
 
@@ -1,9 +1,10 @@
1
1
  import React, { useState } from 'react';
2
2
  import CloudscapeAppLayout from '@cloudscape-design/components/app-layout';
3
3
  import SideNavigation from '@cloudscape-design/components/side-navigation';
4
- import { useResourceDefinitions, useDefaultTitle, useGetResourceLabel } from '@strato-admin/core';
4
+ import { useResourceDefinitions, useDefaultTitle, useGetResourceLabel } from '@strato-admin/ra-core';
5
5
  import { useNavigate } from 'react-router-dom';
6
6
  import { TopNavigation } from './TopNavigation';
7
+ import { Notifications } from './Notifications';
7
8
  import ThemeManager from '../theme/ThemeManager';
8
9
 
9
10
  export interface AppLayoutProps {
@@ -19,14 +20,15 @@ export const AppLayout = ({ children, header, title }: AppLayoutProps) => {
19
20
  const defaultTitle = useDefaultTitle();
20
21
  const [navigationOpen, setNavigationOpen] = useState(true);
21
22
 
22
- const appTitle =
23
- title ?? (typeof defaultTitle === 'string' ? defaultTitle : '');
23
+ const appTitle = title ?? (typeof defaultTitle === 'string' ? defaultTitle : '');
24
24
 
25
- const items = Object.values(resources).map((resource) => ({
26
- type: 'link' as const,
27
- text: getResourceLabel(resource.name),
28
- href: `/${resource.name}`,
29
- }));
25
+ const items = Object.values(resources)
26
+ .filter((resource) => resource.hasList)
27
+ .map((resource) => ({
28
+ type: 'link' as const,
29
+ text: getResourceLabel(resource.name),
30
+ href: `/${resource.name}`,
31
+ }));
30
32
 
31
33
  return (
32
34
  <>
@@ -51,6 +53,7 @@ export const AppLayout = ({ children, header, title }: AppLayoutProps) => {
51
53
  }}
52
54
  />
53
55
  }
56
+ notifications={<Notifications />}
54
57
  content={<div>{children}</div>}
55
58
  />
56
59
  </>
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import { Notifications } from './Notifications';
5
+ import { useNotificationContext, useTranslate } from '@strato-admin/ra-core';
6
+
7
+ vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
8
+
9
+ vi.mock('@cloudscape-design/components/flashbar', () => ({
10
+ default: ({ items }: any) => (
11
+ <div data-testid="flashbar">
12
+ {items.map((item: any) => (
13
+ <div key={item.id} data-testid={`flash-item-${item.id}`} data-type={item.type}>
14
+ <span data-testid="flash-content">{item.content}</span>
15
+ {item.dismissible && (
16
+ <button data-testid={`dismiss-${item.id}`} onClick={item.onDismiss}>
17
+ Dismiss
18
+ </button>
19
+ )}
20
+ </div>
21
+ ))}
22
+ </div>
23
+ ),
24
+ }));
25
+
26
+ describe('Notifications', () => {
27
+ beforeEach(() => {
28
+ vi.clearAllMocks();
29
+ });
30
+
31
+ it('renders nothing when there are no notifications', () => {
32
+ (useNotificationContext as any).mockReturnValue({ notifications: [], setNotifications: vi.fn() });
33
+ const { container } = render(<Notifications />);
34
+ expect(container.firstChild).toBeNull();
35
+ });
36
+
37
+ it('renders a Flashbar item for each notification', () => {
38
+ const n1 = { message: 'Saved', type: 'success' as const };
39
+ const n2 = { message: 'Error occurred', type: 'error' as const };
40
+ (useNotificationContext as any).mockReturnValue({
41
+ notifications: [n1, n2],
42
+ setNotifications: vi.fn(),
43
+ });
44
+ render(<Notifications />);
45
+ expect(screen.getByTestId('flashbar')).toBeDefined();
46
+ expect(screen.getAllByTestId('flash-content')).toHaveLength(2);
47
+ });
48
+
49
+ it('passes the correct type to each Flashbar item', () => {
50
+ const types = ['success', 'info', 'warning', 'error'] as const;
51
+ const notifications = types.map((type) => ({ message: type, type }));
52
+ (useNotificationContext as any).mockReturnValue({ notifications, setNotifications: vi.fn() });
53
+ render(<Notifications />);
54
+ const items = screen.getAllByTestId(/^flash-item-/);
55
+ expect(items.map((el) => el.getAttribute('data-type'))).toEqual(types);
56
+ });
57
+
58
+ it('translates string messages', () => {
59
+ (useTranslate as any).mockReturnValue((key: string) => `translated:${key}`);
60
+ (useNotificationContext as any).mockReturnValue({
61
+ notifications: [{ message: 'ra.action.save', type: 'success' as const }],
62
+ setNotifications: vi.fn(),
63
+ });
64
+ render(<Notifications />);
65
+ expect(screen.getByTestId('flash-content').textContent).toBe('translated:ra.action.save');
66
+ });
67
+
68
+ it('calls setNotifications to remove the dismissed notification', () => {
69
+ const notification = { message: 'Done', type: 'success' as const };
70
+ const setNotifications = vi.fn();
71
+ (useNotificationContext as any).mockReturnValue({
72
+ notifications: [notification],
73
+ setNotifications,
74
+ });
75
+ render(<Notifications />);
76
+ const dismissButtons = screen.getAllByTestId(/^dismiss-/);
77
+ fireEvent.click(dismissButtons[0]);
78
+ expect(setNotifications).toHaveBeenCalled();
79
+ const updater = setNotifications.mock.calls[0][0];
80
+ const result = updater([notification, { message: 'Other', type: 'info' as const }]);
81
+ expect(result).toHaveLength(1);
82
+ expect(result[0].message).toBe('Other');
83
+ });
84
+
85
+ it('does not auto-hide when autoHideDuration is null', () => {
86
+ vi.useFakeTimers();
87
+ const notification = {
88
+ message: 'Persistent',
89
+ type: 'info' as const,
90
+ notificationOptions: { autoHideDuration: null },
91
+ };
92
+ const setNotifications = vi.fn();
93
+ (useNotificationContext as any).mockReturnValue({
94
+ notifications: [notification],
95
+ setNotifications,
96
+ });
97
+ render(<Notifications />);
98
+ vi.advanceTimersByTime(10000);
99
+ expect(setNotifications).not.toHaveBeenCalled();
100
+ vi.useRealTimers();
101
+ });
102
+ });