@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
package/src/Admin.tsx ADDED
@@ -0,0 +1,123 @@
1
+ import React, { ReactNode, useState, useEffect } from 'react';
2
+ import {
3
+ CoreAdmin,
4
+ type CoreAdminProps,
5
+ type AdminChildren,
6
+ SchemaRegistryProvider,
7
+ localStorageStore,
8
+ useLocaleState,
9
+ registerDefaultResourceComponents,
10
+ registerFieldInputMapping,
11
+ } from '@strato-admin/core';
12
+ import { icuI18nProvider } from '@strato-admin/i18n';
13
+ import englishMessages from '@strato-admin/language-en';
14
+ import AppLayout from './layout/AppLayout';
15
+ import { List } from './list';
16
+ import { Create } from './create';
17
+ import { Edit } from './edit';
18
+ import { Show } from './detail';
19
+
20
+ import { TextField, NumberField, CurrencyField, ReferenceField } from './field';
21
+ import { TextInput, NumberInput, ReferenceInput } from './input';
22
+
23
+ import { I18nProvider, importMessages, I18nProviderProps } from '@cloudscape-design/components/i18n';
24
+
25
+ // Register Cloudscape themed components as defaults for ResourceSchema.
26
+ // This is done once, here, to avoid circular dependencies at the module level.
27
+ registerDefaultResourceComponents({
28
+ list: List,
29
+ create: Create,
30
+ edit: Edit,
31
+ show: Show,
32
+ });
33
+
34
+ registerFieldInputMapping(
35
+ new Map<any, any>([
36
+ [TextField, TextInput],
37
+ [NumberField, NumberInput],
38
+ [CurrencyField, NumberInput],
39
+ [ReferenceField, ReferenceInput],
40
+ ])
41
+ );
42
+
43
+ export interface AdminProps extends CoreAdminProps {
44
+ children?: AdminChildren;
45
+ title?: string;
46
+ }
47
+
48
+ const defaultI18nProvider = icuI18nProvider(() => englishMessages as any);
49
+ const defaultStore = localStorageStore();
50
+
51
+ /**
52
+ * Syncs Cloudscape I18n with React-Admin locale.
53
+ */
54
+ const CloudscapeI18n = ({ children }: { children: ReactNode }) => {
55
+ const [locale] = useLocaleState();
56
+ const [messages, setMessages] = useState<ReadonlyArray<I18nProviderProps.Messages>>([]);
57
+
58
+ useEffect(() => {
59
+ let active = true;
60
+ importMessages(locale).then((msgs) => {
61
+ if (active) {
62
+ setMessages(msgs);
63
+ }
64
+ });
65
+ return () => {
66
+ active = false;
67
+ };
68
+ }, [locale]);
69
+
70
+ if (messages.length === 0) {
71
+ return null;
72
+ }
73
+
74
+ return (
75
+ <I18nProvider locale={locale} messages={messages}>
76
+ {children}
77
+ </I18nProvider>
78
+ );
79
+ };
80
+
81
+ const CloudscapeLayout = (props: any) => {
82
+ const { children, layout: Layout = AppLayout, ...rest } = props;
83
+ return (
84
+ <CloudscapeI18n>
85
+ <Layout {...rest}>{children}</Layout>
86
+ </CloudscapeI18n>
87
+ );
88
+ };
89
+
90
+ /**
91
+ * The root component of a Strato Admin application.
92
+ *
93
+ * It sets up the data fetching context, authentication, and layout using Cloudscape.
94
+ *
95
+ * @example
96
+ * <Admin dataProvider={myDataProvider} title="My App">
97
+ * <Resource name="posts" list={PostList} />
98
+ * </Admin>
99
+ */
100
+ export const Admin = ({
101
+ children,
102
+ title,
103
+ layout: Layout = AppLayout,
104
+ i18nProvider = defaultI18nProvider,
105
+ store = defaultStore,
106
+ ...props
107
+ }: AdminProps) => {
108
+ return (
109
+ <SchemaRegistryProvider>
110
+ <CoreAdmin
111
+ {...props}
112
+ layout={(layoutProps: any) => <CloudscapeLayout {...layoutProps} layout={Layout} />}
113
+ title={title}
114
+ i18nProvider={i18nProvider}
115
+ store={store}
116
+ >
117
+ {children}
118
+ </CoreAdmin>
119
+ </SchemaRegistryProvider>
120
+ );
121
+ };
122
+
123
+ export default Admin;
@@ -0,0 +1,56 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import RecordLink from './RecordLink';
3
+ import { withRaContext } from './stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof RecordLink> = {
6
+ title: 'Components/RecordLink',
7
+ component: RecordLink,
8
+ tags: ['autodocs'],
9
+ decorators: [withRaContext({ id: 123, name: 'Sample Record' }, 'samples')],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof RecordLink>;
14
+
15
+ export const Default: Story = {
16
+ args: {
17
+ link: 'edit',
18
+ children: 'Edit this record',
19
+ },
20
+ };
21
+
22
+ export const Show: Story = {
23
+ args: {
24
+ link: 'show',
25
+ children: 'Show details',
26
+ },
27
+ };
28
+
29
+ export const CustomString: Story = {
30
+ args: {
31
+ link: '/custom-path/123',
32
+ children: 'Go to custom path',
33
+ },
34
+ };
35
+
36
+ export const FunctionLink: Story = {
37
+ args: {
38
+ link: (record) => `/records/${record.id}/manage`,
39
+ children: 'Manage Record',
40
+ },
41
+ };
42
+
43
+ export const NoLink: Story = {
44
+ args: {
45
+ link: false,
46
+ children: 'Plain text (link=false)',
47
+ },
48
+ };
49
+
50
+ export const ResourceOverride: Story = {
51
+ args: {
52
+ link: 'edit',
53
+ resource: 'other-resource',
54
+ children: 'Edit other-resource',
55
+ },
56
+ };
@@ -0,0 +1,67 @@
1
+ import React from 'react';
2
+ import Link from '@cloudscape-design/components/link';
3
+ import { useCreatePath, useResourceContext, useRecordContext } from '@strato-admin/core';
4
+ import { useNavigate } from 'react-router-dom';
5
+
6
+ export type RecordLinkType = string | boolean | ((record: any, reference?: string) => string);
7
+
8
+ export interface RecordLinkProps {
9
+ link?: RecordLinkType;
10
+ resource?: string;
11
+ children: React.ReactNode;
12
+ }
13
+
14
+ const RecordLinkImpl = ({ link, resource, children }: RecordLinkProps) => {
15
+ const contextResource = useResourceContext();
16
+ const record = useRecordContext();
17
+ const createPath = useCreatePath();
18
+ const navigate = useNavigate();
19
+
20
+ const finalResource = resource ?? contextResource;
21
+
22
+ if (!record || !finalResource) {
23
+ return <>{children}</>;
24
+ }
25
+
26
+ let href = '';
27
+ if (typeof link === 'function') {
28
+ href = link(record, finalResource);
29
+ } else if (link === true) {
30
+ href = createPath({ resource: finalResource, id: record.id, type: 'edit' });
31
+ } else if (link === 'edit' || link === 'show') {
32
+ href = createPath({ resource: finalResource, id: record.id, type: link });
33
+ } else if (typeof link === 'string') {
34
+ href = link;
35
+ }
36
+
37
+ if (!href) {
38
+ return <>{children}</>;
39
+ }
40
+
41
+ return (
42
+ <Link
43
+ href={href}
44
+ onFollow={(event) => {
45
+ if (!event.detail.external) {
46
+ event.preventDefault();
47
+ navigate(href);
48
+ }
49
+ }}
50
+ >
51
+ {children}
52
+ </Link>
53
+ );
54
+ };
55
+
56
+ const RecordLink = ({ link, resource, children }: RecordLinkProps) => {
57
+ if (!link) {
58
+ return <>{children}</>;
59
+ }
60
+ return (
61
+ <RecordLinkImpl link={link} resource={resource}>
62
+ {children}
63
+ </RecordLinkImpl>
64
+ );
65
+ };
66
+
67
+ export default RecordLink;
@@ -0,0 +1,52 @@
1
+ import React from 'react';
2
+ import { vi } from 'vitest';
3
+
4
+ export const useTranslate = vi.fn(() => (key: string, options: any) => options?._ || key);
5
+ export const useTranslateLabel = vi.fn(() => (label: any) => typeof label === 'string' ? label : label?.source || '');
6
+ export const useGetResourceLabel = vi.fn(() => (resource: string) => {
7
+ if (resource === 'products') return 'Products';
8
+ if (resource === 'categories') return 'Categories';
9
+ return resource;
10
+ });
11
+ export const useResourceDefinitions = vi.fn(() => ({}));
12
+ export const useResourceDefinition = vi.fn(() => ({
13
+ name: 'products',
14
+ options: { label: 'Products' },
15
+ }));
16
+ export const useResourceContext = vi.fn();
17
+ export const useRecordContext = vi.fn((record) => record || {});
18
+ export const useDefaultTitle = vi.fn(() => '');
19
+ export const useInput = vi.fn();
20
+ export const useChoicesContext = vi.fn(() => ({ allChoices: [], isPending: false }));
21
+ export const useGetRecordRepresentation = vi.fn(() => (record: any) => record?.name || record?.id || record);
22
+ export const useFieldSchema = vi.fn(() => []);
23
+ export const useInputSchema = vi.fn(() => []);
24
+ export const ResourceSchemaProvider = vi.fn(({ children }: any) => children);
25
+ export const ValidationError = vi.fn(({ error }: any) => <span>{error}</span>);
26
+ export const useStore = vi.fn(() => ['light', vi.fn()]);
27
+ export const useSaveContext = vi.fn(() => ({ save: vi.fn(), saving: false }));
28
+ export const useNotify = vi.fn(() => vi.fn());
29
+ export const useRedirect = vi.fn(() => vi.fn());
30
+ export const useRefresh = vi.fn(() => vi.fn());
31
+ export const useFieldValue = vi.fn(({ source, record }: any) => record?.[source]);
32
+ export const useCreatePath = vi.fn(() => (params: any) => `/${params.resource}/${params.id}/${params.type}`);
33
+ export const useLocale = vi.fn(() => 'en');
34
+ export const useBulkDeleteController = vi.fn(() => ({
35
+ handleDelete: vi.fn(),
36
+ isPending: false,
37
+ isLoading: false,
38
+ }));
39
+
40
+ export const CreateBase = vi.fn(({ children }: any) => <div data-testid="create-base">{children}</div>);
41
+ export const EditBase = vi.fn(({ children }: any) => <div data-testid="edit-base">{children}</div>);
42
+ export const ShowBase = vi.fn(({ children }: any) => <div data-testid="show-base">{children}</div>);
43
+ export const ListBase = vi.fn(({ children }: any) => <div data-testid="list-base">{children}</div>);
44
+
45
+ export const useShowContext = vi.fn(() => ({ isLoading: false, record: {}, defaultTitle: 'Products' }));
46
+ export const useEditContext = vi.fn(() => ({ isLoading: false, record: {}, defaultTitle: 'Products' }));
47
+ export const useCreateContext = vi.fn(() => ({ isLoading: false, record: {}, defaultTitle: 'Products' }));
48
+ export const useListContext = vi.fn(() => ({ total: 0, isPending: false, selectedIds: [], defaultTitle: 'Products' }));
49
+
50
+ export const Form = vi.fn(({ children }: any) => <div data-testid="ra-form">{children}</div>);
51
+ export const FieldTitle = vi.fn(({ label, source }: any) => <span data-testid="field-title">{label || source}</span>);
52
+ export const RecordContextProvider = vi.fn(({ children }: any) => <>{children}</>);
@@ -0,0 +1,59 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { BulkDeleteButton } from './BulkDeleteButton';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof BulkDeleteButton> = {
6
+ title: 'Components/BulkDeleteButton',
7
+ component: BulkDeleteButton,
8
+ tags: ['autodocs'],
9
+ };
10
+
11
+ export default meta;
12
+ type Story = StoryObj<typeof BulkDeleteButton>;
13
+
14
+ export const Default: Story = {
15
+ decorators: [
16
+ withRaContext(
17
+ undefined,
18
+ 'posts',
19
+ { posts: { name: 'posts' } },
20
+ {
21
+ selectedIds: [1, 2, 3],
22
+ resource: 'posts',
23
+ },
24
+ ),
25
+ ],
26
+ args: {},
27
+ };
28
+
29
+ export const Disabled: Story = {
30
+ decorators: [
31
+ withRaContext(
32
+ undefined,
33
+ 'posts',
34
+ { posts: { name: 'posts' } },
35
+ {
36
+ selectedIds: [],
37
+ resource: 'posts',
38
+ },
39
+ ),
40
+ ],
41
+ args: {},
42
+ };
43
+
44
+ export const Loading: Story = {
45
+ decorators: [
46
+ withRaContext(
47
+ undefined,
48
+ 'posts',
49
+ { posts: { name: 'posts' } },
50
+ {
51
+ selectedIds: [1, 2, 3],
52
+ resource: 'posts',
53
+ },
54
+ ),
55
+ ],
56
+ args: {
57
+ label: 'Deleting...',
58
+ },
59
+ };
@@ -0,0 +1,64 @@
1
+
2
+ import { render, screen, fireEvent, cleanup } from '@testing-library/react';
3
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
4
+ import { BulkDeleteButton } from './BulkDeleteButton';
5
+ import { useListContext, useBulkDeleteController, useTranslate, useResourceDefinition } from '@strato-admin/core';
6
+
7
+ vi.mock('@strato-admin/core', () => ({
8
+ useListContext: vi.fn(),
9
+ useBulkDeleteController: vi.fn(),
10
+ useTranslate: vi.fn(),
11
+ useResourceDefinition: vi.fn(),
12
+ }));
13
+
14
+ describe('BulkDeleteButton', () => {
15
+ beforeEach(() => {
16
+ vi.clearAllMocks();
17
+ (useTranslate as any).mockReturnValue((key: string, options: any) => options?._ || key);
18
+ (useBulkDeleteController as any).mockReturnValue({
19
+ handleDelete: vi.fn(),
20
+ isPending: false,
21
+ isLoading: false,
22
+ });
23
+ (useResourceDefinition as any).mockReturnValue({ options: {} });
24
+ });
25
+
26
+ afterEach(() => {
27
+ cleanup();
28
+ });
29
+
30
+ it('should be disabled when no ids are selected', () => {
31
+ (useListContext as any).mockReturnValue({ selectedIds: [] });
32
+ render(<BulkDeleteButton />);
33
+ expect(screen.getByRole('button')).toHaveProperty('disabled', true);
34
+ });
35
+
36
+ it('should render when ids are selected', () => {
37
+ (useListContext as any).mockReturnValue({ selectedIds: [1, 2] });
38
+ render(<BulkDeleteButton />);
39
+ expect(screen.getByRole('button')).toBeDefined();
40
+ expect(screen.getByText('Delete')).toBeDefined();
41
+ });
42
+
43
+ it('should return null when canDelete is false', () => {
44
+ (useResourceDefinition as any).mockReturnValue({ options: { canDelete: false } });
45
+ (useListContext as any).mockReturnValue({ selectedIds: [1, 2] });
46
+ const { container } = render(<BulkDeleteButton />);
47
+ expect(container.firstChild).toBeNull();
48
+ });
49
+
50
+ it('should call handleDelete on click', () => {
51
+ const handleDelete = vi.fn();
52
+ (useBulkDeleteController as any).mockReturnValue({
53
+ handleDelete,
54
+ isPending: false,
55
+ isLoading: false,
56
+ });
57
+ (useListContext as any).mockReturnValue({ selectedIds: [1, 2] });
58
+
59
+ render(<BulkDeleteButton />);
60
+ fireEvent.click(screen.getByRole('button'));
61
+
62
+ expect(handleDelete).toHaveBeenCalled();
63
+ });
64
+ });
@@ -0,0 +1,41 @@
1
+
2
+ import { useBulkDeleteController, useTranslate, useListContext, useResourceDefinition } from '@strato-admin/core';
3
+ import { Button } from './Button';
4
+
5
+ export interface BulkDeleteButtonProps {
6
+ label?: string;
7
+ variant?: 'primary' | 'normal' | 'link';
8
+ mutationMode?: 'undoable' | 'optimistic' | 'pessimistic';
9
+ }
10
+
11
+ export const BulkDeleteButton = ({
12
+ label,
13
+ variant = 'normal',
14
+ mutationMode = 'pessimistic',
15
+ }: BulkDeleteButtonProps) => {
16
+ const translate = useTranslate();
17
+ const { selectedIds } = useListContext();
18
+ const { options } = useResourceDefinition();
19
+ const { handleDelete, isPending, isLoading } = useBulkDeleteController({
20
+ mutationMode,
21
+ });
22
+
23
+ if (options?.canDelete === false) {
24
+ return null;
25
+ }
26
+
27
+ const isBusy = isPending || isLoading;
28
+
29
+ return (
30
+ <Button
31
+ variant={variant}
32
+ onClick={handleDelete}
33
+ loading={isBusy}
34
+ disabled={isBusy || !selectedIds || selectedIds.length === 0}
35
+ >
36
+ {label || translate('ra.action.delete', { _: 'Delete' })}
37
+ </Button>
38
+ );
39
+ };
40
+
41
+ export default BulkDeleteButton;
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Button } from './Button';
3
+
4
+ const meta: Meta<typeof Button> = {
5
+ title: 'Components/Button',
6
+ component: Button,
7
+ tags: ['autodocs'],
8
+ argTypes: {
9
+ variant: {
10
+ options: ['primary', 'normal', 'link', 'icon', 'inline-icon'],
11
+ control: { type: 'select' },
12
+ },
13
+ },
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof Button>;
18
+
19
+ export const Primary: Story = {
20
+ args: {
21
+ variant: 'primary',
22
+ children: 'Save Changes',
23
+ },
24
+ };
25
+
26
+ export const Normal: Story = {
27
+ args: {
28
+ variant: 'normal',
29
+ children: 'Cancel',
30
+ },
31
+ };
@@ -0,0 +1,12 @@
1
+
2
+ import CloudscapeButton, { ButtonProps as CloudscapeButtonProps } from '@cloudscape-design/components/button';
3
+
4
+ export interface ButtonProps extends CloudscapeButtonProps {
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ export const Button = ({ children, ...props }: ButtonProps) => {
9
+ return <CloudscapeButton {...props}>{children}</CloudscapeButton>;
10
+ };
11
+
12
+ export default Button;
@@ -0,0 +1,42 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { CreateButton } from './CreateButton';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof CreateButton> = {
6
+ title: 'Components/CreateButton',
7
+ component: CreateButton,
8
+ tags: ['autodocs'],
9
+ decorators: [
10
+ withRaContext(undefined, 'posts', {
11
+ posts: { name: 'posts', hasCreate: true },
12
+ }),
13
+ ],
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof CreateButton>;
18
+
19
+ export const Default: Story = {
20
+ args: {},
21
+ };
22
+
23
+ export const CustomLabel: Story = {
24
+ args: {
25
+ label: 'New Post',
26
+ },
27
+ };
28
+
29
+ export const Normal: Story = {
30
+ args: {
31
+ variant: 'normal',
32
+ },
33
+ };
34
+
35
+ export const WithoutPermission: Story = {
36
+ decorators: [
37
+ withRaContext(undefined, 'posts', {
38
+ posts: { name: 'posts', hasCreate: false },
39
+ }),
40
+ ],
41
+ args: {},
42
+ };
@@ -0,0 +1,38 @@
1
+
2
+ import { useResourceContext, useTranslate, useCreatePath, useResourceDefinitions } from '@strato-admin/core';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { Button, ButtonProps } from './Button';
5
+
6
+ export interface CreateButtonProps extends Omit<ButtonProps, 'children'> {
7
+ label?: string;
8
+ }
9
+
10
+ export const CreateButton = ({ label, variant = 'primary', ...props }: CreateButtonProps) => {
11
+ const resource = useResourceContext();
12
+ const translate = useTranslate();
13
+ const createPath = useCreatePath();
14
+ const navigate = useNavigate();
15
+ const definitions = useResourceDefinitions();
16
+
17
+ const definition = resource ? definitions[resource] : undefined;
18
+
19
+ if (!definition || !definition.hasCreate) {
20
+ return null;
21
+ }
22
+
23
+ const handleClick = () => {
24
+ const path = createPath({
25
+ resource,
26
+ type: 'create',
27
+ });
28
+ navigate(path);
29
+ };
30
+
31
+ return (
32
+ <Button variant={variant} onClick={handleClick} iconName="add-plus" {...props}>
33
+ {label || translate('ra.action.create', { _: 'Create' })}
34
+ </Button>
35
+ );
36
+ };
37
+
38
+ export default CreateButton;
@@ -0,0 +1,29 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { EditButton } from './EditButton';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof EditButton> = {
6
+ title: 'Components/EditButton',
7
+ component: EditButton,
8
+ tags: ['autodocs'],
9
+ decorators: [withRaContext({ id: 123, title: 'Sample Record' }, 'posts')],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof EditButton>;
14
+
15
+ export const Default: Story = {
16
+ args: {},
17
+ };
18
+
19
+ export const CustomLabel: Story = {
20
+ args: {
21
+ label: 'Modify Post',
22
+ },
23
+ };
24
+
25
+ export const Normal: Story = {
26
+ args: {
27
+ variant: 'normal',
28
+ },
29
+ };
@@ -0,0 +1,38 @@
1
+
2
+ import { useResourceContext, useRecordContext, useTranslate, useCreatePath, RaRecord } from '@strato-admin/core';
3
+ import { useNavigate } from 'react-router-dom';
4
+ import { Button, ButtonProps } from './Button';
5
+
6
+ export interface EditButtonProps extends Omit<ButtonProps, 'children'> {
7
+ label?: string;
8
+ record?: RaRecord;
9
+ }
10
+
11
+ export const EditButton = ({ label, record: recordProp, variant = 'primary', ...props }: EditButtonProps) => {
12
+ const resource = useResourceContext();
13
+ const record = useRecordContext(recordProp);
14
+ const translate = useTranslate();
15
+ const createPath = useCreatePath();
16
+ const navigate = useNavigate();
17
+
18
+ if (!record) {
19
+ return null;
20
+ }
21
+
22
+ const handleClick = () => {
23
+ const path = createPath({
24
+ resource,
25
+ id: record.id,
26
+ type: 'edit',
27
+ });
28
+ navigate(path);
29
+ };
30
+
31
+ return (
32
+ <Button variant={variant} onClick={handleClick} {...props}>
33
+ {label || translate('ra.action.edit', { _: 'Edit' })}
34
+ </Button>
35
+ );
36
+ };
37
+
38
+ export default EditButton;
@@ -0,0 +1,35 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { SaveButton } from './SaveButton';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof SaveButton> = {
6
+ title: 'Components/SaveButton',
7
+ component: SaveButton,
8
+ tags: ['autodocs'],
9
+ decorators: [withRaContext()],
10
+ };
11
+
12
+ export default meta;
13
+ type Story = StoryObj<typeof SaveButton>;
14
+
15
+ export const Default: Story = {
16
+ args: {},
17
+ };
18
+
19
+ export const CustomLabel: Story = {
20
+ args: {
21
+ label: 'Commit Changes',
22
+ },
23
+ };
24
+
25
+ export const Disabled: Story = {
26
+ args: {
27
+ disabled: true,
28
+ },
29
+ };
30
+
31
+ export const Loading: Story = {
32
+ args: {
33
+ loading: true,
34
+ },
35
+ };