@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,67 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import DateField from './DateField';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof DateField> = {
6
+ title: 'Fields/DateField',
7
+ component: DateField,
8
+ tags: ['autodocs'],
9
+ decorators: [
10
+ withRaContext({
11
+ id: 1,
12
+ published_at: '2023-10-27T10:00:00Z',
13
+ last_seen: new Date('2024-01-15T15:30:00Z'),
14
+ empty: null,
15
+ }),
16
+ ],
17
+ };
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof DateField>;
21
+
22
+ export const Basic: Story = {
23
+ args: {
24
+ source: 'published_at',
25
+ },
26
+ };
27
+
28
+ export const WithDateObject: Story = {
29
+ args: {
30
+ source: 'last_seen',
31
+ },
32
+ };
33
+
34
+ export const CustomOptions: Story = {
35
+ args: {
36
+ source: 'published_at',
37
+ options: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
38
+ },
39
+ };
40
+
41
+ export const CustomLocale: Story = {
42
+ args: {
43
+ source: 'published_at',
44
+ locales: 'fr-FR',
45
+ options: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' },
46
+ },
47
+ };
48
+
49
+ export const Empty: Story = {
50
+ args: {
51
+ source: 'empty',
52
+ },
53
+ };
54
+
55
+ export const WithEmptyText: Story = {
56
+ args: {
57
+ source: 'empty',
58
+ emptyText: 'Not set',
59
+ },
60
+ };
61
+
62
+ export const WithLink: Story = {
63
+ args: {
64
+ source: 'published_at',
65
+ link: 'show',
66
+ },
67
+ };
@@ -0,0 +1,33 @@
1
+ import { type RaRecord, useFieldValue, useRecordContext, useLocale } from '@strato-admin/core';
2
+ import RecordLink from '../RecordLink';
3
+ import { type FieldProps } from './types';
4
+
5
+ export type DateFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
6
+ /**
7
+ * Options for Intl.DateTimeFormat.
8
+ */
9
+ options?: Intl.DateTimeFormatOptions;
10
+ /**
11
+ * Locale(s) to use for formatting. Defaults to the current app locale.
12
+ */
13
+ locales?: string | string[];
14
+ };
15
+
16
+ const DateField = <RecordType extends RaRecord = RaRecord>(props: DateFieldProps<RecordType>) => {
17
+ const { source, record: recordProp, emptyText, options, locales, link } = props;
18
+ const record = useRecordContext<RecordType>({ record: recordProp });
19
+ const value = useFieldValue<RecordType>({ source: source as any, record });
20
+ const currentLocale = useLocale();
21
+ const hasValue = value !== null && value !== undefined && value !== '';
22
+
23
+ if (!hasValue) {
24
+ return <>{emptyText ?? null}</>;
25
+ }
26
+
27
+ const dateValue = value instanceof Date ? value : new Date(value);
28
+ const formattedValue = new Intl.DateTimeFormat(locales || currentLocale, options).format(dateValue);
29
+
30
+ return <RecordLink link={link}>{formattedValue}</RecordLink>;
31
+ };
32
+
33
+ export default DateField;
@@ -0,0 +1,88 @@
1
+ import { render, cleanup } from '@testing-library/react';
2
+ import { vi, describe, it, expect, afterEach } from 'vitest';
3
+ import {
4
+ useFieldValue,
5
+ useRecordContext,
6
+ useResourceDefinition,
7
+ } from '@strato-admin/core';
8
+ import IdField from './IdField';
9
+
10
+ // Mock strato-core
11
+ vi.mock('@strato-admin/core', () => ({
12
+ useRecordContext: vi.fn(),
13
+ useFieldValue: vi.fn(),
14
+ useResourceDefinition: vi.fn(),
15
+ }));
16
+
17
+ // Mock RecordLink
18
+ vi.mock('../RecordLink', () => ({
19
+ default: ({ children, link }: any) => (
20
+ <a data-testid="record-link" data-link={link}>
21
+ {children}
22
+ </a>
23
+ ),
24
+ }));
25
+
26
+ describe('IdField', () => {
27
+ afterEach(() => {
28
+ cleanup();
29
+ vi.clearAllMocks();
30
+ });
31
+
32
+ it('should render the ID and link to show by default if hasShow is true', () => {
33
+ const record = { id: '123' };
34
+ (useRecordContext as any).mockReturnValue(record);
35
+ (useFieldValue as any).mockImplementation(
36
+ ({ source }: any) => (record as any)[source]
37
+ );
38
+ (useResourceDefinition as any).mockReturnValue({ hasShow: true });
39
+
40
+ const { getByTestId, getByText } = render(<IdField />);
41
+
42
+ expect(getByText('123')).toBeDefined();
43
+ const link = getByTestId('record-link');
44
+ expect(link.getAttribute('data-link')).toBe('show');
45
+ });
46
+
47
+ it('should not link by default if hasShow is false', () => {
48
+ const record = { id: '123' };
49
+ (useRecordContext as any).mockReturnValue(record);
50
+ (useFieldValue as any).mockImplementation(
51
+ ({ source }: any) => (record as any)[source]
52
+ );
53
+ (useResourceDefinition as any).mockReturnValue({ hasShow: false });
54
+
55
+ const { getByTestId, getByText } = render(<IdField />);
56
+
57
+ expect(getByText('123')).toBeDefined();
58
+ const link = getByTestId('record-link');
59
+ expect(link.getAttribute('data-link')).toBe(null);
60
+ });
61
+
62
+ it('should use custom source if provided', () => {
63
+ const record = { identifier: 'abc' };
64
+ (useRecordContext as any).mockReturnValue(record);
65
+ (useFieldValue as any).mockImplementation(
66
+ ({ source }: any) => (record as any)[source]
67
+ );
68
+ (useResourceDefinition as any).mockReturnValue({ hasShow: true });
69
+
70
+ const { getByText } = render(<IdField source="identifier" />);
71
+
72
+ expect(getByText('abc')).toBeDefined();
73
+ });
74
+
75
+ it('should allow overriding link', () => {
76
+ const record = { id: '123' };
77
+ (useRecordContext as any).mockReturnValue(record);
78
+ (useFieldValue as any).mockImplementation(
79
+ ({ source }: any) => (record as any)[source]
80
+ );
81
+ (useResourceDefinition as any).mockReturnValue({ hasShow: true });
82
+
83
+ const { getByTestId } = render(<IdField link="edit" />);
84
+
85
+ const link = getByTestId('record-link');
86
+ expect(link.getAttribute('data-link')).toBe('edit');
87
+ });
88
+ });
@@ -0,0 +1,40 @@
1
+ import { type RaRecord, useResourceDefinition } from '@strato-admin/core';
2
+ import TextField, { type TextFieldProps } from './TextField';
3
+
4
+ export type IdFieldProps<RecordType extends RaRecord = RaRecord> =
5
+ TextFieldProps<RecordType>;
6
+
7
+ /**
8
+ * A field that displays the record's ID.
9
+ *
10
+ * Defaults to:
11
+ * - source="id"
12
+ * - input={false} (hidden in forms)
13
+ * - link="show" if the resource has a show page
14
+ *
15
+ * @example
16
+ * <IdField />
17
+ * <IdField source="identifier" />
18
+ */
19
+ const IdField = <RecordType extends RaRecord = RaRecord>(
20
+ props: IdFieldProps<RecordType>
21
+ ) => {
22
+ const { hasShow } = useResourceDefinition(props);
23
+ const {
24
+ source = 'id',
25
+ link = hasShow ? 'show' : undefined,
26
+ input = false,
27
+ ...rest
28
+ } = props;
29
+
30
+ return (
31
+ <TextField<RecordType>
32
+ source={source}
33
+ link={link}
34
+ input={input}
35
+ {...rest}
36
+ />
37
+ );
38
+ };
39
+
40
+ export default IdField;
@@ -0,0 +1,75 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import NumberField from './NumberField';
3
+ import { withRaContext } from '../stories/RaStoryDecorator';
4
+
5
+ const meta: Meta<typeof NumberField> = {
6
+ title: 'Fields/NumberField',
7
+ component: NumberField,
8
+ tags: ['autodocs'],
9
+ decorators: [
10
+ withRaContext({
11
+ id: 1,
12
+ price: 123.45,
13
+ quantity: 10,
14
+ score: 0.85,
15
+ empty: null,
16
+ }),
17
+ ],
18
+ };
19
+
20
+ export default meta;
21
+ type Story = StoryObj<typeof NumberField>;
22
+
23
+ export const Basic: Story = {
24
+ args: {
25
+ source: 'price',
26
+ },
27
+ };
28
+
29
+ export const Integer: Story = {
30
+ args: {
31
+ source: 'quantity',
32
+ },
33
+ };
34
+
35
+ export const Currency: Story = {
36
+ args: {
37
+ source: 'price',
38
+ options: { style: 'currency', currency: 'USD' },
39
+ },
40
+ };
41
+
42
+ export const Percentage: Story = {
43
+ args: {
44
+ source: 'score',
45
+ options: { style: 'percent' },
46
+ },
47
+ };
48
+
49
+ export const CustomLocale: Story = {
50
+ args: {
51
+ source: 'price',
52
+ locales: 'de-DE',
53
+ options: { style: 'currency', currency: 'EUR' },
54
+ },
55
+ };
56
+
57
+ export const Empty: Story = {
58
+ args: {
59
+ source: 'empty',
60
+ },
61
+ };
62
+
63
+ export const WithEmptyText: Story = {
64
+ args: {
65
+ source: 'empty',
66
+ emptyText: 'N/A',
67
+ },
68
+ };
69
+
70
+ export const WithLink: Story = {
71
+ args: {
72
+ source: 'price',
73
+ link: 'show',
74
+ },
75
+ };
@@ -0,0 +1,35 @@
1
+ import { type RaRecord, useFieldValue, useRecordContext, useLocale } from '@strato-admin/core';
2
+ import RecordLink from '../RecordLink';
3
+ import { type FieldProps } from './types';
4
+
5
+ export type NumberFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
6
+ /**
7
+ * Options for Intl.NumberFormat.
8
+ */
9
+ options?: Intl.NumberFormatOptions;
10
+ /**
11
+ * Locale(s) to use for formatting. Defaults to the current app locale.
12
+ */
13
+ locales?: string | string[];
14
+ };
15
+
16
+ const NumberField = <RecordType extends RaRecord = RaRecord>(props: NumberFieldProps<RecordType>) => {
17
+ const { source, record: recordProp, emptyText, options, locales, link } = props;
18
+ const record = useRecordContext<RecordType>({ record: recordProp });
19
+ const value = useFieldValue<RecordType>({ source: source as any, record });
20
+ const currentLocale = useLocale();
21
+ const hasValue = value !== null && value !== undefined && value !== '';
22
+
23
+ if (!hasValue) {
24
+ return <>{emptyText ?? null}</>;
25
+ }
26
+
27
+ const numberValue = typeof value === 'string' ? parseFloat(value) : value;
28
+ const formattedValue = new Intl.NumberFormat(locales || currentLocale, options).format(numberValue);
29
+
30
+ return <RecordLink link={link}>{formattedValue}</RecordLink>;
31
+ };
32
+
33
+ (NumberField as any).isNumberColumn = true;
34
+
35
+ export default NumberField;
@@ -0,0 +1,88 @@
1
+
2
+ import { render, screen } from '@testing-library/react';
3
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
+ import ReferenceField from './ReferenceField';
5
+ import { useRecordContext, useGetRecordRepresentation } from '@strato-admin/core';
6
+
7
+ // Mock ra-core
8
+ vi.mock('@strato-admin/core', () => ({
9
+ ReferenceFieldBase: vi.fn(({ children }: any) => <div data-testid="ra-reference-field-base">{children}</div>),
10
+ useRecordContext: vi.fn(),
11
+ useGetRecordRepresentation: vi.fn(),
12
+ useResourceDefinition: vi.fn(),
13
+ useResourceContext: vi.fn(() => 'categories'),
14
+ useCreatePath: vi.fn(() => (params: any) => `/${params.resource}/${params.id}/${params.type}`),
15
+ ResourceContextProvider: ({ children }: any) => <div data-testid="resource-context-provider">{children}</div>,
16
+ }));
17
+
18
+ // Mock react-router-dom
19
+ vi.mock('react-router-dom', () => ({
20
+ useNavigate: vi.fn(),
21
+ }));
22
+
23
+ // Mock Cloudscape Box
24
+ vi.mock('@cloudscape-design/components/box', () => ({
25
+ default: ({ children }: any) => <div>{children}</div>,
26
+ }));
27
+
28
+ // Mock Cloudscape Link
29
+ vi.mock('@cloudscape-design/components/link', () => ({
30
+ default: ({ children, href }: any) => (
31
+ <a href={href} data-testid="cloudscape-link">
32
+ {children}
33
+ </a>
34
+ ),
35
+ }));
36
+
37
+ describe('ReferenceField', () => {
38
+ beforeEach(() => {
39
+ vi.clearAllMocks();
40
+ });
41
+
42
+ it('should render children when provided', () => {
43
+ (useRecordContext as any).mockReturnValue({ id: 1, name: 'Category 1' });
44
+ (useGetRecordRepresentation as any).mockReturnValue(() => 'Category 1');
45
+
46
+ render(
47
+ <ReferenceField source="categoryId" reference="categories">
48
+ <span data-testid="child">Child Content</span>
49
+ </ReferenceField>,
50
+ );
51
+
52
+ expect(screen.getByTestId('child')).toBeDefined();
53
+ expect(screen.getByTestId('child').textContent).toBe('Child Content');
54
+ });
55
+
56
+ it('should render record representation when no children provided', () => {
57
+ const record = { id: 1, name: 'Category 1' };
58
+ (useRecordContext as any).mockReturnValue(record);
59
+ (useGetRecordRepresentation as any).mockReturnValue((rec: any) => rec.name);
60
+
61
+ render(<ReferenceField source="categoryId" reference="categories" />);
62
+
63
+ expect(screen.getByText('Category 1')).toBeDefined();
64
+ });
65
+
66
+ it('should render id if no other representation is provided', () => {
67
+ const record = { id: 1 };
68
+ (useRecordContext as any).mockReturnValue(record);
69
+ (useGetRecordRepresentation as any).mockReturnValue((rec: any) => `#${rec.id}`);
70
+
71
+ render(<ReferenceField source="categoryId" reference="categories" />);
72
+
73
+ expect(screen.getByText('#1')).toBeDefined();
74
+ });
75
+
76
+ it('should render a link when link prop is provided', () => {
77
+ const record = { id: 1, name: 'Category 1' };
78
+ (useRecordContext as any).mockReturnValue(record);
79
+ (useGetRecordRepresentation as any).mockReturnValue(() => 'Category 1');
80
+
81
+ render(<ReferenceField source="categoryId" reference="categories" link="show" />);
82
+
83
+ const link = screen.getByTestId('cloudscape-link');
84
+ expect(link).toBeDefined();
85
+ expect(link.getAttribute('href')).toBe('/categories/1/show');
86
+ expect(link.textContent).toBe('Category 1');
87
+ });
88
+ });
@@ -0,0 +1,64 @@
1
+ import { type ReactNode } from 'react';
2
+ import {
3
+ ReferenceFieldBase,
4
+ type RaRecord,
5
+ useRecordContext,
6
+ useGetRecordRepresentation,
7
+ } from '@strato-admin/core';
8
+ import RecordLink from '../RecordLink';
9
+ import { type FieldProps } from './types';
10
+
11
+ export type ReferenceFieldProps<RecordType extends RaRecord = RaRecord> = FieldProps<RecordType> & {
12
+ /**
13
+ * The resource name that this field refers to.
14
+ */
15
+ reference: string;
16
+ /**
17
+ * Optional custom representation of the related record. If not provided,
18
+ * the recordRepresentation of the referenced resource will be used.
19
+ */
20
+ children?: ReactNode;
21
+ };
22
+
23
+ const ReferenceField = <RecordType extends RaRecord = RaRecord>(props: ReferenceFieldProps<RecordType>) => {
24
+ const { source, reference, children, emptyText, record, link } = props;
25
+
26
+ if (!source) {
27
+ return null; // Or some fallback
28
+ }
29
+
30
+ return (
31
+ <ReferenceFieldBase source={source} reference={reference} record={record} empty={<>{emptyText ?? null}</>}>
32
+ <ReferenceFieldValue emptyText={emptyText} link={link} reference={reference}>
33
+ {children}
34
+ </ReferenceFieldValue>
35
+ </ReferenceFieldBase>
36
+ );
37
+ };
38
+
39
+ const ReferenceFieldValue = ({ children, emptyText, link, reference }: any) => {
40
+ const record = useRecordContext();
41
+ const getRecordRepresentation = useGetRecordRepresentation();
42
+
43
+ if (!record) {
44
+ return <>{emptyText ?? null}</>;
45
+ }
46
+
47
+ if (children) {
48
+ return (
49
+ <RecordLink link={link} resource={reference}>
50
+ {children}
51
+ </RecordLink>
52
+ );
53
+ }
54
+
55
+ const representation = getRecordRepresentation(record);
56
+
57
+ return (
58
+ <RecordLink link={link} resource={reference}>
59
+ {representation ? String(representation) : (emptyText ?? null)}
60
+ </RecordLink>
61
+ );
62
+ };
63
+
64
+ export default ReferenceField;
@@ -0,0 +1,41 @@
1
+
2
+ import React from 'react';
3
+ import { render, screen, cleanup } from '@testing-library/react';
4
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
5
+ import ReferenceManyField from './ReferenceManyField';
6
+
7
+ // Mock ra-core (via strato-core)
8
+ vi.mock('@strato-admin/core', () => ({
9
+ ReferenceManyFieldBase: vi.fn(({ children }: any) => <div data-testid="ra-reference-many-field-base">{children}</div>),
10
+ ResourceSchemaProvider: vi.fn(({ children }: any) => <div data-testid="resource-schema-provider">{children}</div>),
11
+ ResourceContextProvider: ({ children }: any) => <div data-testid="resource-context-provider">{children}</div>,
12
+ }));
13
+
14
+ describe('ReferenceManyField', () => {
15
+ beforeEach(() => {
16
+ cleanup();
17
+ });
18
+
19
+ it('should render children within providers', () => {
20
+ render(
21
+ <ReferenceManyField reference="comments" target="post_id">
22
+ <div data-testid="child">Child Content</div>
23
+ </ReferenceManyField>,
24
+ );
25
+
26
+ expect(screen.getByTestId('ra-reference-many-field-base')).toBeDefined();
27
+ expect(screen.getByTestId('resource-schema-provider')).toBeDefined();
28
+ expect(screen.getByTestId('child')).toBeDefined();
29
+ expect(screen.getByTestId('child').textContent).toBe('Child Content');
30
+ });
31
+
32
+ it('should pass correct resource to ResourceSchemaProvider', () => {
33
+ render(
34
+ <ReferenceManyField reference="reviews" target="product_id">
35
+ <div data-testid="child">Child Content</div>
36
+ </ReferenceManyField>,
37
+ );
38
+
39
+ expect(screen.getByTestId('resource-schema-provider')).toBeDefined();
40
+ });
41
+ });
@@ -0,0 +1,73 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { ReferenceManyFieldBase, type RaRecord, ResourceSchemaProvider } from '@strato-admin/core';
3
+ import { type FieldProps } from './types';
4
+
5
+ export interface ReferenceManyFieldProps<
6
+ RecordType extends RaRecord = RaRecord,
7
+ ReferenceRecordType extends RaRecord = RaRecord,
8
+ > extends FieldProps<RecordType> {
9
+ children?: ReactNode;
10
+ reference: string;
11
+ target: string;
12
+ filter?: any;
13
+ sort?: { field: string; order: 'ASC' | 'DESC' };
14
+ perPage?: number;
15
+ page?: number;
16
+ fieldSchema?: ReactNode;
17
+ /**
18
+ * Element to display during loading.
19
+ */
20
+ loading?: ReactNode;
21
+ /**
22
+ * Element to display if there's no data.
23
+ */
24
+ empty?: ReactNode;
25
+ /**
26
+ * Element to display if there's an error.
27
+ */
28
+ error?: ReactNode;
29
+ /**
30
+ * Element to display if the application is offline.
31
+ */
32
+ offline?: ReactNode;
33
+ /**
34
+ * Debounce time for filtering.
35
+ */
36
+ debounce?: number;
37
+ /**
38
+ * Whether to synchronize the list with the URL.
39
+ * @default false
40
+ */
41
+ synchronizeWithLocation?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Render related records to the current one.
46
+ *
47
+ * @example
48
+ * <ReferenceManyField reference="comments" target="post_id">
49
+ * <Table title="Comments">
50
+ * <Table.Column source="id" />
51
+ * <Table.Column source="body" />
52
+ * <Table.Column source="created_at" />
53
+ * </Table>
54
+ * </ReferenceManyField>
55
+ */
56
+ export const ReferenceManyField = <
57
+ RecordType extends RaRecord = RaRecord,
58
+ ReferenceRecordType extends RaRecord = RaRecord,
59
+ >(
60
+ props: ReferenceManyFieldProps<RecordType, ReferenceRecordType>
61
+ ) => {
62
+ const { children, reference, fieldSchema, ...rest } = props;
63
+
64
+ return (
65
+ <ReferenceManyFieldBase reference={reference} {...rest}>
66
+ <ResourceSchemaProvider resource={reference} fieldSchema={fieldSchema}>
67
+ {children}
68
+ </ResourceSchemaProvider>
69
+ </ReferenceManyFieldBase>
70
+ );
71
+ };
72
+
73
+ export default ReferenceManyField;