@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
@@ -0,0 +1,96 @@
1
+ import { useState } from 'react';
2
+ import { useDeleteController, useTranslate, useResourceDefinition, RaRecord } from '@strato-admin/ra-core';
3
+ import { useSettingValue } from '@strato-admin/core';
4
+ import Modal from '@cloudscape-design/components/modal';
5
+ import Box from '@cloudscape-design/components/box';
6
+ import SpaceBetween from '@cloudscape-design/components/space-between';
7
+ import { Button } from './Button';
8
+
9
+ export interface DeleteButtonProps {
10
+ label?: string;
11
+ variant?: 'primary' | 'normal' | 'link';
12
+ mutationMode?: 'undoable' | 'optimistic' | 'pessimistic';
13
+ successMessage?: string;
14
+ dialogTitle?: string;
15
+ dialogDescription?: string;
16
+ record?: RaRecord;
17
+ redirect?: string;
18
+ }
19
+
20
+ export const DeleteButton = ({
21
+ label,
22
+ variant = 'normal',
23
+ mutationMode,
24
+ successMessage,
25
+ dialogTitle,
26
+ dialogDescription,
27
+ record,
28
+ redirect,
29
+ }: DeleteButtonProps) => {
30
+ const translate = useTranslate();
31
+ const { options } = useResourceDefinition();
32
+ const resolve = useSettingValue();
33
+ const { handleDelete, isPending, isLoading } = useDeleteController({
34
+ mutationMode: resolve(mutationMode, 'mutationMode'),
35
+ successMessage: successMessage ?? options?.deleteSuccessMessage ?? resolve(undefined, 'deleteSuccessMessage'),
36
+ record,
37
+ redirect,
38
+ });
39
+
40
+ const [isOpen, setIsOpen] = useState(false);
41
+
42
+ if (options?.canDelete === false) {
43
+ return null;
44
+ }
45
+
46
+ const isBusy = isPending || isLoading;
47
+
48
+ const handleConfirm = () => {
49
+ handleDelete();
50
+ setIsOpen(false);
51
+ };
52
+
53
+ const defaultTitle = translate('strato.message.delete_title', {
54
+ _: 'Delete this item',
55
+ });
56
+
57
+ const defaultDescription = translate('strato.message.delete_content', {
58
+ _: 'Are you sure you want to delete this item?',
59
+ });
60
+
61
+ return (
62
+ <>
63
+ <Button
64
+ variant={variant}
65
+ onClick={() => setIsOpen(true)}
66
+ loading={isBusy}
67
+ disabled={isBusy}
68
+ data-testid="delete-button"
69
+ >
70
+ {label || translate('strato.action.delete', { _: 'Delete' })}
71
+ </Button>
72
+ <Modal
73
+ onDismiss={() => setIsOpen(false)}
74
+ visible={isOpen}
75
+ closeAriaLabel={translate('strato.action.close', { _: 'Close' })}
76
+ footer={
77
+ <Box float="right">
78
+ <SpaceBetween direction="horizontal" size="xs">
79
+ <Button variant="link" onClick={() => setIsOpen(false)}>
80
+ {translate('strato.action.cancel', { _: 'Cancel' })}
81
+ </Button>
82
+ <Button variant="primary" onClick={handleConfirm} loading={isBusy} data-testid="confirm-delete">
83
+ {translate('strato.action.confirm', { _: 'Confirm' })}
84
+ </Button>
85
+ </SpaceBetween>
86
+ </Box>
87
+ }
88
+ header={dialogTitle || defaultTitle}
89
+ >
90
+ {dialogDescription || defaultDescription}
91
+ </Modal>
92
+ </>
93
+ );
94
+ };
95
+
96
+ export default DeleteButton;
@@ -1,14 +1,15 @@
1
-
2
- import { useResourceContext, useRecordContext, useTranslate, useCreatePath, RaRecord } from '@strato-admin/core';
1
+ import { useResourceContext, useRecordContext, useTranslate, RaRecord } from '@strato-admin/ra-core';
2
+ import { useCreatePath } from '@strato-admin/core';
3
3
  import { useNavigate } from 'react-router-dom';
4
4
  import { Button, ButtonProps } from './Button';
5
+ import type { ButtonProps as CloudscapeButtonProps } from '@cloudscape-design/components/button';
5
6
 
6
7
  export interface EditButtonProps extends Omit<ButtonProps, 'children'> {
7
8
  label?: string;
8
9
  record?: RaRecord;
9
10
  }
10
11
 
11
- export const EditButton = ({ label, record: recordProp, variant = 'primary', ...props }: EditButtonProps) => {
12
+ export const EditButton = ({ label, record: recordProp, variant = 'normal', ...props }: EditButtonProps) => {
12
13
  const resource = useResourceContext();
13
14
  const record = useRecordContext(recordProp);
14
15
  const translate = useTranslate();
@@ -19,18 +20,18 @@ export const EditButton = ({ label, record: recordProp, variant = 'primary', ...
19
20
  return null;
20
21
  }
21
22
 
22
- const handleClick = () => {
23
- const path = createPath({
24
- resource,
25
- id: record.id,
26
- type: 'edit',
27
- });
28
- navigate(path);
23
+ const path = createPath({ resource, id: record.id, type: 'edit' });
24
+
25
+ const handleClick: NonNullable<CloudscapeButtonProps['onClick']> = (e) => {
26
+ if (!e.detail.metaKey && !e.detail.ctrlKey && !e.detail.shiftKey && e.detail.button === 0) {
27
+ e.preventDefault();
28
+ navigate(path);
29
+ }
29
30
  };
30
31
 
31
32
  return (
32
- <Button variant={variant} onClick={handleClick} {...props}>
33
- {label || translate('ra.action.edit', { _: 'Edit' })}
33
+ <Button variant={variant} href={path} onClick={handleClick} iconName="edit" {...props}>
34
+ {label || translate('strato.action.edit', { _: 'Edit' })}
34
35
  </Button>
35
36
  );
36
37
  };
@@ -1,5 +1,4 @@
1
-
2
- import { useTranslate } from '@strato-admin/core';
1
+ import { useTranslate } from '@strato-admin/ra-core';
3
2
  import { Button, ButtonProps } from './Button';
4
3
 
5
4
  export interface SaveButtonProps extends Omit<ButtonProps, 'children'> {
@@ -11,7 +10,7 @@ export const SaveButton = ({ label, variant = 'primary', ...props }: SaveButtonP
11
10
 
12
11
  return (
13
12
  <Button variant={variant} formAction="submit" nativeButtonAttributes={{ type: 'submit' }} {...props}>
14
- {label || translate('ra.action.save', { _: 'Save' })}
13
+ {label || translate('strato.action.save', { _: 'Save' })}
15
14
  </Button>
16
15
  );
17
16
  };
@@ -1,5 +1,7 @@
1
1
  export * from './Button';
2
2
  export * from './EditButton';
3
3
  export * from './BulkDeleteButton';
4
+ export * from './DeleteButton';
4
5
  export * from './SaveButton';
5
6
  export * from './CreateButton';
7
+ export * from './CancelButton';
@@ -18,9 +18,13 @@ export interface CollectionPreferences {
18
18
  }
19
19
 
20
20
  export interface UseCollectionOptions<_T> {
21
- filtering?: any;
22
- pagination?: any;
23
- sorting?: any;
21
+ /**
22
+ * Set to true when using a client-side data provider where the data array
23
+ * contains ALL records, not just the current page. The hook will slice the
24
+ * array for pagination instead of relying on the server.
25
+ * @default false
26
+ */
27
+ clientSidePagination?: boolean;
24
28
  preferences?: {
25
29
  pageSizeOptions?: ReadonlyArray<{ value: number; label?: string }>;
26
30
  visibleContentOptions?: ReadonlyArray<{
@@ -1,9 +1,9 @@
1
1
  import { renderHook, act } from '@testing-library/react';
2
2
  import { vi, describe, it, expect } from 'vitest';
3
- import { useListContext } from '@strato-admin/core';
3
+ import { useListContext } from '@strato-admin/ra-core';
4
4
  import { useCollection } from './useCollection';
5
5
 
6
- vi.mock('@strato-admin/core', () => ({
6
+ vi.mock('@strato-admin/ra-core', () => ({
7
7
  useListContext: vi.fn(),
8
8
  }));
9
9
 
@@ -375,6 +375,32 @@ describe('useCollection', () => {
375
375
  expect(result.current.preferencesProps.preferences.contentDisplay).toEqual(contentDisplay);
376
376
  });
377
377
 
378
+ it('should re-sync visibleContent when options.preferences.visibleContent prop changes after mount', () => {
379
+ (useListContext as any).mockReturnValue({
380
+ data: [],
381
+ perPage: 25,
382
+ setPerPage: vi.fn(),
383
+ page: 1,
384
+ isPending: false,
385
+ isFetching: false,
386
+ isLoading: false,
387
+ setPage: vi.fn(),
388
+ selectedIds: [],
389
+ onSelect: vi.fn(),
390
+ });
391
+
392
+ const { result, rerender } = renderHook(
393
+ ({ visibleContent }: { visibleContent: ReadonlyArray<string> }) =>
394
+ useCollection({ preferences: { visibleContent } }),
395
+ { initialProps: { visibleContent: ['id'] } },
396
+ );
397
+
398
+ expect(result.current.preferencesProps.preferences.visibleContent).toEqual(['id']);
399
+
400
+ rerender({ visibleContent: ['id', 'name'] });
401
+ expect(result.current.preferencesProps.preferences.visibleContent).toEqual(['id', 'name']);
402
+ });
403
+
378
404
  it('should update wrapLines and stripedRows when preferences onConfirm is called', () => {
379
405
  (useListContext as any).mockReturnValue({
380
406
  data: [],
@@ -411,3 +437,90 @@ describe('useCollection', () => {
411
437
  expect(result.current.preferencesProps.preferences.stripedRows).toBe(true);
412
438
  });
413
439
  });
440
+
441
+ describe('useCollection — client-side pagination', () => {
442
+ const makeData = (count: number) => Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }));
443
+
444
+ const mockContext = (overrides: object) =>
445
+ (useListContext as any).mockReturnValue({
446
+ data: [],
447
+ total: undefined,
448
+ page: 1,
449
+ perPage: 25,
450
+ isPending: false,
451
+ isFetching: false,
452
+ isLoading: false,
453
+ setPage: vi.fn(),
454
+ setPerPage: vi.fn(),
455
+ selectedIds: [],
456
+ onSelect: vi.fn(),
457
+ sort: undefined,
458
+ setSort: vi.fn(),
459
+ filterValues: {},
460
+ setFilters: vi.fn(),
461
+ ...overrides,
462
+ });
463
+
464
+ it('should return all data unsliced when clientSidePagination is false (default)', () => {
465
+ const data = makeData(10);
466
+ mockContext({ data, total: 10, page: 1, perPage: 3 });
467
+
468
+ const { result } = renderHook(() => useCollection({}));
469
+
470
+ expect(result.current.items).toEqual(data);
471
+ });
472
+
473
+ it('should return the first page slice when clientSidePagination is true', () => {
474
+ const data = makeData(10);
475
+ mockContext({ data, total: 10, page: 1, perPage: 3 });
476
+
477
+ const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
478
+
479
+ expect(result.current.items).toEqual([
480
+ { id: 1, name: 'Item 1' },
481
+ { id: 2, name: 'Item 2' },
482
+ { id: 3, name: 'Item 3' },
483
+ ]);
484
+ });
485
+
486
+ it('should return the correct slice for a middle page', () => {
487
+ const data = makeData(10);
488
+ mockContext({ data, total: 10, page: 2, perPage: 3 });
489
+
490
+ const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
491
+
492
+ expect(result.current.items).toEqual([
493
+ { id: 4, name: 'Item 4' },
494
+ { id: 5, name: 'Item 5' },
495
+ { id: 6, name: 'Item 6' },
496
+ ]);
497
+ });
498
+
499
+ it('should return the remaining items on the last partial page', () => {
500
+ const data = makeData(10);
501
+ mockContext({ data, total: 10, page: 4, perPage: 3 });
502
+
503
+ const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
504
+
505
+ expect(result.current.items).toEqual([{ id: 10, name: 'Item 10' }]);
506
+ });
507
+
508
+ it('should NOT slice when a server-side provider returns all items in one response', () => {
509
+ // 30 items, perPage=25 — old heuristic would have incorrectly sliced to 25
510
+ const data = makeData(30);
511
+ mockContext({ data, total: 30, page: 1, perPage: 25 });
512
+
513
+ const { result } = renderHook(() => useCollection({}));
514
+
515
+ expect(result.current.items).toEqual(data);
516
+ expect(result.current.items).toHaveLength(30);
517
+ });
518
+
519
+ it('should return undefined items when data is undefined and clientSidePagination is true', () => {
520
+ mockContext({ data: undefined, total: undefined, page: 1, perPage: 25 });
521
+
522
+ const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
523
+
524
+ expect(result.current.items).toBeUndefined();
525
+ });
526
+ });
@@ -1,5 +1,5 @@
1
- import { useState, useMemo } from 'react';
2
- import { useListContext, RaRecord } from '@strato-admin/core';
1
+ import { useState, useMemo, useEffect } from 'react';
2
+ import { useListContext, RaRecord } from '@strato-admin/ra-core';
3
3
  import { UseCollectionOptions, UseCollectionResult, TableColumnDisplay } from './interfaces';
4
4
 
5
5
  export function useCollection<T extends RaRecord>(options: UseCollectionOptions<T>): UseCollectionResult<T> {
@@ -31,17 +31,24 @@ export function useCollection<T extends RaRecord>(options: UseCollectionOptions<
31
31
  options.preferences?.visibleContent ?? options.preferences?.visibleContentOptions?.map((o) => o.id),
32
32
  );
33
33
  const [contentDisplay, setContentDisplay] = useState<ReadonlyArray<TableColumnDisplay> | undefined>(
34
- options.preferences?.contentDisplay ?? options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
34
+ options.preferences?.contentDisplay ??
35
+ options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
35
36
  );
36
37
 
37
- useMemo(() => {
38
+ useEffect(() => {
38
39
  setVisibleContent(
39
40
  options.preferences?.visibleContent ?? options.preferences?.visibleContentOptions?.map((o) => o.id),
40
41
  );
41
42
  setContentDisplay(
42
- options.preferences?.contentDisplay ?? options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
43
+ options.preferences?.contentDisplay ??
44
+ options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
43
45
  );
44
- }, [options.preferences?.visibleContent, options.preferences?.visibleContentOptions, options.preferences?.contentDisplay, options.preferences?.contentDisplayOptions]);
46
+ }, [
47
+ options.preferences?.visibleContent,
48
+ options.preferences?.visibleContentOptions,
49
+ options.preferences?.contentDisplay,
50
+ options.preferences?.contentDisplayOptions,
51
+ ]);
45
52
 
46
53
  const selectedItems = (selectedIds || []).map((id) => {
47
54
  const item = data?.find((i) => i.id === id);
@@ -50,15 +57,13 @@ export function useCollection<T extends RaRecord>(options: UseCollectionOptions<
50
57
  });
51
58
 
52
59
  const items = useMemo(() => {
53
- // If it's a client-side list (data contains all records), we need to slice it for pagination.
54
- // We detect this by checking if data.length equals total and if data.length is greater than perPage.
55
- if (data && total === data.length && data.length > (perPage || 0)) {
60
+ if (options.clientSidePagination && data) {
56
61
  const p = page || 1;
57
62
  const pp = perPage || 25;
58
63
  return data.slice((p - 1) * pp, p * pp);
59
64
  }
60
65
  return data;
61
- }, [data, total, page, perPage]);
66
+ }, [data, options.clientSidePagination, page, perPage]);
62
67
 
63
68
  return {
64
69
  items,
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import { vi, describe, it, expect, beforeEach } from 'vitest';
4
- import { useResourceContext } from '@strato-admin/core';
4
+ import { useResourceContext } from '@strato-admin/ra-core';
5
5
  import { Create } from './Create';
6
6
 
7
- // Mock strato-core
7
+ vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
8
8
  vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
9
9
 
10
10
  // Mock Cloudscape components
@@ -46,7 +46,7 @@ describe('Create', () => {
46
46
 
47
47
  expect(getByTestId('container')).toBeDefined();
48
48
  expect(getByTestId('content').textContent).toBe('Create New');
49
- expect(getByText('Products')).toBeDefined();
49
+ expect(getByText(/Products/)).toBeDefined();
50
50
  });
51
51
 
52
52
  it('should use provided title', () => {
@@ -1,45 +1,76 @@
1
1
  import React from 'react';
2
- import { CreateBase, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
2
+ import { CreateBase, type RaRecord, type CreateBaseProps, Identifier, useTranslate } from '@strato-admin/ra-core';
3
+ import {
4
+ ResourceSchemaProvider,
5
+ useResourceSchema,
6
+ useConstructedPageTitle,
7
+ useSettingValue,
8
+ } from '@strato-admin/core';
3
9
  import Container from '@cloudscape-design/components/container';
4
10
  import { CreateHeader } from './CreateHeader';
5
11
  import Form from '../form/Form';
6
12
 
7
- export interface CreateProps<RecordType extends RaRecord = RaRecord> {
13
+ export interface CreateProps<
14
+ RecordType extends Omit<RaRecord, 'id'> = any,
15
+ ResultRecordType extends RaRecord = RecordType & { id: Identifier },
16
+ MutationOptionsError = Error,
17
+ > extends CreateBaseProps<RecordType, ResultRecordType, MutationOptionsError> {
8
18
  children?: React.ReactNode;
9
- inputSchema?: React.ReactNode;
10
- title?: React.ReactNode;
19
+ title?: React.ReactNode | ((record: Partial<RecordType>) => React.ReactNode);
20
+ description?: React.ReactNode | ((record: Partial<RecordType>) => React.ReactNode);
11
21
  actions?: React.ReactNode;
12
- resource?: string;
13
- record?: Partial<RecordType>;
14
- redirect?: any;
15
- transform?: any;
16
- mutationOptions?: any;
17
22
  include?: string[];
18
23
  exclude?: string[];
24
+ saveButtonLabel?: string;
19
25
  }
20
26
 
21
27
  const CreateUI = ({
22
28
  children,
23
- resource,
24
- inputSchema,
25
29
  title,
26
30
  actions,
31
+ description,
27
32
  include,
28
33
  exclude,
34
+ saveButtonLabel,
29
35
  }: {
30
36
  children?: React.ReactNode;
31
- resource?: string;
32
- inputSchema?: React.ReactNode;
33
- title?: React.ReactNode;
37
+ title?: React.ReactNode | ((record: any) => React.ReactNode);
34
38
  actions?: React.ReactNode;
39
+ description?: React.ReactNode | ((record: any) => React.ReactNode);
35
40
  include?: string[];
36
41
  exclude?: string[];
42
+ saveButtonLabel?: string;
37
43
  }) => {
38
- const finalChildren = children || <Form include={include} exclude={exclude} />;
44
+ const { label, createTitle, createDescription } = useResourceSchema();
45
+ const translate = useTranslate();
46
+ const constructedTitle = useConstructedPageTitle('create', label);
47
+
48
+ const finalTitle = React.useMemo(() => {
49
+ if (typeof title === 'function') return title({});
50
+ if (React.isValidElement(title)) return title;
51
+ if (title) return translate(title as string);
52
+ if (React.isValidElement(createTitle)) return createTitle;
53
+ if (createTitle) return translate(createTitle as string);
54
+ return constructedTitle;
55
+ }, [title, createTitle, translate, constructedTitle]);
56
+
57
+ const finalDescription = React.useMemo(() => {
58
+ if (typeof description === 'function') return description({});
59
+ if (React.isValidElement(description)) return description;
60
+ if (description) return translate(description as string);
61
+ if (React.isValidElement(createDescription)) return createDescription;
62
+ if (createDescription) return translate(createDescription as string);
63
+ return undefined;
64
+ }, [description, createDescription, translate]);
65
+
66
+ const finalSaveButtonLabel = saveButtonLabel ? translate(saveButtonLabel) : translate('Create');
67
+
68
+ const finalChildren = children || <Form include={include} exclude={exclude} saveButtonLabel={finalSaveButtonLabel} />;
69
+
39
70
  return (
40
- <ResourceSchemaProvider resource={resource} inputSchema={inputSchema}>
41
- <Container header={<CreateHeader title={title} actions={actions} />}>{finalChildren}</Container>
42
- </ResourceSchemaProvider>
71
+ <Container header={<CreateHeader title={finalTitle} description={finalDescription} actions={actions} />}>
72
+ {finalChildren}
73
+ </Container>
43
74
  );
44
75
  };
45
76
 
@@ -52,38 +83,38 @@ const CreateUI = ({
52
83
  * <TextInput source="name" />
53
84
  * </Form>
54
85
  * </Create>
55
- *
86
+ *
56
87
  * @example
57
88
  * // Using InputSchema from context
58
89
  * <Create include={['name', 'price']} />
59
- *
60
- * @example
61
- * // Passing a custom input schema
62
- * <Create inputSchema={<InputSchema>...</InputSchema>}>
63
- * <Form />
64
- * </Create>
65
90
  */
66
91
  export const Create = <RecordType extends RaRecord = RaRecord>({
67
92
  children,
68
- inputSchema,
69
93
  title,
70
94
  actions,
95
+ description,
71
96
  include,
72
97
  exclude,
98
+ redirect,
99
+ saveButtonLabel,
73
100
  ...props
74
101
  }: CreateProps<RecordType>) => {
102
+ const resolve = useSettingValue();
103
+ const resolvedRedirect = redirect !== undefined ? redirect : resolve(undefined, 'createRedirect');
75
104
  return (
76
- <CreateBase {...props}>
77
- <CreateUI
78
- resource={props.resource}
79
- title={title}
80
- actions={actions}
81
- include={include}
82
- exclude={exclude}
83
- inputSchema={inputSchema}
84
- >
85
- {children}
86
- </CreateUI>
105
+ <CreateBase redirect={resolvedRedirect} {...props}>
106
+ <ResourceSchemaProvider resource={props.resource}>
107
+ <CreateUI
108
+ title={title}
109
+ actions={actions}
110
+ description={description}
111
+ include={include}
112
+ exclude={exclude}
113
+ saveButtonLabel={saveButtonLabel}
114
+ >
115
+ {children}
116
+ </CreateUI>
117
+ </ResourceSchemaProvider>
87
118
  </CreateBase>
88
119
  );
89
120
  };
@@ -1,13 +1,13 @@
1
1
  import React from 'react';
2
2
  import Header, { HeaderProps } from '@cloudscape-design/components/header';
3
- import SpaceBetween from '@cloudscape-design/components/space-between';
4
- import { useCreateContext, useTranslate } from '@strato-admin/core';
3
+ import { useCreateContext, useTranslate } from '@strato-admin/ra-core';
5
4
 
6
- export interface CreateHeaderProps extends Omit<HeaderProps, 'children'> {
5
+ export interface CreateHeaderProps
6
+ extends Pick<HeaderProps, 'variant' | 'counter' | 'actions' | 'description' | 'info' | 'headingTagOverride'> {
7
7
  title?: React.ReactNode;
8
8
  }
9
9
 
10
- export const CreateHeader = ({ title, actions, ...props }: CreateHeaderProps) => {
10
+ export const CreateHeader = ({ title, actions, description, counter, info, variant = 'h2', headingTagOverride }: CreateHeaderProps) => {
11
11
  const translate = useTranslate();
12
12
  const { defaultTitle } = useCreateContext();
13
13
 
@@ -18,14 +18,10 @@ export const CreateHeader = ({ title, actions, ...props }: CreateHeaderProps) =>
18
18
  return defaultTitle;
19
19
  }, [title, defaultTitle, translate]);
20
20
 
21
- const headerActions = actions || (
22
- <SpaceBetween direction="horizontal" size="xs">
23
- {/* Add default create actions here if needed */}
24
- </SpaceBetween>
25
- );
21
+ const headerActions = actions ?? null;
26
22
 
27
23
  return (
28
- <Header variant="h2" {...props} actions={headerActions}>
24
+ <Header variant={variant} actions={headerActions} description={description} counter={counter} info={info} headingTagOverride={headingTagOverride}>
29
25
  {headerTitle}
30
26
  </Header>
31
27
  );
@@ -0,0 +1,28 @@
1
+ import type { AdminSettings } from '@strato-admin/core';
2
+ import { Message } from './i18n/Message';
3
+ import { Table } from './list';
4
+ import { DetailHub } from './detail';
5
+
6
+ /**
7
+ * The framework's last-resort default values for all Admin-level configurable settings.
8
+ * Override any of these via the `settings` prop on <Admin>.
9
+ */
10
+ export const FRAMEWORK_DEFAULTS: AdminSettings = {
11
+ listComponent: Table,
12
+ detailComponent: DetailHub,
13
+ editSuccessMessage: <Message>Element updated</Message>,
14
+ deleteSuccessMessage: <Message>Element deleted</Message>,
15
+ bulkDeleteSuccessMessage: (countDeleted: number) => (
16
+ <Message vars={{ countDeleted }}>
17
+ {'{countDeleted, plural, one {# element deleted} other {# elements deleted}}'}
18
+ </Message>
19
+ ),
20
+ mutationMode: 'pessimistic',
21
+ createRedirect: 'list',
22
+ editRedirect: 'detail',
23
+ listPageSize: 25,
24
+ listPageSizes: [10, 25, 50, 100],
25
+ listPageSizeLabel: (pageSize: number) => (
26
+ <Message vars={{ pageSize }}>{'{pageSize, plural, one {# item} other {# items}}'}</Message>
27
+ ),
28
+ };