@strato-admin/cloudscape 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/Admin.d.ts +17 -0
  4. package/dist/Admin.js +69 -0
  5. package/dist/RecordLink.d.ts +9 -0
  6. package/dist/RecordLink.js +43 -0
  7. package/dist/__mocks__/strato-core.js +50 -0
  8. package/dist/__mocks__to__delete/strato-core.js +50 -0
  9. package/dist/button/BulkDeleteButton.d.ts +7 -0
  10. package/dist/button/BulkDeleteButton.js +17 -0
  11. package/dist/button/Button.d.ts +6 -0
  12. package/dist/button/Button.js +6 -0
  13. package/dist/button/CreateButton.d.ts +6 -0
  14. package/dist/button/CreateButton.js +24 -0
  15. package/dist/button/EditButton.d.ts +8 -0
  16. package/dist/button/EditButton.js +24 -0
  17. package/dist/button/SaveButton.d.ts +6 -0
  18. package/dist/button/SaveButton.js +8 -0
  19. package/dist/button/index.d.ts +5 -0
  20. package/dist/button/index.js +5 -0
  21. package/dist/collection-hooks/index.d.ts +2 -0
  22. package/dist/collection-hooks/index.js +2 -0
  23. package/dist/collection-hooks/interfaces.d.ts +93 -0
  24. package/dist/collection-hooks/interfaces.js +1 -0
  25. package/dist/collection-hooks/useCollection.d.ts +3 -0
  26. package/dist/collection-hooks/useCollection.js +102 -0
  27. package/dist/create/Create.d.ts +40 -0
  28. package/dist/create/Create.js +34 -0
  29. package/dist/create/CreateHeader.d.ts +7 -0
  30. package/dist/create/CreateHeader.js +18 -0
  31. package/dist/create/index.d.ts +2 -0
  32. package/dist/create/index.js +2 -0
  33. package/dist/detail/KeyValuePairs.d.ts +36 -0
  34. package/dist/detail/KeyValuePairs.js +58 -0
  35. package/dist/detail/Show.d.ts +39 -0
  36. package/dist/detail/Show.js +40 -0
  37. package/dist/detail/ShowHeader.d.ts +7 -0
  38. package/dist/detail/ShowHeader.js +19 -0
  39. package/dist/detail/index.d.ts +3 -0
  40. package/dist/detail/index.js +3 -0
  41. package/dist/edit/Edit.d.ts +42 -0
  42. package/dist/edit/Edit.js +38 -0
  43. package/dist/edit/EditHeader.d.ts +7 -0
  44. package/dist/edit/EditHeader.js +18 -0
  45. package/dist/edit/index.d.ts +2 -0
  46. package/dist/edit/index.js +2 -0
  47. package/dist/field/ArrayField.d.ts +29 -0
  48. package/dist/field/ArrayField.js +30 -0
  49. package/dist/field/BadgeField.d.ts +12 -0
  50. package/dist/field/BadgeField.js +15 -0
  51. package/dist/field/BooleanField.d.ts +18 -0
  52. package/dist/field/BooleanField.js +14 -0
  53. package/dist/field/CurrencyField.d.ts +19 -0
  54. package/dist/field/CurrencyField.js +23 -0
  55. package/dist/field/DateField.d.ts +14 -0
  56. package/dist/field/DateField.js +17 -0
  57. package/dist/field/IdField.d.ts +17 -0
  58. package/dist/field/IdField.js +21 -0
  59. package/dist/field/NumberField.d.ts +14 -0
  60. package/dist/field/NumberField.js +18 -0
  61. package/dist/field/ReferenceField.d.ts +16 -0
  62. package/dist/field/ReferenceField.js +23 -0
  63. package/dist/field/ReferenceManyField.d.ts +55 -0
  64. package/dist/field/ReferenceManyField.js +19 -0
  65. package/dist/field/StatusIndicatorField.d.ts +56 -0
  66. package/dist/field/StatusIndicatorField.js +48 -0
  67. package/dist/field/TextField.d.ts +5 -0
  68. package/dist/field/TextField.js +11 -0
  69. package/dist/field/index.d.ts +23 -0
  70. package/dist/field/index.js +23 -0
  71. package/dist/field/types.d.ts +56 -0
  72. package/dist/field/types.js +1 -0
  73. package/dist/form/Form.d.ts +13 -0
  74. package/dist/form/Form.js +33 -0
  75. package/dist/form/index.d.ts +2 -0
  76. package/dist/form/index.js +2 -0
  77. package/dist/index.d.ts +22 -0
  78. package/dist/index.js +22 -0
  79. package/dist/input/AttributeEditor.d.ts +25 -0
  80. package/dist/input/AttributeEditor.js +80 -0
  81. package/dist/input/AutocompleteInput.d.ts +10 -0
  82. package/dist/input/AutocompleteInput.js +67 -0
  83. package/dist/input/FieldTitle.d.ts +8 -0
  84. package/dist/input/FieldTitle.js +29 -0
  85. package/dist/input/FormField.d.ts +7 -0
  86. package/dist/input/FormField.js +35 -0
  87. package/dist/input/FormFieldContext.d.ts +6 -0
  88. package/dist/input/FormFieldContext.js +3 -0
  89. package/dist/input/NumberInput.d.ts +7 -0
  90. package/dist/input/NumberInput.js +27 -0
  91. package/dist/input/ReferenceInput.d.ts +3 -0
  92. package/dist/input/ReferenceInput.js +25 -0
  93. package/dist/input/SelectInput.d.ts +15 -0
  94. package/dist/input/SelectInput.js +47 -0
  95. package/dist/input/SliderInput.d.ts +6 -0
  96. package/dist/input/SliderInput.js +25 -0
  97. package/dist/input/TextAreaInput.d.ts +6 -0
  98. package/dist/input/TextAreaInput.js +23 -0
  99. package/dist/input/TextInput.d.ts +7 -0
  100. package/dist/input/TextInput.js +23 -0
  101. package/dist/input/index.d.ts +11 -0
  102. package/dist/input/index.js +11 -0
  103. package/dist/input/types.d.ts +6 -0
  104. package/dist/input/types.js +1 -0
  105. package/dist/layout/AppLayout.d.ts +8 -0
  106. package/dist/layout/AppLayout.js +38 -0
  107. package/dist/layout/TopNavigation.d.ts +6 -0
  108. package/dist/layout/TopNavigation.js +53 -0
  109. package/dist/layout/index.d.ts +2 -0
  110. package/dist/layout/index.js +2 -0
  111. package/dist/list/Cards.d.ts +11 -0
  112. package/dist/list/Cards.js +27 -0
  113. package/dist/list/List.d.ts +43 -0
  114. package/dist/list/List.js +28 -0
  115. package/dist/list/Table.d.ts +112 -0
  116. package/dist/list/Table.examples.d.ts +1 -0
  117. package/dist/list/Table.examples.js +3 -0
  118. package/dist/list/Table.js +218 -0
  119. package/dist/list/TableHeader.d.ts +7 -0
  120. package/dist/list/TableHeader.js +22 -0
  121. package/dist/list/index.d.ts +4 -0
  122. package/dist/list/index.js +4 -0
  123. package/dist/preferences/index.d.ts +0 -0
  124. package/dist/preferences/index.js +1 -0
  125. package/dist/theme/ThemeManager.d.ts +2 -0
  126. package/dist/theme/ThemeManager.js +11 -0
  127. package/dist/theme/index.d.ts +2 -0
  128. package/dist/theme/index.js +2 -0
  129. package/package.json +73 -0
  130. package/src/Admin.test.tsx +32 -0
  131. package/src/Admin.tsx +123 -0
  132. package/src/RecordLink.stories.tsx +56 -0
  133. package/src/RecordLink.tsx +67 -0
  134. package/src/__mocks__/strato-core.tsx +52 -0
  135. package/src/button/BulkDeleteButton.stories.tsx +59 -0
  136. package/src/button/BulkDeleteButton.test.tsx +64 -0
  137. package/src/button/BulkDeleteButton.tsx +41 -0
  138. package/src/button/Button.stories.tsx +31 -0
  139. package/src/button/Button.tsx +12 -0
  140. package/src/button/CreateButton.stories.tsx +42 -0
  141. package/src/button/CreateButton.tsx +38 -0
  142. package/src/button/EditButton.stories.tsx +29 -0
  143. package/src/button/EditButton.tsx +38 -0
  144. package/src/button/SaveButton.stories.tsx +35 -0
  145. package/src/button/SaveButton.tsx +19 -0
  146. package/src/button/index.ts +5 -0
  147. package/src/collection-hooks/index.ts +2 -0
  148. package/src/collection-hooks/interfaces.ts +80 -0
  149. package/src/collection-hooks/useCollection.test.ts +413 -0
  150. package/src/collection-hooks/useCollection.ts +125 -0
  151. package/src/create/Create.test.tsx +63 -0
  152. package/src/create/Create.tsx +93 -0
  153. package/src/create/CreateHeader.tsx +34 -0
  154. package/src/create/index.ts +2 -0
  155. package/src/detail/KeyValuePairs.test.tsx +98 -0
  156. package/src/detail/KeyValuePairs.tsx +107 -0
  157. package/src/detail/Show.test.tsx +96 -0
  158. package/src/detail/Show.tsx +104 -0
  159. package/src/detail/ShowHeader.test.tsx +80 -0
  160. package/src/detail/ShowHeader.tsx +35 -0
  161. package/src/detail/index.ts +3 -0
  162. package/src/edit/Edit.test.tsx +91 -0
  163. package/src/edit/Edit.tsx +102 -0
  164. package/src/edit/EditHeader.tsx +34 -0
  165. package/src/edit/index.ts +2 -0
  166. package/src/field/ArrayField.tsx +51 -0
  167. package/src/field/BadgeField.tsx +33 -0
  168. package/src/field/BooleanField.stories.tsx +56 -0
  169. package/src/field/BooleanField.test.tsx +63 -0
  170. package/src/field/BooleanField.tsx +42 -0
  171. package/src/field/CurrencyField.stories.tsx +67 -0
  172. package/src/field/CurrencyField.tsx +45 -0
  173. package/src/field/DateField.stories.tsx +67 -0
  174. package/src/field/DateField.tsx +33 -0
  175. package/src/field/IdField.test.tsx +88 -0
  176. package/src/field/IdField.tsx +40 -0
  177. package/src/field/NumberField.stories.tsx +75 -0
  178. package/src/field/NumberField.tsx +35 -0
  179. package/src/field/ReferenceField.test.tsx +88 -0
  180. package/src/field/ReferenceField.tsx +64 -0
  181. package/src/field/ReferenceManyField.test.tsx +41 -0
  182. package/src/field/ReferenceManyField.tsx +73 -0
  183. package/src/field/StatusIndicatorField.stories.tsx +93 -0
  184. package/src/field/StatusIndicatorField.test.tsx +143 -0
  185. package/src/field/StatusIndicatorField.tsx +119 -0
  186. package/src/field/TextField.stories.tsx +45 -0
  187. package/src/field/TextField.tsx +17 -0
  188. package/src/field/index.ts +23 -0
  189. package/src/field/types.ts +58 -0
  190. package/src/form/Form.test.tsx +55 -0
  191. package/src/form/Form.tsx +66 -0
  192. package/src/form/index.ts +2 -0
  193. package/src/index.ts +25 -0
  194. package/src/input/AttributeEditor.test.tsx +147 -0
  195. package/src/input/AttributeEditor.tsx +185 -0
  196. package/src/input/AutocompleteInput.test.tsx +178 -0
  197. package/src/input/AutocompleteInput.tsx +116 -0
  198. package/src/input/FieldTitle.tsx +53 -0
  199. package/src/input/FormField.tsx +87 -0
  200. package/src/input/FormFieldContext.ts +9 -0
  201. package/src/input/NumberInput.tsx +56 -0
  202. package/src/input/ReferenceInput.test.tsx +35 -0
  203. package/src/input/ReferenceInput.tsx +36 -0
  204. package/src/input/SelectInput.tsx +91 -0
  205. package/src/input/SliderInput.test.tsx +103 -0
  206. package/src/input/SliderInput.tsx +49 -0
  207. package/src/input/TextAreaInput.tsx +48 -0
  208. package/src/input/TextInput.test.tsx +91 -0
  209. package/src/input/TextInput.tsx +51 -0
  210. package/src/input/index.ts +11 -0
  211. package/src/input/types.ts +14 -0
  212. package/src/layout/AppLayout.test.tsx +87 -0
  213. package/src/layout/AppLayout.tsx +60 -0
  214. package/src/layout/TopNavigation.test.tsx +78 -0
  215. package/src/layout/TopNavigation.tsx +84 -0
  216. package/src/layout/index.ts +2 -0
  217. package/src/list/Cards.tsx +58 -0
  218. package/src/list/List.tsx +76 -0
  219. package/src/list/Table.examples.tsx +11 -0
  220. package/src/list/Table.stories.tsx +73 -0
  221. package/src/list/Table.test.tsx +255 -0
  222. package/src/list/Table.tsx +438 -0
  223. package/src/list/TableHeader.test.tsx +114 -0
  224. package/src/list/TableHeader.tsx +44 -0
  225. package/src/list/index.ts +4 -0
  226. package/src/preferences/index.ts +0 -0
  227. package/src/stories/Button.stories.ts +54 -0
  228. package/src/stories/Button.tsx +31 -0
  229. package/src/stories/Configure.mdx +369 -0
  230. package/src/stories/Header.stories.ts +34 -0
  231. package/src/stories/Header.tsx +47 -0
  232. package/src/stories/Page.stories.ts +33 -0
  233. package/src/stories/Page.tsx +71 -0
  234. package/src/stories/RaStoryDecorator.tsx +38 -0
  235. package/src/stories/assets/accessibility.png +0 -0
  236. package/src/stories/assets/accessibility.svg +1 -0
  237. package/src/stories/assets/addon-library.png +0 -0
  238. package/src/stories/assets/assets.png +0 -0
  239. package/src/stories/assets/avif-test-image.avif +0 -0
  240. package/src/stories/assets/context.png +0 -0
  241. package/src/stories/assets/discord.svg +1 -0
  242. package/src/stories/assets/docs.png +0 -0
  243. package/src/stories/assets/figma-plugin.png +0 -0
  244. package/src/stories/assets/github.svg +1 -0
  245. package/src/stories/assets/share.png +0 -0
  246. package/src/stories/assets/styling.png +0 -0
  247. package/src/stories/assets/testing.png +0 -0
  248. package/src/stories/assets/theming.png +0 -0
  249. package/src/stories/assets/tutorials.svg +1 -0
  250. package/src/stories/assets/youtube.svg +1 -0
  251. package/src/stories/button.css +30 -0
  252. package/src/stories/header.css +32 -0
  253. package/src/stories/page.css +68 -0
  254. package/src/theme/ThemeManager.tsx +15 -0
  255. package/src/theme/index.ts +2 -0
@@ -0,0 +1,19 @@
1
+
2
+ import { useTranslate } from '@strato-admin/core';
3
+ import { Button, ButtonProps } from './Button';
4
+
5
+ export interface SaveButtonProps extends Omit<ButtonProps, 'children'> {
6
+ label?: string;
7
+ }
8
+
9
+ export const SaveButton = ({ label, variant = 'primary', ...props }: SaveButtonProps) => {
10
+ const translate = useTranslate();
11
+
12
+ return (
13
+ <Button variant={variant} formAction="submit" nativeButtonAttributes={{ type: 'submit' }} {...props}>
14
+ {label || translate('ra.action.save', { _: 'Save' })}
15
+ </Button>
16
+ );
17
+ };
18
+
19
+ export default SaveButton;
@@ -0,0 +1,5 @@
1
+ export * from './Button';
2
+ export * from './EditButton';
3
+ export * from './BulkDeleteButton';
4
+ export * from './SaveButton';
5
+ export * from './CreateButton';
@@ -0,0 +1,2 @@
1
+ export * from './useCollection';
2
+ export * from './interfaces';
@@ -0,0 +1,80 @@
1
+ // shim for dom types
2
+ interface CustomEventLike<T> {
3
+ detail: T;
4
+ }
5
+
6
+ export interface TableColumnDisplay {
7
+ id: string;
8
+ visible: boolean;
9
+ }
10
+
11
+ export interface CollectionPreferences {
12
+ pageSize?: number;
13
+ wrapLines?: boolean;
14
+ stripedRows?: boolean;
15
+ visibleContent?: ReadonlyArray<string>;
16
+ contentDisplay?: ReadonlyArray<TableColumnDisplay>;
17
+ contentDensity?: 'comfortable' | 'compact';
18
+ }
19
+
20
+ export interface UseCollectionOptions<_T> {
21
+ filtering?: any;
22
+ pagination?: any;
23
+ sorting?: any;
24
+ preferences?: {
25
+ pageSizeOptions?: ReadonlyArray<{ value: number; label?: string }>;
26
+ visibleContentOptions?: ReadonlyArray<{
27
+ id: string;
28
+ label: string;
29
+ editable?: boolean;
30
+ }>;
31
+ contentDisplayOptions?: ReadonlyArray<{
32
+ id: string;
33
+ label: string;
34
+ alwaysVisible?: boolean;
35
+ }>;
36
+ wrapLines?: boolean;
37
+ stripedRows?: boolean;
38
+ visibleContent?: ReadonlyArray<string>;
39
+ contentDisplay?: ReadonlyArray<TableColumnDisplay>;
40
+ };
41
+ }
42
+
43
+ export interface UseCollectionResult<T> {
44
+ items: ReadonlyArray<T> | undefined;
45
+ collectionProps: {
46
+ selectedItems: T[];
47
+ onSelectionChange(event: CustomEventLike<{ selectedItems: T[] }>): void;
48
+ trackBy: string | ((item: T) => string);
49
+ sortingColumn?: { sortingField?: string };
50
+ sortingDescending?: boolean;
51
+ onSortingChange?(
52
+ event: CustomEventLike<{ sortingColumn: { sortingField?: string }; isDescending?: boolean }>,
53
+ ): void;
54
+ };
55
+ paginationProps: {
56
+ disabled?: boolean;
57
+ currentPageIndex: number;
58
+ pagesCount: number;
59
+ onChange(event: CustomEventLike<{ currentPageIndex: number }>): void;
60
+ };
61
+ filterProps: {
62
+ filteringText: string;
63
+ onChange(event: CustomEventLike<{ filteringText: string }>): void;
64
+ };
65
+ preferencesProps: {
66
+ preferences: CollectionPreferences;
67
+ onConfirm(event: CustomEventLike<CollectionPreferences>): void;
68
+ pageSizeOptions?: ReadonlyArray<{ value: number; label?: string }>;
69
+ visibleContentOptions?: ReadonlyArray<{
70
+ id: string;
71
+ label: string;
72
+ editable?: boolean;
73
+ }>;
74
+ contentDisplayOptions?: ReadonlyArray<{
75
+ id: string;
76
+ label: string;
77
+ alwaysVisible?: boolean;
78
+ }>;
79
+ };
80
+ }
@@ -0,0 +1,413 @@
1
+ import { renderHook, act } from '@testing-library/react';
2
+ import { vi, describe, it, expect } from 'vitest';
3
+ import { useListContext } from '@strato-admin/core';
4
+ import { useCollection } from './useCollection';
5
+
6
+ vi.mock('@strato-admin/core', () => ({
7
+ useListContext: vi.fn(),
8
+ }));
9
+
10
+ describe('useCollection', () => {
11
+ it('should return collection items and pagination props', () => {
12
+ const mockSetPage = vi.fn();
13
+ const mockData = [{ id: 1, name: 'Item 1' }];
14
+
15
+ (useListContext as any).mockReturnValue({
16
+ data: mockData,
17
+ page: 1,
18
+ isPending: false,
19
+ isFetching: false,
20
+ isLoading: false,
21
+ setPage: mockSetPage,
22
+ selectedIds: [],
23
+ onSelect: vi.fn(),
24
+ });
25
+
26
+ const { result } = renderHook(() => useCollection({}));
27
+
28
+ expect(result.current.items).toEqual(mockData);
29
+ expect(result.current.paginationProps.currentPageIndex).toBe(1);
30
+ expect(result.current.paginationProps.disabled).toBe(false);
31
+ });
32
+
33
+ it('should call setPage with the correct page index when onChange is called', () => {
34
+ const mockSetPage = vi.fn();
35
+
36
+ (useListContext as any).mockReturnValue({
37
+ data: [],
38
+ page: 1,
39
+ isPending: false,
40
+ isFetching: false,
41
+ isLoading: false,
42
+ setPage: mockSetPage,
43
+ selectedIds: [],
44
+ onSelect: vi.fn(),
45
+ });
46
+
47
+ const { result } = renderHook(() => useCollection({}));
48
+
49
+ act(() => {
50
+ result.current.paginationProps.onChange({ detail: { currentPageIndex: 2 } });
51
+ });
52
+
53
+ expect(mockSetPage).toHaveBeenCalledWith(2); // React Admin and Cloudscape both use 1-based indexing
54
+ });
55
+
56
+ it('should return selected items based on selectedIds', () => {
57
+ const mockData = [
58
+ { id: 1, name: 'Item 1' },
59
+ { id: 2, name: 'Item 2' },
60
+ { id: 3, name: 'Item 3' },
61
+ ];
62
+ const mockSelectedIds = [1, 3];
63
+
64
+ (useListContext as any).mockReturnValue({
65
+ data: mockData,
66
+ page: 1,
67
+ isPending: false,
68
+ isFetching: false,
69
+ isLoading: false,
70
+ setPage: vi.fn(),
71
+ selectedIds: mockSelectedIds,
72
+ onSelect: vi.fn(),
73
+ });
74
+
75
+ const { result } = renderHook(() => useCollection({}));
76
+
77
+ expect(result.current.collectionProps.selectedItems).toEqual([
78
+ { id: 1, name: 'Item 1' },
79
+ { id: 3, name: 'Item 3' },
80
+ ]);
81
+ });
82
+
83
+ it('should include dummy objects for selectedIds not in current data', () => {
84
+ const mockData = [{ id: 1, name: 'Item 1' }];
85
+ const mockSelectedIds = [1, 2];
86
+
87
+ (useListContext as any).mockReturnValue({
88
+ data: mockData,
89
+ page: 1,
90
+ isPending: false,
91
+ isFetching: false,
92
+ isLoading: false,
93
+ setPage: vi.fn(),
94
+ selectedIds: mockSelectedIds,
95
+ onSelect: vi.fn(),
96
+ });
97
+
98
+ const { result } = renderHook(() => useCollection({}));
99
+
100
+ expect(result.current.collectionProps.selectedItems).toEqual([{ id: 1, name: 'Item 1' }, { id: 2 }]);
101
+ });
102
+
103
+ it('should call onSelect when onSelectionChange is called', () => {
104
+ const mockOnSelect = vi.fn();
105
+ const mockData = [
106
+ { id: 1, name: 'Item 1' },
107
+ { id: 2, name: 'Item 2' },
108
+ ];
109
+
110
+ (useListContext as any).mockReturnValue({
111
+ data: mockData,
112
+ page: 1,
113
+ isPending: false,
114
+ isFetching: false,
115
+ isLoading: false,
116
+ setPage: vi.fn(),
117
+ selectedIds: [],
118
+ onSelect: mockOnSelect,
119
+ });
120
+
121
+ const { result } = renderHook(() => useCollection({}));
122
+
123
+ act(() => {
124
+ result.current.collectionProps.onSelectionChange({
125
+ detail: {
126
+ selectedItems: [{ id: 2, name: 'Item 2' }],
127
+ },
128
+ });
129
+ });
130
+
131
+ expect(mockOnSelect).toHaveBeenCalledWith([2]);
132
+ });
133
+
134
+ it('should return sorting props from sort object', () => {
135
+ (useListContext as any).mockReturnValue({
136
+ data: [],
137
+ sort: { field: 'name', order: 'DESC' },
138
+ setSort: vi.fn(),
139
+ page: 1,
140
+ isPending: false,
141
+ isFetching: false,
142
+ isLoading: false,
143
+ setPage: vi.fn(),
144
+ selectedIds: [],
145
+ onSelect: vi.fn(),
146
+ });
147
+
148
+ const { result } = renderHook(() => useCollection({}));
149
+
150
+ expect(result.current.collectionProps.sortingColumn).toEqual({ sortingField: 'name' });
151
+ expect(result.current.collectionProps.sortingDescending).toBe(true);
152
+ });
153
+
154
+ it('should call setSort when onSortingChange is called', () => {
155
+ const mockSetSort = vi.fn();
156
+ (useListContext as any).mockReturnValue({
157
+ data: [],
158
+ sort: { field: 'name', order: 'ASC' },
159
+ setSort: mockSetSort,
160
+ page: 1,
161
+ isPending: false,
162
+ isFetching: false,
163
+ isLoading: false,
164
+ setPage: vi.fn(),
165
+ selectedIds: [],
166
+ onSelect: vi.fn(),
167
+ });
168
+
169
+ const { result } = renderHook(() => useCollection({}));
170
+
171
+ act(() => {
172
+ result.current.collectionProps.onSortingChange!({
173
+ detail: {
174
+ sortingColumn: { sortingField: 'price' },
175
+ isDescending: true,
176
+ },
177
+ });
178
+ });
179
+
180
+ expect(mockSetSort).toHaveBeenCalledWith({ field: 'price', order: 'DESC' });
181
+ });
182
+
183
+ it('should disable pagination if loading, fetching, or pending', () => {
184
+ (useListContext as any).mockReturnValue({
185
+ data: [],
186
+ page: 1,
187
+ isPending: true,
188
+ isFetching: false,
189
+ isLoading: false,
190
+ setPage: vi.fn(),
191
+ selectedIds: [],
192
+ onSelect: vi.fn(),
193
+ });
194
+
195
+ const { result } = renderHook(() => useCollection({}));
196
+ expect(result.current.paginationProps.disabled).toBe(true);
197
+ });
198
+
199
+ it('should return filteringText from filterValues.q', () => {
200
+ (useListContext as any).mockReturnValue({
201
+ data: [],
202
+ page: 1,
203
+ isPending: false,
204
+ isFetching: false,
205
+ isLoading: false,
206
+ setPage: vi.fn(),
207
+ selectedIds: [],
208
+ onSelect: vi.fn(),
209
+ filterValues: { q: 'search term', status: 'active' },
210
+ setFilters: vi.fn(),
211
+ });
212
+
213
+ const { result } = renderHook(() => useCollection({}));
214
+
215
+ expect(result.current.filterProps.filteringText).toBe('search term');
216
+ });
217
+
218
+ it('should call setFilters with updated q value when filter onChange is called', () => {
219
+ const mockSetFilters = vi.fn();
220
+ (useListContext as any).mockReturnValue({
221
+ data: [],
222
+ page: 1,
223
+ isPending: false,
224
+ isFetching: false,
225
+ isLoading: false,
226
+ setPage: vi.fn(),
227
+ selectedIds: [],
228
+ onSelect: vi.fn(),
229
+ filterValues: { status: 'active' },
230
+ setFilters: mockSetFilters,
231
+ });
232
+
233
+ const { result } = renderHook(() => useCollection({}));
234
+
235
+ act(() => {
236
+ result.current.filterProps.onChange({
237
+ detail: { filteringText: 'new search' },
238
+ });
239
+ });
240
+
241
+ expect(mockSetFilters).toHaveBeenCalledWith({ status: 'active', q: 'new search' });
242
+ });
243
+
244
+ it('should return preferencesProps with pageSize from perPage', () => {
245
+ (useListContext as any).mockReturnValue({
246
+ data: [],
247
+ perPage: 50,
248
+ setPerPage: vi.fn(),
249
+ page: 1,
250
+ isPending: false,
251
+ isFetching: false,
252
+ isLoading: false,
253
+ setPage: vi.fn(),
254
+ selectedIds: [],
255
+ onSelect: vi.fn(),
256
+ });
257
+
258
+ const { result } = renderHook(() => useCollection({}));
259
+
260
+ expect(result.current.preferencesProps.preferences.pageSize).toBe(50);
261
+ });
262
+
263
+ it('should call setPerPage when preferences onConfirm is called with new pageSize', () => {
264
+ const mockSetPerPage = vi.fn();
265
+ (useListContext as any).mockReturnValue({
266
+ data: [],
267
+ perPage: 25,
268
+ setPerPage: mockSetPerPage,
269
+ page: 1,
270
+ isPending: false,
271
+ isFetching: false,
272
+ isLoading: false,
273
+ setPage: vi.fn(),
274
+ selectedIds: [],
275
+ onSelect: vi.fn(),
276
+ });
277
+
278
+ const { result } = renderHook(() => useCollection({}));
279
+
280
+ act(() => {
281
+ result.current.preferencesProps.onConfirm({
282
+ detail: { pageSize: 50 },
283
+ });
284
+ });
285
+
286
+ expect(mockSetPerPage).toHaveBeenCalledWith(50);
287
+ });
288
+
289
+ it('should not call setPerPage if pageSize is unchanged', () => {
290
+ const mockSetPerPage = vi.fn();
291
+ (useListContext as any).mockReturnValue({
292
+ data: [],
293
+ perPage: 25,
294
+ setPerPage: mockSetPerPage,
295
+ page: 1,
296
+ isPending: false,
297
+ isFetching: false,
298
+ isLoading: false,
299
+ setPage: vi.fn(),
300
+ selectedIds: [],
301
+ onSelect: vi.fn(),
302
+ });
303
+
304
+ const { result } = renderHook(() => useCollection({}));
305
+
306
+ act(() => {
307
+ result.current.preferencesProps.onConfirm({
308
+ detail: { pageSize: 25 },
309
+ });
310
+ });
311
+
312
+ expect(mockSetPerPage).not.toHaveBeenCalled();
313
+ });
314
+
315
+ it('should initialize visibleContent from visibleContentOptions if provided', () => {
316
+ (useListContext as any).mockReturnValue({
317
+ data: [],
318
+ perPage: 25,
319
+ setPerPage: vi.fn(),
320
+ page: 1,
321
+ isPending: false,
322
+ isFetching: false,
323
+ isLoading: false,
324
+ setPage: vi.fn(),
325
+ selectedIds: [],
326
+ onSelect: vi.fn(),
327
+ });
328
+
329
+ const visibleContentOptions = [
330
+ { id: 'id', label: 'ID' },
331
+ { id: 'name', label: 'Name' },
332
+ ];
333
+ const { result } = renderHook(() =>
334
+ useCollection({
335
+ preferences: {
336
+ visibleContentOptions,
337
+ },
338
+ }),
339
+ );
340
+
341
+ expect(result.current.preferencesProps.preferences.visibleContent).toEqual(['id', 'name']);
342
+ });
343
+
344
+ it('should initialize visibleContent and contentDisplay from preferences if provided', () => {
345
+ (useListContext as any).mockReturnValue({
346
+ data: [],
347
+ perPage: 25,
348
+ setPerPage: vi.fn(),
349
+ page: 1,
350
+ isPending: false,
351
+ isFetching: false,
352
+ isLoading: false,
353
+ setPage: vi.fn(),
354
+ selectedIds: [],
355
+ onSelect: vi.fn(),
356
+ });
357
+
358
+ const visibleContent = ['id', 'name'];
359
+ const contentDisplay = [
360
+ { id: 'id', visible: true },
361
+ { id: 'name', visible: true },
362
+ { id: 'price', visible: false },
363
+ ];
364
+
365
+ const { result } = renderHook(() =>
366
+ useCollection({
367
+ preferences: {
368
+ visibleContent,
369
+ contentDisplay,
370
+ },
371
+ }),
372
+ );
373
+
374
+ expect(result.current.preferencesProps.preferences.visibleContent).toEqual(visibleContent);
375
+ expect(result.current.preferencesProps.preferences.contentDisplay).toEqual(contentDisplay);
376
+ });
377
+
378
+ it('should update wrapLines and stripedRows when preferences onConfirm is called', () => {
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 } = renderHook(() =>
393
+ useCollection({
394
+ preferences: {
395
+ wrapLines: false,
396
+ stripedRows: false,
397
+ },
398
+ }),
399
+ );
400
+
401
+ expect(result.current.preferencesProps.preferences.wrapLines).toBe(false);
402
+ expect(result.current.preferencesProps.preferences.stripedRows).toBe(false);
403
+
404
+ act(() => {
405
+ result.current.preferencesProps.onConfirm({
406
+ detail: { wrapLines: true, stripedRows: true },
407
+ });
408
+ });
409
+
410
+ expect(result.current.preferencesProps.preferences.wrapLines).toBe(true);
411
+ expect(result.current.preferencesProps.preferences.stripedRows).toBe(true);
412
+ });
413
+ });
@@ -0,0 +1,125 @@
1
+ import { useState, useMemo } from 'react';
2
+ import { useListContext, RaRecord } from '@strato-admin/core';
3
+ import { UseCollectionOptions, UseCollectionResult, TableColumnDisplay } from './interfaces';
4
+
5
+ export function useCollection<T extends RaRecord>(options: UseCollectionOptions<T>): UseCollectionResult<T> {
6
+ const {
7
+ data, // Array of the list records, e.g. [{ id: 123, title: 'hello world' }, { ... }
8
+ total, // Total number of results for the current filters, excluding pagination. Useful to build the pagination controls, e.g. 23
9
+ isPending, // Boolean, true until the data is available
10
+ isFetching, // Boolean, true while the data is being fetched, false once the data is fetched
11
+ isLoading, // Boolean, true until the data is fetched for the first time
12
+ // Pagination
13
+ page, // Current page. Starts at 1
14
+ perPage, // Number of results per page. Defaults to 25
15
+ setPage, // Callback to change the page, e.g. setPage(3)
16
+ setPerPage, // Callback to change the number of results per page, e.g. setPerPage(50)
17
+ // Selection
18
+ selectedIds, // Array of the selected record IDs
19
+ onSelect, // Callback to change the selection, e.g. onSelect([123, 456])
20
+ // Sorting
21
+ sort, // Current sort. E.g. { field: 'id', order: 'ASC' }
22
+ setSort, // Callback to change the sort. E.g. setSort({ field: 'id', order: 'ASC' })
23
+ // Filtering
24
+ filterValues,
25
+ setFilters,
26
+ } = useListContext<T>();
27
+
28
+ const [wrapLines, setWrapLines] = useState(options.preferences?.wrapLines);
29
+ const [stripedRows, setStripedRows] = useState(options.preferences?.stripedRows);
30
+ const [visibleContent, setVisibleContent] = useState<ReadonlyArray<string> | undefined>(
31
+ options.preferences?.visibleContent ?? options.preferences?.visibleContentOptions?.map((o) => o.id),
32
+ );
33
+ const [contentDisplay, setContentDisplay] = useState<ReadonlyArray<TableColumnDisplay> | undefined>(
34
+ options.preferences?.contentDisplay ?? options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
35
+ );
36
+
37
+ useMemo(() => {
38
+ setVisibleContent(
39
+ options.preferences?.visibleContent ?? options.preferences?.visibleContentOptions?.map((o) => o.id),
40
+ );
41
+ setContentDisplay(
42
+ options.preferences?.contentDisplay ?? options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
43
+ );
44
+ }, [options.preferences?.visibleContent, options.preferences?.visibleContentOptions, options.preferences?.contentDisplay, options.preferences?.contentDisplayOptions]);
45
+
46
+ const selectedItems = (selectedIds || []).map((id) => {
47
+ const item = data?.find((i) => i.id === id);
48
+ if (item) return item;
49
+ return { id } as T;
50
+ });
51
+
52
+ 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)) {
56
+ const p = page || 1;
57
+ const pp = perPage || 25;
58
+ return data.slice((p - 1) * pp, p * pp);
59
+ }
60
+ return data;
61
+ }, [data, total, page, perPage]);
62
+
63
+ return {
64
+ items,
65
+ collectionProps: {
66
+ selectedItems,
67
+ onSelectionChange: (event) => onSelect(event.detail.selectedItems.map((item) => item.id)),
68
+ trackBy: 'id',
69
+ sortingColumn: sort ? { sortingField: sort.field } : undefined,
70
+ sortingDescending: sort?.order === 'DESC',
71
+ onSortingChange: (event) => {
72
+ const { sortingColumn, isDescending } = event.detail;
73
+ if (sortingColumn?.sortingField) {
74
+ setSort({ field: sortingColumn.sortingField, order: isDescending ? 'DESC' : 'ASC' });
75
+ }
76
+ },
77
+ },
78
+ paginationProps: {
79
+ disabled: isPending || isFetching || isLoading,
80
+ currentPageIndex: page,
81
+ pagesCount: total !== undefined && perPage ? Math.ceil(total / perPage) : 1,
82
+ onChange: (event) => setPage(event.detail.currentPageIndex),
83
+ },
84
+ filterProps: {
85
+ filteringText: filterValues?.q || '',
86
+ onChange: (event) => {
87
+ setFilters({ ...filterValues, q: event.detail.filteringText });
88
+ },
89
+ },
90
+ preferencesProps: {
91
+ preferences: {
92
+ pageSize: perPage,
93
+ wrapLines,
94
+ stripedRows,
95
+ visibleContent,
96
+ contentDisplay,
97
+ },
98
+ onConfirm: (event) => {
99
+ const {
100
+ pageSize,
101
+ wrapLines: newWrapLines,
102
+ stripedRows: newStripedRows,
103
+ visibleContent: newVisibleContent,
104
+ contentDisplay: newContentDisplay,
105
+ } = event.detail;
106
+ if (pageSize !== undefined && pageSize !== perPage) {
107
+ setPerPage(pageSize);
108
+ }
109
+ if (newWrapLines !== undefined) {
110
+ setWrapLines(newWrapLines);
111
+ }
112
+ if (newStripedRows !== undefined) {
113
+ setStripedRows(newStripedRows);
114
+ }
115
+ if (newVisibleContent !== undefined) {
116
+ setVisibleContent(newVisibleContent);
117
+ }
118
+ if (newContentDisplay !== undefined) {
119
+ setContentDisplay(newContentDisplay);
120
+ }
121
+ },
122
+ ...options.preferences,
123
+ },
124
+ };
125
+ }