@strato-admin/cloudscape 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (231) hide show
  1. package/dist/Admin.d.ts +6 -2
  2. package/dist/Admin.js +14 -8
  3. package/dist/RecordLink.js +5 -4
  4. package/dist/Settings.d.ts +17 -0
  5. package/dist/Settings.js +14 -0
  6. package/dist/button/BulkDeleteButton.d.ts +4 -1
  7. package/dist/button/BulkDeleteButton.js +37 -5
  8. package/dist/button/Button.d.ts +2 -1
  9. package/dist/button/CancelButton.d.ts +6 -0
  10. package/dist/button/CancelButton.js +10 -0
  11. package/dist/button/CreateButton.js +9 -8
  12. package/dist/button/DeleteButton.d.ts +13 -0
  13. package/dist/button/DeleteButton.js +36 -0
  14. package/dist/button/EditButton.d.ts +1 -1
  15. package/dist/button/EditButton.js +10 -10
  16. package/dist/button/SaveButton.js +2 -2
  17. package/dist/button/index.d.ts +2 -0
  18. package/dist/button/index.js +2 -0
  19. package/dist/collection-hooks/interfaces.d.ts +7 -3
  20. package/dist/collection-hooks/useCollection.d.ts +1 -1
  21. package/dist/collection-hooks/useCollection.js +15 -10
  22. package/dist/create/Create.d.ts +9 -17
  23. package/dist/create/Create.js +40 -12
  24. package/dist/create/CreateHeader.d.ts +2 -2
  25. package/dist/create/CreateHeader.js +4 -5
  26. package/dist/defaults.d.ts +6 -0
  27. package/dist/defaults.js +21 -0
  28. package/dist/detail/Detail.d.ts +33 -0
  29. package/dist/detail/Detail.js +22 -0
  30. package/dist/detail/DetailHeader.d.ts +11 -0
  31. package/dist/detail/{ShowHeader.js → DetailHeader.js} +7 -5
  32. package/dist/detail/DetailHub.d.ts +27 -0
  33. package/dist/detail/DetailHub.js +63 -0
  34. package/dist/detail/KeyValuePairs.d.ts +7 -1
  35. package/dist/detail/KeyValuePairs.js +14 -8
  36. package/dist/detail/index.d.ts +3 -2
  37. package/dist/detail/index.js +3 -2
  38. package/dist/edit/Edit.d.ts +8 -19
  39. package/dist/edit/Edit.js +48 -12
  40. package/dist/edit/EditHeader.d.ts +2 -2
  41. package/dist/edit/EditHeader.js +5 -4
  42. package/dist/field/ArrayField.d.ts +26 -10
  43. package/dist/field/ArrayField.js +38 -10
  44. package/dist/field/BadgeField.d.ts +1 -1
  45. package/dist/field/BadgeField.js +1 -1
  46. package/dist/field/BooleanField.d.ts +1 -1
  47. package/dist/field/BooleanField.js +2 -2
  48. package/dist/field/CurrencyField.d.ts +1 -1
  49. package/dist/field/CurrencyField.js +1 -1
  50. package/dist/field/DateField.d.ts +1 -1
  51. package/dist/field/DateField.js +1 -1
  52. package/dist/field/IdField.d.ts +1 -1
  53. package/dist/field/IdField.js +3 -3
  54. package/dist/field/NumberField.d.ts +1 -1
  55. package/dist/field/NumberField.js +1 -1
  56. package/dist/field/ReferenceField.d.ts +1 -1
  57. package/dist/field/ReferenceField.js +4 -2
  58. package/dist/field/ReferenceManyField.d.ts +35 -4
  59. package/dist/field/ReferenceManyField.js +17 -4
  60. package/dist/field/StatusIndicatorField.d.ts +1 -1
  61. package/dist/field/StatusIndicatorField.js +6 -5
  62. package/dist/field/TextField.d.ts +1 -1
  63. package/dist/field/TextField.js +1 -1
  64. package/dist/field/types.d.ts +9 -9
  65. package/dist/form/Form.d.ts +12 -2
  66. package/dist/form/Form.js +10 -16
  67. package/dist/form/index.d.ts +1 -1
  68. package/dist/form/index.js +1 -1
  69. package/dist/hooks/useSchemaFields.d.ts +22 -0
  70. package/dist/hooks/useSchemaFields.js +45 -0
  71. package/dist/i18n/Message.d.ts +15 -0
  72. package/dist/i18n/Message.js +19 -0
  73. package/dist/i18n/RecordMessage.d.ts +14 -0
  74. package/dist/i18n/RecordMessage.js +16 -0
  75. package/dist/i18n/index.d.ts +3 -0
  76. package/dist/i18n/index.js +2 -0
  77. package/dist/i18n/types.d.ts +19 -0
  78. package/dist/i18n/types.js +1 -0
  79. package/dist/index.d.ts +5 -1
  80. package/dist/index.js +5 -1
  81. package/dist/input/ArrayInput.d.ts +33 -0
  82. package/dist/input/{AttributeEditor.js → ArrayInput.js} +18 -11
  83. package/dist/input/AutocompleteInput.d.ts +1 -1
  84. package/dist/input/AutocompleteInput.js +3 -3
  85. package/dist/input/BooleanInput.d.ts +6 -0
  86. package/dist/input/BooleanInput.js +23 -0
  87. package/dist/input/CommonInputProps.d.ts +6 -0
  88. package/dist/input/CommonInputProps.js +6 -0
  89. package/dist/input/FieldTitle.js +4 -4
  90. package/dist/input/FormField.js +12 -3
  91. package/dist/input/FormFieldContext.d.ts +1 -1
  92. package/dist/input/NumberInput.d.ts +1 -1
  93. package/dist/input/NumberInput.js +3 -3
  94. package/dist/input/ReferenceInput.d.ts +1 -1
  95. package/dist/input/ReferenceInput.js +22 -12
  96. package/dist/input/SelectInput.d.ts +1 -1
  97. package/dist/input/SelectInput.js +3 -3
  98. package/dist/input/SliderInput.d.ts +1 -1
  99. package/dist/input/SliderInput.js +4 -4
  100. package/dist/input/TextAreaInput.d.ts +1 -1
  101. package/dist/input/TextAreaInput.js +3 -3
  102. package/dist/input/TextInput.d.ts +1 -1
  103. package/dist/input/TextInput.js +6 -12
  104. package/dist/input/index.d.ts +2 -1
  105. package/dist/input/index.js +2 -1
  106. package/dist/input/types.d.ts +33 -2
  107. package/dist/layout/AppLayout.js +6 -3
  108. package/dist/layout/Notifications.d.ts +1 -0
  109. package/dist/layout/Notifications.js +51 -0
  110. package/dist/layout/Ready.d.ts +6 -0
  111. package/dist/layout/Ready.js +24 -0
  112. package/dist/layout/TopNavigation.d.ts +4 -2
  113. package/dist/layout/TopNavigation.js +7 -7
  114. package/dist/layout/index.d.ts +2 -0
  115. package/dist/layout/index.js +2 -0
  116. package/dist/list/Cards.d.ts +31 -4
  117. package/dist/list/Cards.js +81 -10
  118. package/dist/list/List.d.ts +9 -12
  119. package/dist/list/List.js +41 -11
  120. package/dist/list/Table.d.ts +8 -4
  121. package/dist/list/Table.js +55 -55
  122. package/dist/list/TableHeader.d.ts +2 -2
  123. package/dist/list/TableHeader.js +4 -5
  124. package/dist/theme/ThemeManager.js +1 -1
  125. package/package.json +9 -6
  126. package/src/Admin.tsx +35 -18
  127. package/src/RecordLink.stories.tsx +1 -1
  128. package/src/RecordLink.tsx +5 -4
  129. package/src/Settings.tsx +16 -0
  130. package/src/__mocks__/ra-core.tsx +83 -0
  131. package/src/__mocks__/strato-core.tsx +36 -42
  132. package/src/button/BulkDeleteButton.test.tsx +45 -8
  133. package/src/button/BulkDeleteButton.tsx +75 -12
  134. package/src/button/Button.tsx +31 -2
  135. package/src/button/CancelButton.tsx +20 -0
  136. package/src/button/CreateButton.tsx +12 -10
  137. package/src/button/DeleteButton.tsx +96 -0
  138. package/src/button/EditButton.tsx +13 -12
  139. package/src/button/SaveButton.tsx +2 -3
  140. package/src/button/index.ts +2 -0
  141. package/src/collection-hooks/interfaces.ts +7 -3
  142. package/src/collection-hooks/useCollection.test.ts +115 -2
  143. package/src/collection-hooks/useCollection.ts +15 -10
  144. package/src/create/Create.test.tsx +3 -3
  145. package/src/create/Create.tsx +68 -37
  146. package/src/create/CreateHeader.tsx +6 -10
  147. package/src/defaults.tsx +28 -0
  148. package/src/detail/Detail-CollectionFields.test.tsx +84 -0
  149. package/src/detail/Detail.test.tsx +91 -0
  150. package/src/detail/Detail.tsx +48 -0
  151. package/src/detail/{ShowHeader.test.tsx → DetailHeader.test.tsx} +11 -9
  152. package/src/detail/DetailHeader.tsx +42 -0
  153. package/src/detail/DetailHub.tsx +88 -0
  154. package/src/detail/KeyValuePairs.test.tsx +2 -2
  155. package/src/detail/KeyValuePairs.tsx +25 -18
  156. package/src/detail/index.ts +3 -2
  157. package/src/edit/Edit.test.tsx +7 -5
  158. package/src/edit/Edit.tsx +92 -40
  159. package/src/edit/EditHeader.tsx +7 -5
  160. package/src/field/ArrayField.tsx +57 -11
  161. package/src/field/BadgeField.tsx +2 -3
  162. package/src/field/BooleanField.test.tsx +2 -3
  163. package/src/field/BooleanField.tsx +3 -3
  164. package/src/field/CurrencyField.tsx +1 -1
  165. package/src/field/DateField.tsx +1 -1
  166. package/src/field/IdField.test.tsx +8 -20
  167. package/src/field/IdField.tsx +5 -20
  168. package/src/field/NumberField.tsx +1 -1
  169. package/src/field/ReferenceField.test.tsx +15 -6
  170. package/src/field/ReferenceField.tsx +10 -7
  171. package/src/field/ReferenceManyField.test.tsx +55 -10
  172. package/src/field/ReferenceManyField.tsx +84 -13
  173. package/src/field/StatusIndicatorField.test.tsx +7 -21
  174. package/src/field/StatusIndicatorField.tsx +8 -20
  175. package/src/field/TextField.tsx +1 -1
  176. package/src/field/types.ts +12 -13
  177. package/src/form/Form.test.tsx +8 -4
  178. package/src/form/Form.tsx +24 -19
  179. package/src/form/index.ts +1 -1
  180. package/src/hooks/useSchemaFields.ts +89 -0
  181. package/src/i18n/Message.tsx +22 -0
  182. package/src/i18n/RecordMessage.tsx +22 -0
  183. package/src/i18n/index.ts +3 -0
  184. package/src/i18n/types.ts +19 -0
  185. package/src/index.ts +5 -1
  186. package/src/input/ArrayInput.test.tsx +81 -0
  187. package/src/input/{AttributeEditor.tsx → ArrayInput.tsx} +36 -18
  188. package/src/input/AutocompleteInput.test.tsx +2 -4
  189. package/src/input/AutocompleteInput.tsx +9 -11
  190. package/src/input/BooleanInput.tsx +42 -0
  191. package/src/input/CommonInputProps.tsx +8 -0
  192. package/src/input/FieldTitle.tsx +3 -15
  193. package/src/input/FormField.tsx +78 -67
  194. package/src/input/FormFieldContext.ts +1 -1
  195. package/src/input/NumberInput.tsx +10 -7
  196. package/src/input/ReferenceInput.test.tsx +12 -2
  197. package/src/input/ReferenceInput.tsx +32 -14
  198. package/src/input/SelectInput.tsx +14 -17
  199. package/src/input/SliderInput.test.tsx +2 -3
  200. package/src/input/SliderInput.tsx +48 -38
  201. package/src/input/TextAreaInput.tsx +10 -6
  202. package/src/input/TextInput.test.tsx +2 -4
  203. package/src/input/TextInput.tsx +35 -20
  204. package/src/input/index.ts +2 -1
  205. package/src/input/types.ts +40 -8
  206. package/src/layout/AppLayout.test.tsx +23 -3
  207. package/src/layout/AppLayout.tsx +11 -8
  208. package/src/layout/Notifications.test.tsx +102 -0
  209. package/src/layout/Notifications.tsx +61 -0
  210. package/src/layout/Ready.tsx +123 -0
  211. package/src/layout/TopNavigation.test.tsx +2 -3
  212. package/src/layout/TopNavigation.tsx +9 -8
  213. package/src/layout/index.ts +2 -0
  214. package/src/list/Cards.test.tsx +320 -0
  215. package/src/list/Cards.tsx +146 -16
  216. package/src/list/List.tsx +87 -26
  217. package/src/list/Table.test.tsx +40 -5
  218. package/src/list/Table.tsx +89 -98
  219. package/src/list/TableHeader.test.tsx +15 -11
  220. package/src/list/TableHeader.tsx +6 -8
  221. package/src/theme/ThemeManager.tsx +1 -1
  222. package/dist/__mocks__/strato-core.js +0 -50
  223. package/dist/__mocks__to__delete/strato-core.js +0 -50
  224. package/dist/detail/Show.d.ts +0 -39
  225. package/dist/detail/Show.js +0 -40
  226. package/dist/detail/ShowHeader.d.ts +0 -7
  227. package/dist/input/AttributeEditor.d.ts +0 -25
  228. package/src/detail/Show.test.tsx +0 -96
  229. package/src/detail/Show.tsx +0 -104
  230. package/src/detail/ShowHeader.tsx +0 -35
  231. package/src/input/AttributeEditor.test.tsx +0 -147
@@ -0,0 +1,61 @@
1
+ import { useCallback, useEffect, useRef } from 'react';
2
+ import Flashbar, { FlashbarProps } from '@cloudscape-design/components/flashbar';
3
+ import { useNotificationContext, useTranslate, NotificationPayload } from '@strato-admin/ra-core';
4
+
5
+ const DEFAULT_AUTO_HIDE_MS = null;
6
+
7
+ let idCounter = 0;
8
+ const stableIds = new WeakMap<object, string>();
9
+ const getStableId = (obj: object): string => {
10
+ if (!stableIds.has(obj)) stableIds.set(obj, String(++idCounter));
11
+ return stableIds.get(obj)!;
12
+ };
13
+
14
+ export const Notifications = () => {
15
+ const { notifications, setNotifications } = useNotificationContext();
16
+ const translate = useTranslate();
17
+ const timers = useRef(new Map<string, ReturnType<typeof setTimeout>>());
18
+
19
+ const dismiss = useCallback(
20
+ (notification: NotificationPayload) => {
21
+ const id = getStableId(notification);
22
+ clearTimeout(timers.current.get(id));
23
+ timers.current.delete(id);
24
+ setNotifications((prev: NotificationPayload[]) => prev.filter((n) => n !== notification));
25
+ },
26
+ [setNotifications],
27
+ );
28
+
29
+ useEffect(() => {
30
+ notifications.forEach((notification) => {
31
+ const id = getStableId(notification);
32
+ if (timers.current.has(id)) return;
33
+ const ms = notification.notificationOptions?.autoHideDuration ?? DEFAULT_AUTO_HIDE_MS;
34
+ if (ms === null) return;
35
+ timers.current.set(
36
+ id,
37
+ setTimeout(() => dismiss(notification), ms),
38
+ );
39
+ });
40
+
41
+ return () => {
42
+ timers.current.forEach((timer) => clearTimeout(timer));
43
+ timers.current.clear();
44
+ };
45
+ }, [notifications, dismiss]);
46
+
47
+ if (notifications.length === 0) return null;
48
+
49
+ const items: FlashbarProps.MessageDefinition[] = notifications.map((notification) => {
50
+ const { message, type, notificationOptions } = notification;
51
+ return {
52
+ id: getStableId(notification),
53
+ type,
54
+ content: typeof message === 'string' ? translate(message, notificationOptions?.messageArgs) : message,
55
+ dismissible: true,
56
+ onDismiss: () => dismiss(notification),
57
+ };
58
+ });
59
+
60
+ return <Flashbar items={items} />;
61
+ };
@@ -0,0 +1,123 @@
1
+ import {
2
+ Box,
3
+ Button,
4
+ Container,
5
+ Header,
6
+ SpaceBetween,
7
+ Link,
8
+ Icon,
9
+ ContentLayout,
10
+ ColumnLayout,
11
+ } from '@cloudscape-design/components';
12
+ import { CodeView } from '@cloudscape-design/code-view';
13
+ import typescriptHighlight from '@cloudscape-design/code-view/highlight/typescript';
14
+ import { useDefaultTitle } from '@strato-admin/ra-core';
15
+
16
+ /**
17
+ * Default component to display when the admin has no resources.
18
+ * This is a Cloudscape-themed replacement for the default react-admin Ready component.
19
+ */
20
+ export const Ready = () => {
21
+ const defaultTitle = useDefaultTitle();
22
+ const appTitle = typeof defaultTitle === 'string' ? defaultTitle : 'Strato Admin';
23
+
24
+ const codeSnippet = `import { Admin, ResourceSchema, TextField } from '@strato-admin/admin';
25
+
26
+ export const App = () => (
27
+ <Admin dataProvider={...}>
28
+ <ResourceSchema name="posts" >
29
+ <TextField source="id" />
30
+ </ResourceSchema>
31
+ </Admin>
32
+ );`;
33
+
34
+ return (
35
+ <ContentLayout
36
+ maxContentWidth={1040}
37
+ headerVariant="high-contrast"
38
+ header={
39
+ <Box padding={{ vertical: 'xxxl' }} textAlign="center">
40
+ <SpaceBetween size="l">
41
+ <Header variant="h1" description="Your Strato Admin application is properly configured and ready to go.">
42
+ Welcome to {appTitle}
43
+ </Header>
44
+ <Box variant="p">
45
+ Strato Admin uses a <strong>Schema First</strong> approach to build admin interfaces efficiently. Start by
46
+ defining your resources and let us handle the rest.
47
+ </Box>
48
+ <Box margin={{ top: 'm' }}>
49
+ <SpaceBetween direction="horizontal" size="xs">
50
+ <Button
51
+ variant="primary"
52
+ href="https://strato-admin.dev/docs/getting-started/tutorial"
53
+ iconName="external"
54
+ iconAlign="right"
55
+ >
56
+ Get started with the tutorial
57
+ </Button>
58
+ <Button href="https://github.com/vadimgu/strato-admin" iconName="external" iconAlign="right">
59
+ View on GitHub
60
+ </Button>
61
+ </SpaceBetween>
62
+ </Box>
63
+ </SpaceBetween>
64
+ </Box>
65
+ }
66
+ >
67
+ <SpaceBetween size="xxl">
68
+ {/* Features / Details Section */}
69
+ <SpaceBetween size="l">
70
+ <Container header={<Header variant="h2">How to add your first resource</Header>}>
71
+ <SpaceBetween size="m">
72
+ <Box variant="p">
73
+ Add a <code>&lt;ResourceSchema&gt;</code> component as a child of your <code>&lt;Admin&gt;</code> to
74
+ define your data model.
75
+ </Box>
76
+ <CodeView content={codeSnippet} highlight={typescriptHighlight} />
77
+ </SpaceBetween>
78
+ </Container>
79
+
80
+ <Container header={<Header variant="h2">Next steps</Header>}>
81
+ <ColumnLayout columns={2} variant="text-grid">
82
+ <div>
83
+ <Box variant="h3" fontWeight="bold">
84
+ <Icon name="status-info" variant="link" /> Core Concepts
85
+ </Box>
86
+ <Box variant="p">
87
+ Learn how{' '}
88
+ <Link href="https://strato-admin.dev/docs/core-concepts/schema" external>
89
+ Schemas
90
+ </Link>{' '}
91
+ work to automate your UI.
92
+ </Box>
93
+ </div>
94
+ <div>
95
+ <Box variant="h3" fontWeight="bold">
96
+ <Icon name="star" variant="warning" /> Design System
97
+ </Box>
98
+ <Box variant="p">
99
+ Explore the{' '}
100
+ <Link href="https://cloudscape.design/" external>
101
+ Cloudscape design system
102
+ </Link>{' '}
103
+ components.
104
+ </Box>
105
+ </div>
106
+ </ColumnLayout>
107
+ </Container>
108
+ </SpaceBetween>
109
+
110
+ {/* Footer */}
111
+ <Box textAlign="center" color="text-label" margin={{ top: 'xs' }} padding={{ bottom: 'xxxl' }}>
112
+ Built with{' '}
113
+ <Link href="https://www.strato-admin.dev" external>
114
+ Strato Admin
115
+ </Link>
116
+ .
117
+ </Box>
118
+ </SpaceBetween>
119
+ </ContentLayout>
120
+ );
121
+ };
122
+
123
+ export default Ready;
@@ -1,12 +1,11 @@
1
-
2
1
  import { render } from '@testing-library/react';
3
2
  import { vi, describe, it, expect, beforeEach } from 'vitest';
4
- import { useAuthProvider } from '@strato-admin/core';
3
+ import { useAuthProvider } from '@strato-admin/ra-core';
5
4
  import CloudscapeTopNavigation from '@cloudscape-design/components/top-navigation';
6
5
  import TopNavigation from './TopNavigation';
7
6
 
8
7
  // Mock ra-core
9
- vi.mock('@strato-admin/core', () => ({
8
+ vi.mock('@strato-admin/ra-core', () => ({
10
9
  useLocale: vi.fn(() => 'en'),
11
10
  useSetLocale: vi.fn(),
12
11
  useLocales: vi.fn(() => []),
@@ -1,8 +1,10 @@
1
1
  import CloudscapeTopNavigation, { TopNavigationProps } from '@cloudscape-design/components/top-navigation';
2
- import { useLocale, useSetLocale, useLocales, useTranslate, useAuthProvider, useStore } from '@strato-admin/core';
2
+ import { useLocale, useSetLocale, useLocales, useTranslate, useAuthProvider, useStore } from '@strato-admin/ra-core';
3
3
 
4
- export interface MyTopNavigationProps extends Omit<TopNavigationProps, 'identity'> {
4
+ export interface MyTopNavigationProps {
5
5
  identity?: TopNavigationProps.Identity;
6
+ utilities?: ReadonlyArray<TopNavigationProps.Utility>;
7
+ extraUtilities?: ReadonlyArray<TopNavigationProps.Utility>;
6
8
  }
7
9
 
8
10
  const LightModeIcon = (
@@ -19,7 +21,7 @@ const DarkModeIcon = (
19
21
  </svg>
20
22
  );
21
23
 
22
- export const TopNavigation = ({ utilities: providedUtilities, identity, ...props }: MyTopNavigationProps) => {
24
+ export const TopNavigation = ({ utilities: providedUtilities, identity, extraUtilities }: MyTopNavigationProps) => {
23
25
  const locale = useLocale();
24
26
  const setLocale = useSetLocale();
25
27
  const locales = useLocales();
@@ -59,15 +61,15 @@ export const TopNavigation = ({ utilities: providedUtilities, identity, ...props
59
61
  if (authProvider) {
60
62
  autoUtilities.push({
61
63
  type: 'menu-dropdown',
62
- text: translate('ra.auth.user_menu', { _: 'User' }),
64
+ text: translate('strato.auth.user_menu', { _: 'User' }),
63
65
  iconName: 'user-profile',
64
66
  items: [
65
- { id: 'profile', text: translate('ra.auth.profile', { _: 'Profile' }) },
66
- { id: 'signout', text: translate('ra.auth.logout', { _: 'Sign out' }) },
67
+ { id: 'profile', text: translate('strato.auth.profile', { _: 'Profile' }) },
68
+ { id: 'signout', text: translate('strato.auth.logout', { _: 'Sign out' }) },
67
69
  ],
68
70
  });
69
71
  }
70
- utilities = autoUtilities;
72
+ utilities = extraUtilities ? [...autoUtilities, ...extraUtilities] : autoUtilities;
71
73
  }
72
74
 
73
75
  return (
@@ -75,7 +77,6 @@ export const TopNavigation = ({ utilities: providedUtilities, identity, ...props
75
77
  <CloudscapeTopNavigation
76
78
  identity={identity || { title: 'Strato Admin', href: '/' }}
77
79
  utilities={utilities}
78
- {...props}
79
80
  />
80
81
  </div>
81
82
  );
@@ -1,2 +1,4 @@
1
1
  export * from './AppLayout';
2
2
  export * from './TopNavigation';
3
+ export * from './Ready';
4
+ export * from './Notifications';
@@ -0,0 +1,320 @@
1
+ import React from 'react';
2
+ import { render, cleanup } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
4
+ import { useTranslate, useTranslateLabel } from '@strato-admin/ra-core';
5
+ import { useResourceSchema } from '@strato-admin/core';
6
+ import { useCollection } from '../collection-hooks';
7
+ import Cards from './Cards';
8
+ import CloudscapeCards from '@cloudscape-design/components/cards';
9
+ import KeyValuePairs from '../detail/KeyValuePairs';
10
+
11
+ vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
12
+ vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
13
+
14
+ // Mock useCollection
15
+ vi.mock('../collection-hooks', () => ({
16
+ useCollection: vi.fn(),
17
+ }));
18
+
19
+ // Mock Cloudscape components
20
+ vi.mock('@cloudscape-design/components/cards', () => ({
21
+ default: vi.fn(({ items, cardDefinition, header, selectionType, filter, preferences }: any) => (
22
+ <div data-testid="cloudscape-cards" data-selection-type={selectionType}>
23
+ {header && <div data-testid="cards-header">{header}</div>}
24
+ {filter && <div data-testid="cards-filter">{filter}</div>}
25
+ {preferences && <div data-testid="cards-preferences">{preferences}</div>}
26
+ {items.map((item: any, index: number) => (
27
+ <div key={index} data-testid="card">
28
+ {cardDefinition.sections.map((section: any) => (
29
+ <div key={section.id} data-testid={`section-${section.id}`}>
30
+ {section.content(item)}
31
+ </div>
32
+ ))}
33
+ </div>
34
+ ))}
35
+ </div>
36
+ )),
37
+ }));
38
+
39
+ vi.mock('@cloudscape-design/components/pagination', () => ({
40
+ default: () => <div data-testid="pagination" />,
41
+ }));
42
+
43
+ vi.mock('@cloudscape-design/components/text-filter', () => ({
44
+ default: vi.fn(({ filteringPlaceholder }: any) => <div data-testid="text-filter">{filteringPlaceholder}</div>),
45
+ }));
46
+
47
+ vi.mock('@cloudscape-design/components/collection-preferences', () => ({
48
+ default: vi.fn(() => <div data-testid="collection-preferences" />),
49
+ }));
50
+
51
+ vi.mock('../detail/KeyValuePairs', () => ({
52
+ default: vi.fn(({ children }: any) => <div data-testid="key-value-pairs">{children}</div>),
53
+ }));
54
+
55
+ vi.mock('./TableHeader', () => ({
56
+ default: vi.fn(({ title, description, actions }: any) => (
57
+ <div data-testid="mock-table-header">
58
+ <div data-testid="header-title">{title}</div>
59
+ <div data-testid="header-description">{description}</div>
60
+ <div data-testid="header-actions">{actions}</div>
61
+ </div>
62
+ )),
63
+ }));
64
+
65
+ describe('Cards', () => {
66
+ beforeEach(() => {
67
+ vi.clearAllMocks();
68
+ (useCollection as any).mockReturnValue({
69
+ items: [{ id: 1, name: 'Product 1', price: 100, description: 'Desc 1' }],
70
+ paginationProps: {},
71
+ collectionProps: {},
72
+ filterProps: { filteringPlaceholder: 'Search...' },
73
+ preferencesProps: {
74
+ preferences: {},
75
+ onConfirm: vi.fn(),
76
+ },
77
+ });
78
+ });
79
+
80
+ afterEach(() => {
81
+ cleanup();
82
+ });
83
+
84
+ it('should honor listInclude from resource schema', () => {
85
+ (useResourceSchema as any).mockReturnValue({
86
+ fieldSchema: [
87
+ <div key="name" source="name" data-testid="field-name" />,
88
+ <div key="price" source="price" data-testid="field-price" />,
89
+ <div key="description" source="description" data-testid="field-description" />,
90
+ ],
91
+ listInclude: ['name', 'price'],
92
+ });
93
+
94
+ render(<Cards />);
95
+
96
+ // Check that KeyValuePairs received only name and price fields
97
+ const keyValuePairsProps = (KeyValuePairs as any).mock.calls[0][0];
98
+ const children = React.Children.toArray(keyValuePairsProps.children);
99
+
100
+ expect(children).toHaveLength(2);
101
+ expect((children[0] as any).props.source).toBe('name');
102
+ expect((children[1] as any).props.source).toBe('price');
103
+ });
104
+
105
+ it('should honor listExclude from resource schema', () => {
106
+ (useResourceSchema as any).mockReturnValue({
107
+ fieldSchema: [
108
+ <div key="name" source="name" data-testid="field-name" />,
109
+ <div key="price" source="price" data-testid="field-price" />,
110
+ <div key="description" source="description" data-testid="field-description" />,
111
+ ],
112
+ listExclude: ['description'],
113
+ });
114
+
115
+ render(<Cards />);
116
+
117
+ const keyValuePairsProps = (KeyValuePairs as any).mock.calls[0][0];
118
+ const children = React.Children.toArray(keyValuePairsProps.children);
119
+
120
+ expect(children).toHaveLength(2);
121
+ expect((children[0] as any).props.source).toBe('name');
122
+ expect((children[1] as any).props.source).toBe('price');
123
+ });
124
+
125
+ it('should honor include prop over listInclude from schema', () => {
126
+ (useResourceSchema as any).mockReturnValue({
127
+ fieldSchema: [
128
+ <div key="name" source="name" data-testid="field-name" />,
129
+ <div key="price" source="price" data-testid="field-price" />,
130
+ <div key="description" source="description" data-testid="field-description" />,
131
+ ],
132
+ listInclude: ['name'],
133
+ });
134
+
135
+ render(<Cards include={['price', 'description']} />);
136
+
137
+ const keyValuePairsProps = (KeyValuePairs as any).mock.calls[0][0];
138
+ const children = React.Children.toArray(keyValuePairsProps.children);
139
+
140
+ expect(children).toHaveLength(2);
141
+ expect((children[0] as any).props.source).toBe('price');
142
+ expect((children[1] as any).props.source).toBe('description');
143
+ });
144
+
145
+ it('should support declarative children', () => {
146
+ (useResourceSchema as any).mockReturnValue({
147
+ fieldSchema: [<div key="name" source="name" data-testid="field-name" />],
148
+ });
149
+
150
+ render(
151
+ <Cards>
152
+ <div source="custom" data-testid="field-custom" />
153
+ </Cards>,
154
+ );
155
+
156
+ const keyValuePairsProps = (KeyValuePairs as any).mock.calls[0][0];
157
+ const children = React.Children.toArray(keyValuePairsProps.children);
158
+
159
+ expect(children).toHaveLength(1);
160
+ expect((children[0] as any).props.source).toBe('custom');
161
+ });
162
+
163
+ it('should render TableHeader with correct title and description', () => {
164
+ (useResourceSchema as any).mockReturnValue({
165
+ fieldSchema: [],
166
+ label: 'Products',
167
+ });
168
+
169
+ const { getByTestId } = render(<Cards title="Custom Title" description="Custom Description" />);
170
+
171
+ expect(getByTestId('header-title').textContent).toBe('Custom Title');
172
+ expect(getByTestId('header-description').textContent).toBe('Custom Description');
173
+ });
174
+
175
+ it('should use schema label as default title', () => {
176
+ (useResourceSchema as any).mockReturnValue({
177
+ fieldSchema: [],
178
+ label: 'Schema Products',
179
+ });
180
+
181
+ const { getByTestId } = render(<Cards />);
182
+
183
+ expect(getByTestId('header-title').textContent).toBe('Schema Products');
184
+ });
185
+
186
+ it('should pass selectionType to CloudscapeCards', () => {
187
+ (useResourceSchema as any).mockReturnValue({
188
+ fieldSchema: [],
189
+ });
190
+
191
+ const { getByTestId } = render(<Cards selectionType="multi" />);
192
+
193
+ expect(getByTestId('cloudscape-cards').getAttribute('data-selection-type')).toBe('multi');
194
+ });
195
+
196
+ it('should default selectionType to multi if canDelete is true', () => {
197
+ (useResourceSchema as any).mockReturnValue({
198
+ fieldSchema: [],
199
+ definition: {
200
+ options: { canDelete: true },
201
+ },
202
+ });
203
+
204
+ const { getByTestId } = render(<Cards />);
205
+
206
+ expect(getByTestId('cloudscape-cards').getAttribute('data-selection-type')).toBe('multi');
207
+ });
208
+
209
+ it('should render TextFilter by default', () => {
210
+ (useResourceSchema as any).mockReturnValue({
211
+ fieldSchema: [],
212
+ });
213
+
214
+ const { getByTestId } = render(<Cards />);
215
+
216
+ expect(getByTestId('text-filter')).toBeDefined();
217
+ });
218
+
219
+ it('should not render TextFilter if filtering is false', () => {
220
+ (useResourceSchema as any).mockReturnValue({
221
+ fieldSchema: [],
222
+ });
223
+
224
+ const { queryByTestId } = render(<Cards filtering={false} />);
225
+
226
+ expect(queryByTestId('text-filter')).toBeNull();
227
+ });
228
+
229
+ it('should pass filteringPlaceholder to TextFilter', () => {
230
+ (useResourceSchema as any).mockReturnValue({
231
+ fieldSchema: [],
232
+ });
233
+
234
+ const { getByTestId } = render(<Cards filteringPlaceholder="Custom Search" />);
235
+
236
+ expect(getByTestId('text-filter').textContent).toBe('Custom Search');
237
+ });
238
+
239
+ it('should render CollectionPreferences by default', () => {
240
+ (useResourceSchema as any).mockReturnValue({
241
+ fieldSchema: [],
242
+ });
243
+
244
+ const { getByTestId } = render(<Cards />);
245
+
246
+ expect(getByTestId('collection-preferences')).toBeDefined();
247
+ });
248
+
249
+ it('should pass pageSizeOptions to useCollection', () => {
250
+ (useResourceSchema as any).mockReturnValue({
251
+ fieldSchema: [],
252
+ });
253
+
254
+ const pageSizeOptions = [{ value: 5, label: '5 items' }];
255
+ render(<Cards pageSizeOptions={pageSizeOptions} />);
256
+
257
+ expect(useCollection).toHaveBeenCalledWith(
258
+ expect.objectContaining({
259
+ preferences: expect.objectContaining({
260
+ pageSizeOptions,
261
+ } as any),
262
+ }),
263
+ );
264
+ });
265
+
266
+ it('should filter children based on visibleContent preference', () => {
267
+ (useResourceSchema as any).mockReturnValue({
268
+ fieldSchema: [
269
+ <div key="name" source="name" data-testid="field-name" />,
270
+ <div key="price" source="price" data-testid="field-price" />,
271
+ ],
272
+ });
273
+
274
+ (useCollection as any).mockReturnValue({
275
+ items: [{ id: 1, name: 'Product 1', price: 100 }],
276
+ paginationProps: {},
277
+ collectionProps: {},
278
+ filterProps: {},
279
+ preferencesProps: {
280
+ preferences: {
281
+ visibleContent: ['name'],
282
+ },
283
+ },
284
+ });
285
+
286
+ render(<Cards />);
287
+
288
+ const keyValuePairsProps = (KeyValuePairs as any).mock.calls[0][0];
289
+ const children = React.Children.toArray(keyValuePairsProps.children);
290
+
291
+ expect(children).toHaveLength(1);
292
+ expect((children[0] as any).props.source).toBe('name');
293
+ });
294
+
295
+ it('should not filter children if visibleContent preference is not set', () => {
296
+ (useResourceSchema as any).mockReturnValue({
297
+ fieldSchema: [
298
+ <div key="name" source="name" data-testid="field-name" />,
299
+ <div key="price" source="price" data-testid="field-price" />,
300
+ ],
301
+ });
302
+
303
+ (useCollection as any).mockReturnValue({
304
+ items: [{ id: 1, name: 'Product 1', price: 100 }],
305
+ paginationProps: {},
306
+ collectionProps: {},
307
+ filterProps: {},
308
+ preferencesProps: {
309
+ preferences: {},
310
+ },
311
+ });
312
+
313
+ render(<Cards />);
314
+
315
+ const keyValuePairsProps = (KeyValuePairs as any).mock.calls[0][0];
316
+ const children = React.Children.toArray(keyValuePairsProps.children);
317
+
318
+ expect(children).toHaveLength(2);
319
+ });
320
+ });