@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.
- package/LICENSE +21 -0
- package/README.md +3 -0
- package/dist/Admin.d.ts +17 -0
- package/dist/Admin.js +69 -0
- package/dist/RecordLink.d.ts +9 -0
- package/dist/RecordLink.js +43 -0
- package/dist/__mocks__/strato-core.js +50 -0
- package/dist/__mocks__to__delete/strato-core.js +50 -0
- package/dist/button/BulkDeleteButton.d.ts +7 -0
- package/dist/button/BulkDeleteButton.js +17 -0
- package/dist/button/Button.d.ts +6 -0
- package/dist/button/Button.js +6 -0
- package/dist/button/CreateButton.d.ts +6 -0
- package/dist/button/CreateButton.js +24 -0
- package/dist/button/EditButton.d.ts +8 -0
- package/dist/button/EditButton.js +24 -0
- package/dist/button/SaveButton.d.ts +6 -0
- package/dist/button/SaveButton.js +8 -0
- package/dist/button/index.d.ts +5 -0
- package/dist/button/index.js +5 -0
- package/dist/collection-hooks/index.d.ts +2 -0
- package/dist/collection-hooks/index.js +2 -0
- package/dist/collection-hooks/interfaces.d.ts +93 -0
- package/dist/collection-hooks/interfaces.js +1 -0
- package/dist/collection-hooks/useCollection.d.ts +3 -0
- package/dist/collection-hooks/useCollection.js +102 -0
- package/dist/create/Create.d.ts +40 -0
- package/dist/create/Create.js +34 -0
- package/dist/create/CreateHeader.d.ts +7 -0
- package/dist/create/CreateHeader.js +18 -0
- package/dist/create/index.d.ts +2 -0
- package/dist/create/index.js +2 -0
- package/dist/detail/KeyValuePairs.d.ts +36 -0
- package/dist/detail/KeyValuePairs.js +58 -0
- package/dist/detail/Show.d.ts +39 -0
- package/dist/detail/Show.js +40 -0
- package/dist/detail/ShowHeader.d.ts +7 -0
- package/dist/detail/ShowHeader.js +19 -0
- package/dist/detail/index.d.ts +3 -0
- package/dist/detail/index.js +3 -0
- package/dist/edit/Edit.d.ts +42 -0
- package/dist/edit/Edit.js +38 -0
- package/dist/edit/EditHeader.d.ts +7 -0
- package/dist/edit/EditHeader.js +18 -0
- package/dist/edit/index.d.ts +2 -0
- package/dist/edit/index.js +2 -0
- package/dist/field/ArrayField.d.ts +29 -0
- package/dist/field/ArrayField.js +30 -0
- package/dist/field/BadgeField.d.ts +12 -0
- package/dist/field/BadgeField.js +15 -0
- package/dist/field/BooleanField.d.ts +18 -0
- package/dist/field/BooleanField.js +14 -0
- package/dist/field/CurrencyField.d.ts +19 -0
- package/dist/field/CurrencyField.js +23 -0
- package/dist/field/DateField.d.ts +14 -0
- package/dist/field/DateField.js +17 -0
- package/dist/field/IdField.d.ts +17 -0
- package/dist/field/IdField.js +21 -0
- package/dist/field/NumberField.d.ts +14 -0
- package/dist/field/NumberField.js +18 -0
- package/dist/field/ReferenceField.d.ts +16 -0
- package/dist/field/ReferenceField.js +23 -0
- package/dist/field/ReferenceManyField.d.ts +55 -0
- package/dist/field/ReferenceManyField.js +19 -0
- package/dist/field/StatusIndicatorField.d.ts +56 -0
- package/dist/field/StatusIndicatorField.js +48 -0
- package/dist/field/TextField.d.ts +5 -0
- package/dist/field/TextField.js +11 -0
- package/dist/field/index.d.ts +23 -0
- package/dist/field/index.js +23 -0
- package/dist/field/types.d.ts +56 -0
- package/dist/field/types.js +1 -0
- package/dist/form/Form.d.ts +13 -0
- package/dist/form/Form.js +33 -0
- package/dist/form/index.d.ts +2 -0
- package/dist/form/index.js +2 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/input/AttributeEditor.d.ts +25 -0
- package/dist/input/AttributeEditor.js +80 -0
- package/dist/input/AutocompleteInput.d.ts +10 -0
- package/dist/input/AutocompleteInput.js +67 -0
- package/dist/input/FieldTitle.d.ts +8 -0
- package/dist/input/FieldTitle.js +29 -0
- package/dist/input/FormField.d.ts +7 -0
- package/dist/input/FormField.js +35 -0
- package/dist/input/FormFieldContext.d.ts +6 -0
- package/dist/input/FormFieldContext.js +3 -0
- package/dist/input/NumberInput.d.ts +7 -0
- package/dist/input/NumberInput.js +27 -0
- package/dist/input/ReferenceInput.d.ts +3 -0
- package/dist/input/ReferenceInput.js +25 -0
- package/dist/input/SelectInput.d.ts +15 -0
- package/dist/input/SelectInput.js +47 -0
- package/dist/input/SliderInput.d.ts +6 -0
- package/dist/input/SliderInput.js +25 -0
- package/dist/input/TextAreaInput.d.ts +6 -0
- package/dist/input/TextAreaInput.js +23 -0
- package/dist/input/TextInput.d.ts +7 -0
- package/dist/input/TextInput.js +23 -0
- package/dist/input/index.d.ts +11 -0
- package/dist/input/index.js +11 -0
- package/dist/input/types.d.ts +6 -0
- package/dist/input/types.js +1 -0
- package/dist/layout/AppLayout.d.ts +8 -0
- package/dist/layout/AppLayout.js +38 -0
- package/dist/layout/TopNavigation.d.ts +6 -0
- package/dist/layout/TopNavigation.js +53 -0
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +2 -0
- package/dist/list/Cards.d.ts +11 -0
- package/dist/list/Cards.js +27 -0
- package/dist/list/List.d.ts +43 -0
- package/dist/list/List.js +28 -0
- package/dist/list/Table.d.ts +112 -0
- package/dist/list/Table.examples.d.ts +1 -0
- package/dist/list/Table.examples.js +3 -0
- package/dist/list/Table.js +218 -0
- package/dist/list/TableHeader.d.ts +7 -0
- package/dist/list/TableHeader.js +22 -0
- package/dist/list/index.d.ts +4 -0
- package/dist/list/index.js +4 -0
- package/dist/preferences/index.d.ts +0 -0
- package/dist/preferences/index.js +1 -0
- package/dist/theme/ThemeManager.d.ts +2 -0
- package/dist/theme/ThemeManager.js +11 -0
- package/dist/theme/index.d.ts +2 -0
- package/dist/theme/index.js +2 -0
- package/package.json +73 -0
- package/src/Admin.test.tsx +32 -0
- package/src/Admin.tsx +123 -0
- package/src/RecordLink.stories.tsx +56 -0
- package/src/RecordLink.tsx +67 -0
- package/src/__mocks__/strato-core.tsx +52 -0
- package/src/button/BulkDeleteButton.stories.tsx +59 -0
- package/src/button/BulkDeleteButton.test.tsx +64 -0
- package/src/button/BulkDeleteButton.tsx +41 -0
- package/src/button/Button.stories.tsx +31 -0
- package/src/button/Button.tsx +12 -0
- package/src/button/CreateButton.stories.tsx +42 -0
- package/src/button/CreateButton.tsx +38 -0
- package/src/button/EditButton.stories.tsx +29 -0
- package/src/button/EditButton.tsx +38 -0
- package/src/button/SaveButton.stories.tsx +35 -0
- package/src/button/SaveButton.tsx +19 -0
- package/src/button/index.ts +5 -0
- package/src/collection-hooks/index.ts +2 -0
- package/src/collection-hooks/interfaces.ts +80 -0
- package/src/collection-hooks/useCollection.test.ts +413 -0
- package/src/collection-hooks/useCollection.ts +125 -0
- package/src/create/Create.test.tsx +63 -0
- package/src/create/Create.tsx +93 -0
- package/src/create/CreateHeader.tsx +34 -0
- package/src/create/index.ts +2 -0
- package/src/detail/KeyValuePairs.test.tsx +98 -0
- package/src/detail/KeyValuePairs.tsx +107 -0
- package/src/detail/Show.test.tsx +96 -0
- package/src/detail/Show.tsx +104 -0
- package/src/detail/ShowHeader.test.tsx +80 -0
- package/src/detail/ShowHeader.tsx +35 -0
- package/src/detail/index.ts +3 -0
- package/src/edit/Edit.test.tsx +91 -0
- package/src/edit/Edit.tsx +102 -0
- package/src/edit/EditHeader.tsx +34 -0
- package/src/edit/index.ts +2 -0
- package/src/field/ArrayField.tsx +51 -0
- package/src/field/BadgeField.tsx +33 -0
- package/src/field/BooleanField.stories.tsx +56 -0
- package/src/field/BooleanField.test.tsx +63 -0
- package/src/field/BooleanField.tsx +42 -0
- package/src/field/CurrencyField.stories.tsx +67 -0
- package/src/field/CurrencyField.tsx +45 -0
- package/src/field/DateField.stories.tsx +67 -0
- package/src/field/DateField.tsx +33 -0
- package/src/field/IdField.test.tsx +88 -0
- package/src/field/IdField.tsx +40 -0
- package/src/field/NumberField.stories.tsx +75 -0
- package/src/field/NumberField.tsx +35 -0
- package/src/field/ReferenceField.test.tsx +88 -0
- package/src/field/ReferenceField.tsx +64 -0
- package/src/field/ReferenceManyField.test.tsx +41 -0
- package/src/field/ReferenceManyField.tsx +73 -0
- package/src/field/StatusIndicatorField.stories.tsx +93 -0
- package/src/field/StatusIndicatorField.test.tsx +143 -0
- package/src/field/StatusIndicatorField.tsx +119 -0
- package/src/field/TextField.stories.tsx +45 -0
- package/src/field/TextField.tsx +17 -0
- package/src/field/index.ts +23 -0
- package/src/field/types.ts +58 -0
- package/src/form/Form.test.tsx +55 -0
- package/src/form/Form.tsx +66 -0
- package/src/form/index.ts +2 -0
- package/src/index.ts +25 -0
- package/src/input/AttributeEditor.test.tsx +147 -0
- package/src/input/AttributeEditor.tsx +185 -0
- package/src/input/AutocompleteInput.test.tsx +178 -0
- package/src/input/AutocompleteInput.tsx +116 -0
- package/src/input/FieldTitle.tsx +53 -0
- package/src/input/FormField.tsx +87 -0
- package/src/input/FormFieldContext.ts +9 -0
- package/src/input/NumberInput.tsx +56 -0
- package/src/input/ReferenceInput.test.tsx +35 -0
- package/src/input/ReferenceInput.tsx +36 -0
- package/src/input/SelectInput.tsx +91 -0
- package/src/input/SliderInput.test.tsx +103 -0
- package/src/input/SliderInput.tsx +49 -0
- package/src/input/TextAreaInput.tsx +48 -0
- package/src/input/TextInput.test.tsx +91 -0
- package/src/input/TextInput.tsx +51 -0
- package/src/input/index.ts +11 -0
- package/src/input/types.ts +14 -0
- package/src/layout/AppLayout.test.tsx +87 -0
- package/src/layout/AppLayout.tsx +60 -0
- package/src/layout/TopNavigation.test.tsx +78 -0
- package/src/layout/TopNavigation.tsx +84 -0
- package/src/layout/index.ts +2 -0
- package/src/list/Cards.tsx +58 -0
- package/src/list/List.tsx +76 -0
- package/src/list/Table.examples.tsx +11 -0
- package/src/list/Table.stories.tsx +73 -0
- package/src/list/Table.test.tsx +255 -0
- package/src/list/Table.tsx +438 -0
- package/src/list/TableHeader.test.tsx +114 -0
- package/src/list/TableHeader.tsx +44 -0
- package/src/list/index.ts +4 -0
- package/src/preferences/index.ts +0 -0
- package/src/stories/Button.stories.ts +54 -0
- package/src/stories/Button.tsx +31 -0
- package/src/stories/Configure.mdx +369 -0
- package/src/stories/Header.stories.ts +34 -0
- package/src/stories/Header.tsx +47 -0
- package/src/stories/Page.stories.ts +33 -0
- package/src/stories/Page.tsx +71 -0
- package/src/stories/RaStoryDecorator.tsx +38 -0
- package/src/stories/assets/accessibility.png +0 -0
- package/src/stories/assets/accessibility.svg +1 -0
- package/src/stories/assets/addon-library.png +0 -0
- package/src/stories/assets/assets.png +0 -0
- package/src/stories/assets/avif-test-image.avif +0 -0
- package/src/stories/assets/context.png +0 -0
- package/src/stories/assets/discord.svg +1 -0
- package/src/stories/assets/docs.png +0 -0
- package/src/stories/assets/figma-plugin.png +0 -0
- package/src/stories/assets/github.svg +1 -0
- package/src/stories/assets/share.png +0 -0
- package/src/stories/assets/styling.png +0 -0
- package/src/stories/assets/testing.png +0 -0
- package/src/stories/assets/theming.png +0 -0
- package/src/stories/assets/tutorials.svg +1 -0
- package/src/stories/assets/youtube.svg +1 -0
- package/src/stories/button.css +30 -0
- package/src/stories/header.css +32 -0
- package/src/stories/page.css +68 -0
- package/src/theme/ThemeManager.tsx +15 -0
- package/src/theme/index.ts +2 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { type RaRecord } from '@strato-admin/core';
|
|
3
|
+
export interface ListProps<_RecordType extends RaRecord = any> {
|
|
4
|
+
children?: React.ReactNode;
|
|
5
|
+
fieldSchema?: React.ReactNode;
|
|
6
|
+
include?: string[];
|
|
7
|
+
exclude?: string[];
|
|
8
|
+
title?: React.ReactNode;
|
|
9
|
+
actions?: React.ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* Whether to enable text filtering in the implicit Table.
|
|
12
|
+
* @default true
|
|
13
|
+
*/
|
|
14
|
+
filtering?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Whether to show the preferences button in the implicit Table.
|
|
17
|
+
* @default true
|
|
18
|
+
*/
|
|
19
|
+
preferences?: boolean | React.ReactNode;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* A List component that provides a list context and a Cloudscape Table.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* <List>
|
|
27
|
+
* <Table>
|
|
28
|
+
* <Table.Column source="name" />
|
|
29
|
+
* </Table>
|
|
30
|
+
* </List>
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Using FieldSchema from context
|
|
34
|
+
* <List include={['name', 'price']} />
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* // Passing a custom field schema
|
|
38
|
+
* <List fieldSchema={<FieldSchema>...</FieldSchema>}>
|
|
39
|
+
* <Table />
|
|
40
|
+
* </List>
|
|
41
|
+
*/
|
|
42
|
+
export declare const List: <RecordType extends RaRecord = any>({ children, fieldSchema, include, exclude, title, actions, filtering, preferences, ...props }: ListProps<RecordType>) => import("react/jsx-runtime").JSX.Element;
|
|
43
|
+
export default List;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { ListBase, ResourceSchemaProvider } from '@strato-admin/core';
|
|
3
|
+
import Table from './Table';
|
|
4
|
+
/**
|
|
5
|
+
* A List component that provides a list context and a Cloudscape Table.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* <List>
|
|
9
|
+
* <Table>
|
|
10
|
+
* <Table.Column source="name" />
|
|
11
|
+
* </Table>
|
|
12
|
+
* </List>
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* // Using FieldSchema from context
|
|
16
|
+
* <List include={['name', 'price']} />
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* // Passing a custom field schema
|
|
20
|
+
* <List fieldSchema={<FieldSchema>...</FieldSchema>}>
|
|
21
|
+
* <Table />
|
|
22
|
+
* </List>
|
|
23
|
+
*/
|
|
24
|
+
export const List = ({ children, fieldSchema, include, exclude, title, actions, filtering = true, preferences = true, ...props }) => {
|
|
25
|
+
const finalChildren = children || (_jsx(Table, { include: include, exclude: exclude, title: title, actions: actions, filtering: filtering, preferences: preferences }));
|
|
26
|
+
return (_jsx(ListBase, { ...props, children: _jsx(ResourceSchemaProvider, { resource: props.resource, fieldSchema: fieldSchema, children: finalChildren }) }));
|
|
27
|
+
};
|
|
28
|
+
export default List;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { TableProps as CloudscapeTableProps } from '@cloudscape-design/components/table';
|
|
3
|
+
import { RaRecord } from '@strato-admin/core';
|
|
4
|
+
import { type RecordLinkType } from '../RecordLink';
|
|
5
|
+
export type CloudscapeColumnDefinitionProps = Partial<Omit<CloudscapeTableProps.ColumnDefinition<any>, 'id' | 'header' | 'cell' | 'sortingField'>>;
|
|
6
|
+
export interface ColumnProps extends CloudscapeColumnDefinitionProps {
|
|
7
|
+
source?: string;
|
|
8
|
+
label?: string | React.ReactNode;
|
|
9
|
+
header?: React.ReactNode;
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
sortable?: boolean;
|
|
12
|
+
link?: RecordLinkType;
|
|
13
|
+
field?: React.ComponentType<any>;
|
|
14
|
+
}
|
|
15
|
+
export declare const Column: ({ children, source, link, field: FieldComponent }: ColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export interface NumberColumnProps extends ColumnProps {
|
|
17
|
+
source?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const NumberColumn: ({ children, source, link, field: FieldComponent }: NumberColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
20
|
+
export interface DateColumnProps extends ColumnProps {
|
|
21
|
+
source?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare const DateColumn: ({ children, source, link, field: FieldComponent }: DateColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
24
|
+
export interface BooleanColumnProps extends ColumnProps {
|
|
25
|
+
source?: string;
|
|
26
|
+
}
|
|
27
|
+
export declare const BooleanColumn: ({ children, source, field: FieldComponent }: BooleanColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
28
|
+
export interface ReferenceColumnProps extends ColumnProps {
|
|
29
|
+
source?: string;
|
|
30
|
+
reference: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const ReferenceColumn: ({ children, source, reference, link, field: FieldComponent }: ReferenceColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
33
|
+
/**
|
|
34
|
+
* Properties for the Table component.
|
|
35
|
+
*/
|
|
36
|
+
export interface TableProps<RecordType extends RaRecord = any> extends Partial<Omit<CloudscapeTableProps<RecordType>, 'items' | 'columnDefinitions' | 'preferences'>> {
|
|
37
|
+
/**
|
|
38
|
+
* The title content of the table. Can be a string or a React node.
|
|
39
|
+
*/
|
|
40
|
+
title?: React.ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* Actions to display in the table header, typically a button group.
|
|
43
|
+
*/
|
|
44
|
+
actions?: React.ReactNode;
|
|
45
|
+
/**
|
|
46
|
+
* The columns to display, usually using `Table.Column` and its variants.
|
|
47
|
+
*/
|
|
48
|
+
children?: React.ReactNode;
|
|
49
|
+
/**
|
|
50
|
+
* Include only these fields from the schema.
|
|
51
|
+
*/
|
|
52
|
+
include?: string[];
|
|
53
|
+
/**
|
|
54
|
+
* Exclude these fields from the schema.
|
|
55
|
+
*/
|
|
56
|
+
exclude?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* Whether to enable text filtering.
|
|
59
|
+
* @default true
|
|
60
|
+
*/
|
|
61
|
+
filtering?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Placeholder text for the filter input.
|
|
64
|
+
* @default "Search..."
|
|
65
|
+
*/
|
|
66
|
+
filteringPlaceholder?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Options for the page size selector.
|
|
69
|
+
*/
|
|
70
|
+
pageSizeOptions?: ReadonlyArray<{
|
|
71
|
+
value: number;
|
|
72
|
+
label?: string;
|
|
73
|
+
}>;
|
|
74
|
+
/**
|
|
75
|
+
* Whether to show the preferences button or custom preferences content.
|
|
76
|
+
* @default true
|
|
77
|
+
*/
|
|
78
|
+
preferences?: boolean | React.ReactNode;
|
|
79
|
+
/**
|
|
80
|
+
* Whether columns can be reordered by the user.
|
|
81
|
+
* @default true
|
|
82
|
+
*/
|
|
83
|
+
reorderable?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* The fields to display by default.
|
|
86
|
+
* Can be an array of field sources/IDs.
|
|
87
|
+
* If not specified, the first 5 fields will be shown.
|
|
88
|
+
*/
|
|
89
|
+
defaultVisibleFields?: string[];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* The Table component provides a declarative way to build data tables with Cloudscape components
|
|
93
|
+
* while integrating with React Admin's data fetching and state management.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```tsx
|
|
97
|
+
* <Table title="Products">
|
|
98
|
+
* <Table.Column source="name" label="Name" />
|
|
99
|
+
* <Table.NumberColumn source="price" label="Price" />
|
|
100
|
+
* </Table>
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare const Table: {
|
|
104
|
+
<RecordType extends RaRecord = any>({ title, actions, children, include, exclude, filtering, filteringPlaceholder, pageSizeOptions, preferences, reorderable, defaultVisibleFields, selectionType, ...props }: TableProps<RecordType>): import("react/jsx-runtime").JSX.Element;
|
|
105
|
+
Column: ({ children, source, link, field: FieldComponent }: ColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
106
|
+
NumberColumn: ({ children, source, link, field: FieldComponent }: NumberColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
107
|
+
DateColumn: ({ children, source, link, field: FieldComponent }: DateColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
108
|
+
BooleanColumn: ({ children, source, field: FieldComponent }: BooleanColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
109
|
+
ReferenceColumn: ({ children, source, reference, link, field: FieldComponent }: ReferenceColumnProps) => import("react/jsx-runtime").JSX.Element;
|
|
110
|
+
Header: ({ title, actions, ...props }: import("./TableHeader").TableHeaderProps) => import("react/jsx-runtime").JSX.Element;
|
|
111
|
+
};
|
|
112
|
+
export default Table;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ProductList: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Table, Column, NumberColumn, DateColumn } from './Table';
|
|
3
|
+
export const ProductList = () => (_jsxs(Table, { title: "Product Catalog", children: [_jsx(Column, { source: "name", label: "Product Name" }), _jsx(Column, { source: "category", label: "Category" }), _jsx(NumberColumn, { source: "price", label: "Price" }), _jsx(NumberColumn, { source: "stock", label: "Inventory" }), _jsx(DateColumn, { source: "lastUpdated", label: "Last Updated" })] }));
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import CloudscapeTable from '@cloudscape-design/components/table';
|
|
4
|
+
import Pagination from '@cloudscape-design/components/pagination';
|
|
5
|
+
import Box from '@cloudscape-design/components/box';
|
|
6
|
+
import TextFilter from '@cloudscape-design/components/text-filter';
|
|
7
|
+
import CollectionPreferences from '@cloudscape-design/components/collection-preferences';
|
|
8
|
+
import { RecordContextProvider, useResourceContext, useFieldSchema, useResourceDefinition, useGetResourceLabel, useTranslateLabel, useTranslate } from '@strato-admin/core';
|
|
9
|
+
import { useCollection } from '../collection-hooks';
|
|
10
|
+
import TextField from '../field/TextField';
|
|
11
|
+
import NumberField from '../field/NumberField';
|
|
12
|
+
import DateField from '../field/DateField';
|
|
13
|
+
import BooleanField from '../field/BooleanField';
|
|
14
|
+
import ReferenceField from '../field/ReferenceField';
|
|
15
|
+
import { TableHeader } from './TableHeader';
|
|
16
|
+
export const Column = ({ children, source, link, field: FieldComponent }) => {
|
|
17
|
+
if (children) {
|
|
18
|
+
return (_jsx(_Fragment, { children: React.Children.map(children, (child) => React.isValidElement(child) ? React.cloneElement(child, { source }) : child) }));
|
|
19
|
+
}
|
|
20
|
+
if (FieldComponent) {
|
|
21
|
+
return _jsx(FieldComponent, { link: link, source: source });
|
|
22
|
+
}
|
|
23
|
+
return _jsx(TextField, { link: link, source: source });
|
|
24
|
+
};
|
|
25
|
+
export const NumberColumn = ({ children, source, link, field: FieldComponent }) => {
|
|
26
|
+
if (children) {
|
|
27
|
+
return (_jsx(_Fragment, { children: React.Children.map(children, (child) => React.isValidElement(child) ? React.cloneElement(child, { source }) : child) }));
|
|
28
|
+
}
|
|
29
|
+
if (FieldComponent) {
|
|
30
|
+
return _jsx(FieldComponent, { link: link, source: source });
|
|
31
|
+
}
|
|
32
|
+
return _jsx(NumberField, { link: link, source: source });
|
|
33
|
+
};
|
|
34
|
+
NumberColumn.isNumberColumn = true;
|
|
35
|
+
export const DateColumn = ({ children, source, link, field: FieldComponent }) => {
|
|
36
|
+
if (children) {
|
|
37
|
+
return (_jsx(_Fragment, { children: React.Children.map(children, (child) => React.isValidElement(child) ? React.cloneElement(child, { source }) : child) }));
|
|
38
|
+
}
|
|
39
|
+
if (FieldComponent) {
|
|
40
|
+
return _jsx(FieldComponent, { link: link, source: source });
|
|
41
|
+
}
|
|
42
|
+
return _jsx(DateField, { link: link, source: source });
|
|
43
|
+
};
|
|
44
|
+
export const BooleanColumn = ({ children, source, field: FieldComponent }) => {
|
|
45
|
+
if (children) {
|
|
46
|
+
return (_jsx(_Fragment, { children: React.Children.map(children, (child) => React.isValidElement(child) ? React.cloneElement(child, { source }) : child) }));
|
|
47
|
+
}
|
|
48
|
+
if (FieldComponent) {
|
|
49
|
+
return _jsx(FieldComponent, { source: source });
|
|
50
|
+
}
|
|
51
|
+
return _jsx(BooleanField, { source: source });
|
|
52
|
+
};
|
|
53
|
+
export const ReferenceColumn = ({ children, source, reference, link, field: FieldComponent }) => {
|
|
54
|
+
// ReferenceCol requires reference, so we pass it down
|
|
55
|
+
if (FieldComponent) {
|
|
56
|
+
return (_jsx(FieldComponent, { reference: reference, link: link, source: source, children: children }));
|
|
57
|
+
}
|
|
58
|
+
return (_jsx(ReferenceField, { reference: reference, link: link, source: source, children: children }));
|
|
59
|
+
};
|
|
60
|
+
const defaultPageSizeOptions = [
|
|
61
|
+
{ value: 10, label: '10 items' },
|
|
62
|
+
{ value: 25, label: '25 items' },
|
|
63
|
+
{ value: 50, label: '50 items' },
|
|
64
|
+
{ value: 100, label: '100 items' },
|
|
65
|
+
];
|
|
66
|
+
/**
|
|
67
|
+
* The Table component provides a declarative way to build data tables with Cloudscape components
|
|
68
|
+
* while integrating with React Admin's data fetching and state management.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```tsx
|
|
72
|
+
* <Table title="Products">
|
|
73
|
+
* <Table.Column source="name" label="Name" />
|
|
74
|
+
* <Table.NumberColumn source="price" label="Price" />
|
|
75
|
+
* </Table>
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export const Table = ({ title, actions, children, include, exclude, filtering = true, filteringPlaceholder, pageSizeOptions = defaultPageSizeOptions, preferences = true, reorderable = true, defaultVisibleFields, selectionType, ...props }) => {
|
|
79
|
+
const resource = useResourceContext();
|
|
80
|
+
const translate = useTranslate();
|
|
81
|
+
const translateLabel = useTranslateLabel();
|
|
82
|
+
const schemaChildren = useFieldSchema();
|
|
83
|
+
const resourceDefinition = useResourceDefinition({ resource });
|
|
84
|
+
const finalSelectionType = selectionType ?? (resourceDefinition?.options?.canDelete ? 'multi' : undefined);
|
|
85
|
+
const finalChildren = React.useMemo(() => {
|
|
86
|
+
const baseChildren = children || schemaChildren;
|
|
87
|
+
let result = React.Children.toArray(baseChildren);
|
|
88
|
+
if (include) {
|
|
89
|
+
result = result.filter((child) => React.isValidElement(child) && include.includes(child.props.source));
|
|
90
|
+
}
|
|
91
|
+
else if (exclude) {
|
|
92
|
+
result = result.filter((child) => React.isValidElement(child) && !exclude.includes(child.props.source));
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}, [children, schemaChildren, include, exclude]);
|
|
96
|
+
// 1. Extract columns and options before calling useCollection
|
|
97
|
+
const extractedColumns = React.useMemo(() => {
|
|
98
|
+
const columns = [];
|
|
99
|
+
const options = [];
|
|
100
|
+
finalChildren.forEach((child, index) => {
|
|
101
|
+
if (!React.isValidElement(child)) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const { source, label, header: childHeader, sortable, link, field, children: childChildren, ...restColumnProps } = child.props;
|
|
105
|
+
const isNumberColumn = child.type?.isNumberColumn;
|
|
106
|
+
const headerLabel = translateLabel({ label, resource, source });
|
|
107
|
+
const finalHeader = isNumberColumn ? _jsx(Box, { textAlign: "right", children: headerLabel }) : headerLabel;
|
|
108
|
+
const columnId = source || `col-${index}`;
|
|
109
|
+
const id = resource ? `${resource}___${columnId}` : columnId;
|
|
110
|
+
columns.push({
|
|
111
|
+
...restColumnProps,
|
|
112
|
+
id,
|
|
113
|
+
header: finalHeader,
|
|
114
|
+
cell: (item) => {
|
|
115
|
+
const content = _jsx(RecordContextProvider, { value: item, children: child });
|
|
116
|
+
return isNumberColumn ? _jsx(Box, { textAlign: "right", children: content }) : content;
|
|
117
|
+
},
|
|
118
|
+
sortingField: sortable !== false ? source : undefined,
|
|
119
|
+
});
|
|
120
|
+
// If we have a meaningful label/header string, allow toggling
|
|
121
|
+
if (typeof headerLabel === 'string') {
|
|
122
|
+
options.push({
|
|
123
|
+
id,
|
|
124
|
+
label: headerLabel,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
return { columns, options };
|
|
129
|
+
}, [finalChildren, resource, translateLabel]);
|
|
130
|
+
const defaultVisibleContent = React.useMemo(() => {
|
|
131
|
+
if (extractedColumns.options.length === 0)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (defaultVisibleFields) {
|
|
134
|
+
// Map user-provided fields to their actual IDs
|
|
135
|
+
return extractedColumns.options
|
|
136
|
+
.filter((opt) => {
|
|
137
|
+
const column = extractedColumns.columns.find((c) => c.id === opt.id);
|
|
138
|
+
return (defaultVisibleFields.includes(opt.id) ||
|
|
139
|
+
(column?.sortingField && defaultVisibleFields.includes(column.sortingField)));
|
|
140
|
+
})
|
|
141
|
+
.map((opt) => opt.id);
|
|
142
|
+
}
|
|
143
|
+
// Default to first 5 toggleable columns
|
|
144
|
+
return extractedColumns.options.slice(0, 5).map((opt) => opt.id);
|
|
145
|
+
}, [extractedColumns, defaultVisibleFields]);
|
|
146
|
+
const defaultContentDisplay = React.useMemo(() => {
|
|
147
|
+
if (extractedColumns.options.length === 0)
|
|
148
|
+
return undefined;
|
|
149
|
+
const visibleIds = defaultVisibleContent || [];
|
|
150
|
+
return extractedColumns.options.map((opt) => ({
|
|
151
|
+
id: opt.id,
|
|
152
|
+
visible: visibleIds.includes(opt.id),
|
|
153
|
+
}));
|
|
154
|
+
}, [extractedColumns.options, defaultVisibleContent]);
|
|
155
|
+
const { items, paginationProps, collectionProps, filterProps, preferencesProps } = useCollection({
|
|
156
|
+
filtering: {},
|
|
157
|
+
pagination: {},
|
|
158
|
+
sorting: {},
|
|
159
|
+
preferences: {
|
|
160
|
+
pageSizeOptions,
|
|
161
|
+
visibleContentOptions: !reorderable && extractedColumns.options.length > 0 ? extractedColumns.options : undefined,
|
|
162
|
+
contentDisplayOptions: reorderable && extractedColumns.options.length > 0 ? extractedColumns.options : undefined,
|
|
163
|
+
visibleContent: defaultVisibleContent,
|
|
164
|
+
contentDisplay: defaultContentDisplay,
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
// 2. Filter columnDefinitions if reordering is disabled (Cloudscape Table handles it if columnDisplay is passed)
|
|
168
|
+
const columnDefinitions = React.useMemo(() => {
|
|
169
|
+
if (reorderable || !preferencesProps.preferences.visibleContent) {
|
|
170
|
+
return extractedColumns.columns;
|
|
171
|
+
}
|
|
172
|
+
return extractedColumns.columns.filter((col) => {
|
|
173
|
+
// Always show columns that are not in options (non-toggleable columns like Actions)
|
|
174
|
+
const isToggleable = extractedColumns.options.some((opt) => opt.id === col.id);
|
|
175
|
+
if (!isToggleable)
|
|
176
|
+
return true;
|
|
177
|
+
return preferencesProps.preferences.visibleContent?.includes(col.id);
|
|
178
|
+
});
|
|
179
|
+
}, [extractedColumns.columns, extractedColumns.options, preferencesProps.preferences.visibleContent, reorderable]);
|
|
180
|
+
const getResourceLabel = useGetResourceLabel();
|
|
181
|
+
const tableHeader = React.useMemo(() => {
|
|
182
|
+
if (title === null || title === false) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if (React.isValidElement(title)) {
|
|
186
|
+
return title;
|
|
187
|
+
}
|
|
188
|
+
const finalTitle = title !== undefined ? title : getResourceLabel(resource, 2);
|
|
189
|
+
return _jsx(TableHeader, { title: finalTitle, actions: actions });
|
|
190
|
+
}, [title, actions, resource, getResourceLabel]);
|
|
191
|
+
return (_jsx(CloudscapeTable, { ...collectionProps, ...props, selectionType: finalSelectionType, stripedRows: preferencesProps.preferences.stripedRows, wrapLines: preferencesProps.preferences.wrapLines, columnDefinitions: columnDefinitions, columnDisplay: reorderable ? preferencesProps.preferences.contentDisplay : undefined, items: items || [], header: tableHeader, filter: filtering && (_jsx(TextFilter, { ...filterProps })), pagination: _jsx(Pagination, { ...paginationProps }), preferences: preferences === true || pageSizeOptions ? (_jsx(CollectionPreferences, { ...preferencesProps, pageSizePreference: pageSizeOptions
|
|
192
|
+
? {
|
|
193
|
+
options: pageSizeOptions,
|
|
194
|
+
}
|
|
195
|
+
: undefined, visibleContentPreference: !reorderable && extractedColumns.options.length > 0
|
|
196
|
+
? {
|
|
197
|
+
title: translate('ra.action.select_columns', { _: 'Select visible columns' }),
|
|
198
|
+
options: [
|
|
199
|
+
{
|
|
200
|
+
label: translate('ra.action.select_columns', { _: 'Select visible columns' }),
|
|
201
|
+
options: extractedColumns.options,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
}
|
|
205
|
+
: undefined, contentDisplayPreference: reorderable && extractedColumns.options.length > 0
|
|
206
|
+
? {
|
|
207
|
+
title: translate('ra.action.select_columns', { _: 'Select visible columns' }),
|
|
208
|
+
options: extractedColumns.options,
|
|
209
|
+
}
|
|
210
|
+
: undefined })) : React.isValidElement(preferences) ? (preferences) : undefined }));
|
|
211
|
+
};
|
|
212
|
+
Table.Column = Column;
|
|
213
|
+
Table.NumberColumn = NumberColumn;
|
|
214
|
+
Table.DateColumn = DateColumn;
|
|
215
|
+
Table.BooleanColumn = BooleanColumn;
|
|
216
|
+
Table.ReferenceColumn = ReferenceColumn;
|
|
217
|
+
Table.Header = TableHeader;
|
|
218
|
+
export default Table;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { HeaderProps } from '@cloudscape-design/components/header';
|
|
3
|
+
export interface TableHeaderProps extends Omit<HeaderProps, 'children'> {
|
|
4
|
+
title?: React.ReactNode;
|
|
5
|
+
}
|
|
6
|
+
export declare const TableHeader: ({ title, actions, ...props }: TableHeaderProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export default TableHeader;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import Header from '@cloudscape-design/components/header';
|
|
4
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
5
|
+
import { useListContext, useTranslate, useLocale } from '@strato-admin/core';
|
|
6
|
+
import { BulkDeleteButton } from '../button/BulkDeleteButton';
|
|
7
|
+
import { CreateButton } from '../button/CreateButton';
|
|
8
|
+
export const TableHeader = ({ title, actions, ...props }) => {
|
|
9
|
+
const translate = useTranslate();
|
|
10
|
+
const locale = useLocale();
|
|
11
|
+
const { total, isPending, defaultTitle } = useListContext();
|
|
12
|
+
const headerTitle = React.useMemo(() => {
|
|
13
|
+
if (title !== undefined) {
|
|
14
|
+
return typeof title === 'string' ? translate(title, { _: title }) : title;
|
|
15
|
+
}
|
|
16
|
+
return defaultTitle;
|
|
17
|
+
}, [title, defaultTitle, translate, locale]);
|
|
18
|
+
const counter = props.counter !== undefined ? props.counter : !isPending && total !== undefined ? `(${total})` : undefined;
|
|
19
|
+
const headerActions = actions !== undefined ? (actions) : (_jsxs(SpaceBetween, { direction: "horizontal", size: "xs", children: [_jsx(BulkDeleteButton, {}), _jsx(CreateButton, {})] }));
|
|
20
|
+
return (_jsx(Header, { variant: "h2", ...props, actions: headerActions, counter: counter, children: headerTitle }));
|
|
21
|
+
};
|
|
22
|
+
export default TableHeader;
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { useStore } from '@strato-admin/core';
|
|
3
|
+
import { Mode, applyMode } from '@cloudscape-design/global-styles';
|
|
4
|
+
export const ThemeManager = () => {
|
|
5
|
+
const [theme] = useStore('theme', 'light');
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
applyMode(theme === 'dark' ? Mode.Dark : Mode.Light);
|
|
8
|
+
}, [theme]);
|
|
9
|
+
return null;
|
|
10
|
+
};
|
|
11
|
+
export default ThemeManager;
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@strato-admin/cloudscape",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Strato Admin Cloudscape implementation - UI component library and theme for React Admin",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"src",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"strato",
|
|
24
|
+
"admin",
|
|
25
|
+
"react-admin",
|
|
26
|
+
"cloudscape",
|
|
27
|
+
"aws",
|
|
28
|
+
"ui"
|
|
29
|
+
],
|
|
30
|
+
"author": "Vadim Gubergrits <vadim.gubergrits@gmail.com>",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/vgrits/strato-admin.git",
|
|
35
|
+
"directory": "packages/strato-cloudscape"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@cloudscape-design/collection-hooks": "^1.0.85",
|
|
39
|
+
"@cloudscape-design/components": "^3.0.1217",
|
|
40
|
+
"@cloudscape-design/global-styles": "^1.0.0",
|
|
41
|
+
"inflection": "^3.0.2",
|
|
42
|
+
"react-hook-form": "^7.71.2",
|
|
43
|
+
"react-router-dom": "^6.22.3",
|
|
44
|
+
"@strato-admin/core": "0.1.0",
|
|
45
|
+
"@strato-admin/i18n": "0.1.0",
|
|
46
|
+
"@strato-admin/language-en": "0.1.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@chromatic-com/storybook": "^5.0.1",
|
|
50
|
+
"@playwright/test": "^1.58.2",
|
|
51
|
+
"@storybook/addon-a11y": "^10.2.17",
|
|
52
|
+
"@storybook/addon-docs": "^10.2.17",
|
|
53
|
+
"@storybook/addon-onboarding": "^10.2.17",
|
|
54
|
+
"@storybook/addon-vitest": "^10.2.17",
|
|
55
|
+
"@storybook/react": "^10.2.17",
|
|
56
|
+
"@storybook/react-vite": "^10.2.17",
|
|
57
|
+
"@types/react": "^19.0.0",
|
|
58
|
+
"@types/react-dom": "^19.0.0",
|
|
59
|
+
"@vitest/browser-playwright": "^4.0.18",
|
|
60
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
61
|
+
"eslint-plugin-storybook": "^10.2.17",
|
|
62
|
+
"playwright": "^1.58.2",
|
|
63
|
+
"react": "^19.0.0",
|
|
64
|
+
"react-dom": "^19.0.0",
|
|
65
|
+
"storybook": "^10.2.17"
|
|
66
|
+
},
|
|
67
|
+
"scripts": {
|
|
68
|
+
"build": "tsc -p tsconfig.build.json",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"storybook": "storybook dev -p 6006",
|
|
71
|
+
"build-storybook": "storybook build"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { Admin } from './Admin';
|
|
5
|
+
import { Resource } from '@strato-admin/core';
|
|
6
|
+
|
|
7
|
+
// Mock Cloudscape TopNavigation
|
|
8
|
+
vi.mock('./layout/TopNavigation', () => ({
|
|
9
|
+
TopNavigation: () => <div data-testid="top-nav" />,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe('Admin', () => {
|
|
13
|
+
it('should render without crashing when no i18nProvider is provided', () => {
|
|
14
|
+
const dataProvider = {
|
|
15
|
+
getList: () => Promise.resolve({ data: [], total: 0 }),
|
|
16
|
+
getOne: () => Promise.resolve({ data: {} }),
|
|
17
|
+
getMany: () => Promise.resolve({ data: [] }),
|
|
18
|
+
getManyReference: () => Promise.resolve({ data: [], total: 0 }),
|
|
19
|
+
update: () => Promise.resolve({ data: {} }),
|
|
20
|
+
updateMany: () => Promise.resolve({ data: [] }),
|
|
21
|
+
create: () => Promise.resolve({ data: {} }),
|
|
22
|
+
delete: () => Promise.resolve({ data: {} }),
|
|
23
|
+
deleteMany: () => Promise.resolve({ data: [] }),
|
|
24
|
+
} as any;
|
|
25
|
+
|
|
26
|
+
render(
|
|
27
|
+
<Admin dataProvider={dataProvider}>
|
|
28
|
+
<Resource name="posts" list={() => <div>Posts List</div>} />
|
|
29
|
+
</Admin>,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|