@strato-admin/cloudscape 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Admin.d.ts +6 -2
- package/dist/Admin.js +14 -8
- package/dist/RecordLink.js +5 -4
- package/dist/Settings.d.ts +17 -0
- package/dist/Settings.js +14 -0
- package/dist/button/BulkDeleteButton.d.ts +4 -1
- package/dist/button/BulkDeleteButton.js +37 -5
- package/dist/button/Button.d.ts +2 -1
- package/dist/button/CancelButton.d.ts +6 -0
- package/dist/button/CancelButton.js +10 -0
- package/dist/button/CreateButton.js +9 -8
- package/dist/button/DeleteButton.d.ts +13 -0
- package/dist/button/DeleteButton.js +36 -0
- package/dist/button/EditButton.d.ts +1 -1
- package/dist/button/EditButton.js +10 -10
- package/dist/button/SaveButton.js +2 -2
- package/dist/button/index.d.ts +2 -0
- package/dist/button/index.js +2 -0
- package/dist/collection-hooks/interfaces.d.ts +7 -3
- package/dist/collection-hooks/useCollection.d.ts +1 -1
- package/dist/collection-hooks/useCollection.js +15 -10
- package/dist/create/Create.d.ts +9 -17
- package/dist/create/Create.js +40 -12
- package/dist/create/CreateHeader.d.ts +2 -2
- package/dist/create/CreateHeader.js +4 -5
- package/dist/defaults.d.ts +6 -0
- package/dist/defaults.js +21 -0
- package/dist/detail/Detail.d.ts +33 -0
- package/dist/detail/Detail.js +22 -0
- package/dist/detail/DetailHeader.d.ts +11 -0
- package/dist/detail/{ShowHeader.js → DetailHeader.js} +7 -5
- package/dist/detail/DetailHub.d.ts +27 -0
- package/dist/detail/DetailHub.js +63 -0
- package/dist/detail/KeyValuePairs.d.ts +7 -1
- package/dist/detail/KeyValuePairs.js +14 -8
- package/dist/detail/index.d.ts +3 -2
- package/dist/detail/index.js +3 -2
- package/dist/edit/Edit.d.ts +8 -19
- package/dist/edit/Edit.js +48 -12
- package/dist/edit/EditHeader.d.ts +2 -2
- package/dist/edit/EditHeader.js +5 -4
- package/dist/field/ArrayField.d.ts +26 -10
- package/dist/field/ArrayField.js +38 -10
- package/dist/field/BadgeField.d.ts +1 -1
- package/dist/field/BadgeField.js +1 -1
- package/dist/field/BooleanField.d.ts +1 -1
- package/dist/field/BooleanField.js +2 -2
- package/dist/field/CurrencyField.d.ts +1 -1
- package/dist/field/CurrencyField.js +1 -1
- package/dist/field/DateField.d.ts +1 -1
- package/dist/field/DateField.js +1 -1
- package/dist/field/IdField.d.ts +1 -1
- package/dist/field/IdField.js +3 -3
- package/dist/field/NumberField.d.ts +1 -1
- package/dist/field/NumberField.js +1 -1
- package/dist/field/ReferenceField.d.ts +1 -1
- package/dist/field/ReferenceField.js +4 -2
- package/dist/field/ReferenceManyField.d.ts +35 -4
- package/dist/field/ReferenceManyField.js +17 -4
- package/dist/field/StatusIndicatorField.d.ts +1 -1
- package/dist/field/StatusIndicatorField.js +6 -5
- package/dist/field/TextField.d.ts +1 -1
- package/dist/field/TextField.js +1 -1
- package/dist/field/types.d.ts +9 -9
- package/dist/form/Form.d.ts +12 -2
- package/dist/form/Form.js +10 -16
- package/dist/form/index.d.ts +1 -1
- package/dist/form/index.js +1 -1
- package/dist/hooks/useSchemaFields.d.ts +22 -0
- package/dist/hooks/useSchemaFields.js +45 -0
- package/dist/i18n/Message.d.ts +15 -0
- package/dist/i18n/Message.js +19 -0
- package/dist/i18n/RecordMessage.d.ts +14 -0
- package/dist/i18n/RecordMessage.js +16 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/types.d.ts +19 -0
- package/dist/i18n/types.js +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/input/ArrayInput.d.ts +33 -0
- package/dist/input/{AttributeEditor.js → ArrayInput.js} +18 -11
- package/dist/input/AutocompleteInput.d.ts +1 -1
- package/dist/input/AutocompleteInput.js +3 -3
- package/dist/input/BooleanInput.d.ts +6 -0
- package/dist/input/BooleanInput.js +23 -0
- package/dist/input/CommonInputProps.d.ts +6 -0
- package/dist/input/CommonInputProps.js +6 -0
- package/dist/input/FieldTitle.js +4 -4
- package/dist/input/FormField.js +12 -3
- package/dist/input/FormFieldContext.d.ts +1 -1
- package/dist/input/NumberInput.d.ts +1 -1
- package/dist/input/NumberInput.js +3 -3
- package/dist/input/ReferenceInput.d.ts +1 -1
- package/dist/input/ReferenceInput.js +22 -12
- package/dist/input/SelectInput.d.ts +1 -1
- package/dist/input/SelectInput.js +3 -3
- package/dist/input/SliderInput.d.ts +1 -1
- package/dist/input/SliderInput.js +4 -4
- package/dist/input/TextAreaInput.d.ts +1 -1
- package/dist/input/TextAreaInput.js +3 -3
- package/dist/input/TextInput.d.ts +1 -1
- package/dist/input/TextInput.js +6 -12
- package/dist/input/index.d.ts +2 -1
- package/dist/input/index.js +2 -1
- package/dist/input/types.d.ts +33 -2
- package/dist/layout/AppLayout.js +6 -3
- package/dist/layout/Notifications.d.ts +1 -0
- package/dist/layout/Notifications.js +51 -0
- package/dist/layout/Ready.d.ts +6 -0
- package/dist/layout/Ready.js +24 -0
- package/dist/layout/TopNavigation.d.ts +4 -2
- package/dist/layout/TopNavigation.js +7 -7
- package/dist/layout/index.d.ts +2 -0
- package/dist/layout/index.js +2 -0
- package/dist/list/Cards.d.ts +31 -4
- package/dist/list/Cards.js +81 -10
- package/dist/list/List.d.ts +9 -12
- package/dist/list/List.js +41 -11
- package/dist/list/Table.d.ts +8 -4
- package/dist/list/Table.js +55 -55
- package/dist/list/TableHeader.d.ts +2 -2
- package/dist/list/TableHeader.js +4 -5
- package/dist/theme/ThemeManager.js +1 -1
- package/package.json +9 -6
- package/src/Admin.tsx +35 -18
- package/src/RecordLink.stories.tsx +1 -1
- package/src/RecordLink.tsx +5 -4
- package/src/Settings.tsx +16 -0
- package/src/__mocks__/ra-core.tsx +83 -0
- package/src/__mocks__/strato-core.tsx +36 -42
- package/src/button/BulkDeleteButton.test.tsx +45 -8
- package/src/button/BulkDeleteButton.tsx +75 -12
- package/src/button/Button.tsx +31 -2
- package/src/button/CancelButton.tsx +20 -0
- package/src/button/CreateButton.tsx +12 -10
- package/src/button/DeleteButton.tsx +96 -0
- package/src/button/EditButton.tsx +13 -12
- package/src/button/SaveButton.tsx +2 -3
- package/src/button/index.ts +2 -0
- package/src/collection-hooks/interfaces.ts +7 -3
- package/src/collection-hooks/useCollection.test.ts +115 -2
- package/src/collection-hooks/useCollection.ts +15 -10
- package/src/create/Create.test.tsx +3 -3
- package/src/create/Create.tsx +68 -37
- package/src/create/CreateHeader.tsx +6 -10
- package/src/defaults.tsx +28 -0
- package/src/detail/Detail-CollectionFields.test.tsx +84 -0
- package/src/detail/Detail.test.tsx +91 -0
- package/src/detail/Detail.tsx +48 -0
- package/src/detail/{ShowHeader.test.tsx → DetailHeader.test.tsx} +11 -9
- package/src/detail/DetailHeader.tsx +42 -0
- package/src/detail/DetailHub.tsx +88 -0
- package/src/detail/KeyValuePairs.test.tsx +2 -2
- package/src/detail/KeyValuePairs.tsx +25 -18
- package/src/detail/index.ts +3 -2
- package/src/edit/Edit.test.tsx +7 -5
- package/src/edit/Edit.tsx +92 -40
- package/src/edit/EditHeader.tsx +7 -5
- package/src/field/ArrayField.tsx +57 -11
- package/src/field/BadgeField.tsx +2 -3
- package/src/field/BooleanField.test.tsx +2 -3
- package/src/field/BooleanField.tsx +3 -3
- package/src/field/CurrencyField.tsx +1 -1
- package/src/field/DateField.tsx +1 -1
- package/src/field/IdField.test.tsx +8 -20
- package/src/field/IdField.tsx +5 -20
- package/src/field/NumberField.tsx +1 -1
- package/src/field/ReferenceField.test.tsx +15 -6
- package/src/field/ReferenceField.tsx +10 -7
- package/src/field/ReferenceManyField.test.tsx +55 -10
- package/src/field/ReferenceManyField.tsx +84 -13
- package/src/field/StatusIndicatorField.test.tsx +7 -21
- package/src/field/StatusIndicatorField.tsx +8 -20
- package/src/field/TextField.tsx +1 -1
- package/src/field/types.ts +12 -13
- package/src/form/Form.test.tsx +8 -4
- package/src/form/Form.tsx +24 -19
- package/src/form/index.ts +1 -1
- package/src/hooks/useSchemaFields.ts +89 -0
- package/src/i18n/Message.tsx +22 -0
- package/src/i18n/RecordMessage.tsx +22 -0
- package/src/i18n/index.ts +3 -0
- package/src/i18n/types.ts +19 -0
- package/src/index.ts +5 -1
- package/src/input/ArrayInput.test.tsx +81 -0
- package/src/input/{AttributeEditor.tsx → ArrayInput.tsx} +36 -18
- package/src/input/AutocompleteInput.test.tsx +2 -4
- package/src/input/AutocompleteInput.tsx +9 -11
- package/src/input/BooleanInput.tsx +42 -0
- package/src/input/CommonInputProps.tsx +8 -0
- package/src/input/FieldTitle.tsx +3 -15
- package/src/input/FormField.tsx +78 -67
- package/src/input/FormFieldContext.ts +1 -1
- package/src/input/NumberInput.tsx +10 -7
- package/src/input/ReferenceInput.test.tsx +12 -2
- package/src/input/ReferenceInput.tsx +32 -14
- package/src/input/SelectInput.tsx +14 -17
- package/src/input/SliderInput.test.tsx +2 -3
- package/src/input/SliderInput.tsx +48 -38
- package/src/input/TextAreaInput.tsx +10 -6
- package/src/input/TextInput.test.tsx +2 -4
- package/src/input/TextInput.tsx +35 -20
- package/src/input/index.ts +2 -1
- package/src/input/types.ts +40 -8
- package/src/layout/AppLayout.test.tsx +23 -3
- package/src/layout/AppLayout.tsx +11 -8
- package/src/layout/Notifications.test.tsx +102 -0
- package/src/layout/Notifications.tsx +61 -0
- package/src/layout/Ready.tsx +123 -0
- package/src/layout/TopNavigation.test.tsx +2 -3
- package/src/layout/TopNavigation.tsx +9 -8
- package/src/layout/index.ts +2 -0
- package/src/list/Cards.test.tsx +320 -0
- package/src/list/Cards.tsx +146 -16
- package/src/list/List.tsx +87 -26
- package/src/list/Table.test.tsx +40 -5
- package/src/list/Table.tsx +89 -98
- package/src/list/TableHeader.test.tsx +15 -11
- package/src/list/TableHeader.tsx +6 -8
- package/src/theme/ThemeManager.tsx +1 -1
- package/dist/__mocks__/strato-core.js +0 -50
- package/dist/__mocks__to__delete/strato-core.js +0 -50
- package/dist/detail/Show.d.ts +0 -39
- package/dist/detail/Show.js +0 -40
- package/dist/detail/ShowHeader.d.ts +0 -7
- package/dist/input/AttributeEditor.d.ts +0 -25
- package/src/detail/Show.test.tsx +0 -96
- package/src/detail/Show.tsx +0 -104
- package/src/detail/ShowHeader.tsx +0 -35
- package/src/input/AttributeEditor.test.tsx +0 -147
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { useResourceContext, useTranslate, useResourceDefinitions } from '@strato-admin/ra-core';
|
|
2
|
+
import { useCreatePath } from '@strato-admin/core';
|
|
3
3
|
import { useNavigate } from 'react-router-dom';
|
|
4
4
|
import { Button, ButtonProps } from './Button';
|
|
5
|
+
import type { ButtonProps as CloudscapeButtonProps } from '@cloudscape-design/components/button';
|
|
5
6
|
|
|
6
7
|
export interface CreateButtonProps extends Omit<ButtonProps, 'children'> {
|
|
7
8
|
label?: string;
|
|
@@ -20,17 +21,18 @@ export const CreateButton = ({ label, variant = 'primary', ...props }: CreateBut
|
|
|
20
21
|
return null;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
const path = createPath({ resource, type: 'create' });
|
|
25
|
+
|
|
26
|
+
const handleClick: NonNullable<CloudscapeButtonProps['onClick']> = (e) => {
|
|
27
|
+
if (!e.detail.metaKey && !e.detail.ctrlKey && !e.detail.shiftKey && e.detail.button === 0) {
|
|
28
|
+
e.preventDefault();
|
|
29
|
+
navigate(path);
|
|
30
|
+
}
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
|
-
<Button variant={variant} onClick={handleClick} iconName="add-plus" {...props}>
|
|
33
|
-
{label || translate('
|
|
34
|
+
<Button variant={variant} href={path} onClick={handleClick} iconName="add-plus" {...props}>
|
|
35
|
+
{label || translate('strato.action.create', { _: 'Create' })}
|
|
34
36
|
</Button>
|
|
35
37
|
);
|
|
36
38
|
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { useDeleteController, useTranslate, useResourceDefinition, RaRecord } from '@strato-admin/ra-core';
|
|
3
|
+
import { useSettingValue } from '@strato-admin/core';
|
|
4
|
+
import Modal from '@cloudscape-design/components/modal';
|
|
5
|
+
import Box from '@cloudscape-design/components/box';
|
|
6
|
+
import SpaceBetween from '@cloudscape-design/components/space-between';
|
|
7
|
+
import { Button } from './Button';
|
|
8
|
+
|
|
9
|
+
export interface DeleteButtonProps {
|
|
10
|
+
label?: string;
|
|
11
|
+
variant?: 'primary' | 'normal' | 'link';
|
|
12
|
+
mutationMode?: 'undoable' | 'optimistic' | 'pessimistic';
|
|
13
|
+
successMessage?: string;
|
|
14
|
+
dialogTitle?: string;
|
|
15
|
+
dialogDescription?: string;
|
|
16
|
+
record?: RaRecord;
|
|
17
|
+
redirect?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DeleteButton = ({
|
|
21
|
+
label,
|
|
22
|
+
variant = 'normal',
|
|
23
|
+
mutationMode,
|
|
24
|
+
successMessage,
|
|
25
|
+
dialogTitle,
|
|
26
|
+
dialogDescription,
|
|
27
|
+
record,
|
|
28
|
+
redirect,
|
|
29
|
+
}: DeleteButtonProps) => {
|
|
30
|
+
const translate = useTranslate();
|
|
31
|
+
const { options } = useResourceDefinition();
|
|
32
|
+
const resolve = useSettingValue();
|
|
33
|
+
const { handleDelete, isPending, isLoading } = useDeleteController({
|
|
34
|
+
mutationMode: resolve(mutationMode, 'mutationMode'),
|
|
35
|
+
successMessage: successMessage ?? options?.deleteSuccessMessage ?? resolve(undefined, 'deleteSuccessMessage'),
|
|
36
|
+
record,
|
|
37
|
+
redirect,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
41
|
+
|
|
42
|
+
if (options?.canDelete === false) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const isBusy = isPending || isLoading;
|
|
47
|
+
|
|
48
|
+
const handleConfirm = () => {
|
|
49
|
+
handleDelete();
|
|
50
|
+
setIsOpen(false);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const defaultTitle = translate('strato.message.delete_title', {
|
|
54
|
+
_: 'Delete this item',
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const defaultDescription = translate('strato.message.delete_content', {
|
|
58
|
+
_: 'Are you sure you want to delete this item?',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<>
|
|
63
|
+
<Button
|
|
64
|
+
variant={variant}
|
|
65
|
+
onClick={() => setIsOpen(true)}
|
|
66
|
+
loading={isBusy}
|
|
67
|
+
disabled={isBusy}
|
|
68
|
+
data-testid="delete-button"
|
|
69
|
+
>
|
|
70
|
+
{label || translate('strato.action.delete', { _: 'Delete' })}
|
|
71
|
+
</Button>
|
|
72
|
+
<Modal
|
|
73
|
+
onDismiss={() => setIsOpen(false)}
|
|
74
|
+
visible={isOpen}
|
|
75
|
+
closeAriaLabel={translate('strato.action.close', { _: 'Close' })}
|
|
76
|
+
footer={
|
|
77
|
+
<Box float="right">
|
|
78
|
+
<SpaceBetween direction="horizontal" size="xs">
|
|
79
|
+
<Button variant="link" onClick={() => setIsOpen(false)}>
|
|
80
|
+
{translate('strato.action.cancel', { _: 'Cancel' })}
|
|
81
|
+
</Button>
|
|
82
|
+
<Button variant="primary" onClick={handleConfirm} loading={isBusy} data-testid="confirm-delete">
|
|
83
|
+
{translate('strato.action.confirm', { _: 'Confirm' })}
|
|
84
|
+
</Button>
|
|
85
|
+
</SpaceBetween>
|
|
86
|
+
</Box>
|
|
87
|
+
}
|
|
88
|
+
header={dialogTitle || defaultTitle}
|
|
89
|
+
>
|
|
90
|
+
{dialogDescription || defaultDescription}
|
|
91
|
+
</Modal>
|
|
92
|
+
</>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default DeleteButton;
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
1
|
+
import { useResourceContext, useRecordContext, useTranslate, RaRecord } from '@strato-admin/ra-core';
|
|
2
|
+
import { useCreatePath } from '@strato-admin/core';
|
|
3
3
|
import { useNavigate } from 'react-router-dom';
|
|
4
4
|
import { Button, ButtonProps } from './Button';
|
|
5
|
+
import type { ButtonProps as CloudscapeButtonProps } from '@cloudscape-design/components/button';
|
|
5
6
|
|
|
6
7
|
export interface EditButtonProps extends Omit<ButtonProps, 'children'> {
|
|
7
8
|
label?: string;
|
|
8
9
|
record?: RaRecord;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export const EditButton = ({ label, record: recordProp, variant = '
|
|
12
|
+
export const EditButton = ({ label, record: recordProp, variant = 'normal', ...props }: EditButtonProps) => {
|
|
12
13
|
const resource = useResourceContext();
|
|
13
14
|
const record = useRecordContext(recordProp);
|
|
14
15
|
const translate = useTranslate();
|
|
@@ -19,18 +20,18 @@ export const EditButton = ({ label, record: recordProp, variant = 'primary', ...
|
|
|
19
20
|
return null;
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
const path = createPath({ resource, id: record.id, type: 'edit' });
|
|
24
|
+
|
|
25
|
+
const handleClick: NonNullable<CloudscapeButtonProps['onClick']> = (e) => {
|
|
26
|
+
if (!e.detail.metaKey && !e.detail.ctrlKey && !e.detail.shiftKey && e.detail.button === 0) {
|
|
27
|
+
e.preventDefault();
|
|
28
|
+
navigate(path);
|
|
29
|
+
}
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
return (
|
|
32
|
-
<Button variant={variant} onClick={handleClick} {...props}>
|
|
33
|
-
{label || translate('
|
|
33
|
+
<Button variant={variant} href={path} onClick={handleClick} iconName="edit" {...props}>
|
|
34
|
+
{label || translate('strato.action.edit', { _: 'Edit' })}
|
|
34
35
|
</Button>
|
|
35
36
|
);
|
|
36
37
|
};
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { useTranslate } from '@strato-admin/core';
|
|
1
|
+
import { useTranslate } from '@strato-admin/ra-core';
|
|
3
2
|
import { Button, ButtonProps } from './Button';
|
|
4
3
|
|
|
5
4
|
export interface SaveButtonProps extends Omit<ButtonProps, 'children'> {
|
|
@@ -11,7 +10,7 @@ export const SaveButton = ({ label, variant = 'primary', ...props }: SaveButtonP
|
|
|
11
10
|
|
|
12
11
|
return (
|
|
13
12
|
<Button variant={variant} formAction="submit" nativeButtonAttributes={{ type: 'submit' }} {...props}>
|
|
14
|
-
{label || translate('
|
|
13
|
+
{label || translate('strato.action.save', { _: 'Save' })}
|
|
15
14
|
</Button>
|
|
16
15
|
);
|
|
17
16
|
};
|
package/src/button/index.ts
CHANGED
|
@@ -18,9 +18,13 @@ export interface CollectionPreferences {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface UseCollectionOptions<_T> {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Set to true when using a client-side data provider where the data array
|
|
23
|
+
* contains ALL records, not just the current page. The hook will slice the
|
|
24
|
+
* array for pagination instead of relying on the server.
|
|
25
|
+
* @default false
|
|
26
|
+
*/
|
|
27
|
+
clientSidePagination?: boolean;
|
|
24
28
|
preferences?: {
|
|
25
29
|
pageSizeOptions?: ReadonlyArray<{ value: number; label?: string }>;
|
|
26
30
|
visibleContentOptions?: ReadonlyArray<{
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { renderHook, act } from '@testing-library/react';
|
|
2
2
|
import { vi, describe, it, expect } from 'vitest';
|
|
3
|
-
import { useListContext } from '@strato-admin/core';
|
|
3
|
+
import { useListContext } from '@strato-admin/ra-core';
|
|
4
4
|
import { useCollection } from './useCollection';
|
|
5
5
|
|
|
6
|
-
vi.mock('@strato-admin/core', () => ({
|
|
6
|
+
vi.mock('@strato-admin/ra-core', () => ({
|
|
7
7
|
useListContext: vi.fn(),
|
|
8
8
|
}));
|
|
9
9
|
|
|
@@ -375,6 +375,32 @@ describe('useCollection', () => {
|
|
|
375
375
|
expect(result.current.preferencesProps.preferences.contentDisplay).toEqual(contentDisplay);
|
|
376
376
|
});
|
|
377
377
|
|
|
378
|
+
it('should re-sync visibleContent when options.preferences.visibleContent prop changes after mount', () => {
|
|
379
|
+
(useListContext as any).mockReturnValue({
|
|
380
|
+
data: [],
|
|
381
|
+
perPage: 25,
|
|
382
|
+
setPerPage: vi.fn(),
|
|
383
|
+
page: 1,
|
|
384
|
+
isPending: false,
|
|
385
|
+
isFetching: false,
|
|
386
|
+
isLoading: false,
|
|
387
|
+
setPage: vi.fn(),
|
|
388
|
+
selectedIds: [],
|
|
389
|
+
onSelect: vi.fn(),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const { result, rerender } = renderHook(
|
|
393
|
+
({ visibleContent }: { visibleContent: ReadonlyArray<string> }) =>
|
|
394
|
+
useCollection({ preferences: { visibleContent } }),
|
|
395
|
+
{ initialProps: { visibleContent: ['id'] } },
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
expect(result.current.preferencesProps.preferences.visibleContent).toEqual(['id']);
|
|
399
|
+
|
|
400
|
+
rerender({ visibleContent: ['id', 'name'] });
|
|
401
|
+
expect(result.current.preferencesProps.preferences.visibleContent).toEqual(['id', 'name']);
|
|
402
|
+
});
|
|
403
|
+
|
|
378
404
|
it('should update wrapLines and stripedRows when preferences onConfirm is called', () => {
|
|
379
405
|
(useListContext as any).mockReturnValue({
|
|
380
406
|
data: [],
|
|
@@ -411,3 +437,90 @@ describe('useCollection', () => {
|
|
|
411
437
|
expect(result.current.preferencesProps.preferences.stripedRows).toBe(true);
|
|
412
438
|
});
|
|
413
439
|
});
|
|
440
|
+
|
|
441
|
+
describe('useCollection — client-side pagination', () => {
|
|
442
|
+
const makeData = (count: number) => Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }));
|
|
443
|
+
|
|
444
|
+
const mockContext = (overrides: object) =>
|
|
445
|
+
(useListContext as any).mockReturnValue({
|
|
446
|
+
data: [],
|
|
447
|
+
total: undefined,
|
|
448
|
+
page: 1,
|
|
449
|
+
perPage: 25,
|
|
450
|
+
isPending: false,
|
|
451
|
+
isFetching: false,
|
|
452
|
+
isLoading: false,
|
|
453
|
+
setPage: vi.fn(),
|
|
454
|
+
setPerPage: vi.fn(),
|
|
455
|
+
selectedIds: [],
|
|
456
|
+
onSelect: vi.fn(),
|
|
457
|
+
sort: undefined,
|
|
458
|
+
setSort: vi.fn(),
|
|
459
|
+
filterValues: {},
|
|
460
|
+
setFilters: vi.fn(),
|
|
461
|
+
...overrides,
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('should return all data unsliced when clientSidePagination is false (default)', () => {
|
|
465
|
+
const data = makeData(10);
|
|
466
|
+
mockContext({ data, total: 10, page: 1, perPage: 3 });
|
|
467
|
+
|
|
468
|
+
const { result } = renderHook(() => useCollection({}));
|
|
469
|
+
|
|
470
|
+
expect(result.current.items).toEqual(data);
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should return the first page slice when clientSidePagination is true', () => {
|
|
474
|
+
const data = makeData(10);
|
|
475
|
+
mockContext({ data, total: 10, page: 1, perPage: 3 });
|
|
476
|
+
|
|
477
|
+
const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
|
|
478
|
+
|
|
479
|
+
expect(result.current.items).toEqual([
|
|
480
|
+
{ id: 1, name: 'Item 1' },
|
|
481
|
+
{ id: 2, name: 'Item 2' },
|
|
482
|
+
{ id: 3, name: 'Item 3' },
|
|
483
|
+
]);
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('should return the correct slice for a middle page', () => {
|
|
487
|
+
const data = makeData(10);
|
|
488
|
+
mockContext({ data, total: 10, page: 2, perPage: 3 });
|
|
489
|
+
|
|
490
|
+
const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
|
|
491
|
+
|
|
492
|
+
expect(result.current.items).toEqual([
|
|
493
|
+
{ id: 4, name: 'Item 4' },
|
|
494
|
+
{ id: 5, name: 'Item 5' },
|
|
495
|
+
{ id: 6, name: 'Item 6' },
|
|
496
|
+
]);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it('should return the remaining items on the last partial page', () => {
|
|
500
|
+
const data = makeData(10);
|
|
501
|
+
mockContext({ data, total: 10, page: 4, perPage: 3 });
|
|
502
|
+
|
|
503
|
+
const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
|
|
504
|
+
|
|
505
|
+
expect(result.current.items).toEqual([{ id: 10, name: 'Item 10' }]);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should NOT slice when a server-side provider returns all items in one response', () => {
|
|
509
|
+
// 30 items, perPage=25 — old heuristic would have incorrectly sliced to 25
|
|
510
|
+
const data = makeData(30);
|
|
511
|
+
mockContext({ data, total: 30, page: 1, perPage: 25 });
|
|
512
|
+
|
|
513
|
+
const { result } = renderHook(() => useCollection({}));
|
|
514
|
+
|
|
515
|
+
expect(result.current.items).toEqual(data);
|
|
516
|
+
expect(result.current.items).toHaveLength(30);
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
it('should return undefined items when data is undefined and clientSidePagination is true', () => {
|
|
520
|
+
mockContext({ data: undefined, total: undefined, page: 1, perPage: 25 });
|
|
521
|
+
|
|
522
|
+
const { result } = renderHook(() => useCollection({ clientSidePagination: true }));
|
|
523
|
+
|
|
524
|
+
expect(result.current.items).toBeUndefined();
|
|
525
|
+
});
|
|
526
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState, useMemo } from 'react';
|
|
2
|
-
import { useListContext, RaRecord } from '@strato-admin/core';
|
|
1
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
2
|
+
import { useListContext, RaRecord } from '@strato-admin/ra-core';
|
|
3
3
|
import { UseCollectionOptions, UseCollectionResult, TableColumnDisplay } from './interfaces';
|
|
4
4
|
|
|
5
5
|
export function useCollection<T extends RaRecord>(options: UseCollectionOptions<T>): UseCollectionResult<T> {
|
|
@@ -31,17 +31,24 @@ export function useCollection<T extends RaRecord>(options: UseCollectionOptions<
|
|
|
31
31
|
options.preferences?.visibleContent ?? options.preferences?.visibleContentOptions?.map((o) => o.id),
|
|
32
32
|
);
|
|
33
33
|
const [contentDisplay, setContentDisplay] = useState<ReadonlyArray<TableColumnDisplay> | undefined>(
|
|
34
|
-
options.preferences?.contentDisplay ??
|
|
34
|
+
options.preferences?.contentDisplay ??
|
|
35
|
+
options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
|
|
35
36
|
);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
useEffect(() => {
|
|
38
39
|
setVisibleContent(
|
|
39
40
|
options.preferences?.visibleContent ?? options.preferences?.visibleContentOptions?.map((o) => o.id),
|
|
40
41
|
);
|
|
41
42
|
setContentDisplay(
|
|
42
|
-
options.preferences?.contentDisplay ??
|
|
43
|
+
options.preferences?.contentDisplay ??
|
|
44
|
+
options.preferences?.contentDisplayOptions?.map((o) => ({ id: o.id, visible: true })),
|
|
43
45
|
);
|
|
44
|
-
}, [
|
|
46
|
+
}, [
|
|
47
|
+
options.preferences?.visibleContent,
|
|
48
|
+
options.preferences?.visibleContentOptions,
|
|
49
|
+
options.preferences?.contentDisplay,
|
|
50
|
+
options.preferences?.contentDisplayOptions,
|
|
51
|
+
]);
|
|
45
52
|
|
|
46
53
|
const selectedItems = (selectedIds || []).map((id) => {
|
|
47
54
|
const item = data?.find((i) => i.id === id);
|
|
@@ -50,15 +57,13 @@ export function useCollection<T extends RaRecord>(options: UseCollectionOptions<
|
|
|
50
57
|
});
|
|
51
58
|
|
|
52
59
|
const items = useMemo(() => {
|
|
53
|
-
|
|
54
|
-
// We detect this by checking if data.length equals total and if data.length is greater than perPage.
|
|
55
|
-
if (data && total === data.length && data.length > (perPage || 0)) {
|
|
60
|
+
if (options.clientSidePagination && data) {
|
|
56
61
|
const p = page || 1;
|
|
57
62
|
const pp = perPage || 25;
|
|
58
63
|
return data.slice((p - 1) * pp, p * pp);
|
|
59
64
|
}
|
|
60
65
|
return data;
|
|
61
|
-
}, [data,
|
|
66
|
+
}, [data, options.clientSidePagination, page, perPage]);
|
|
62
67
|
|
|
63
68
|
return {
|
|
64
69
|
items,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
-
import { useResourceContext } from '@strato-admin/core';
|
|
4
|
+
import { useResourceContext } from '@strato-admin/ra-core';
|
|
5
5
|
import { Create } from './Create';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
8
8
|
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
9
9
|
|
|
10
10
|
// Mock Cloudscape components
|
|
@@ -46,7 +46,7 @@ describe('Create', () => {
|
|
|
46
46
|
|
|
47
47
|
expect(getByTestId('container')).toBeDefined();
|
|
48
48
|
expect(getByTestId('content').textContent).toBe('Create New');
|
|
49
|
-
expect(getByText(
|
|
49
|
+
expect(getByText(/Products/)).toBeDefined();
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('should use provided title', () => {
|
package/src/create/Create.tsx
CHANGED
|
@@ -1,45 +1,76 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { CreateBase, type RaRecord,
|
|
2
|
+
import { CreateBase, type RaRecord, type CreateBaseProps, Identifier, useTranslate } from '@strato-admin/ra-core';
|
|
3
|
+
import {
|
|
4
|
+
ResourceSchemaProvider,
|
|
5
|
+
useResourceSchema,
|
|
6
|
+
useConstructedPageTitle,
|
|
7
|
+
useSettingValue,
|
|
8
|
+
} from '@strato-admin/core';
|
|
3
9
|
import Container from '@cloudscape-design/components/container';
|
|
4
10
|
import { CreateHeader } from './CreateHeader';
|
|
5
11
|
import Form from '../form/Form';
|
|
6
12
|
|
|
7
|
-
export interface CreateProps<
|
|
13
|
+
export interface CreateProps<
|
|
14
|
+
RecordType extends Omit<RaRecord, 'id'> = any,
|
|
15
|
+
ResultRecordType extends RaRecord = RecordType & { id: Identifier },
|
|
16
|
+
MutationOptionsError = Error,
|
|
17
|
+
> extends CreateBaseProps<RecordType, ResultRecordType, MutationOptionsError> {
|
|
8
18
|
children?: React.ReactNode;
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
title?: React.ReactNode | ((record: Partial<RecordType>) => React.ReactNode);
|
|
20
|
+
description?: React.ReactNode | ((record: Partial<RecordType>) => React.ReactNode);
|
|
11
21
|
actions?: React.ReactNode;
|
|
12
|
-
resource?: string;
|
|
13
|
-
record?: Partial<RecordType>;
|
|
14
|
-
redirect?: any;
|
|
15
|
-
transform?: any;
|
|
16
|
-
mutationOptions?: any;
|
|
17
22
|
include?: string[];
|
|
18
23
|
exclude?: string[];
|
|
24
|
+
saveButtonLabel?: string;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
const CreateUI = ({
|
|
22
28
|
children,
|
|
23
|
-
resource,
|
|
24
|
-
inputSchema,
|
|
25
29
|
title,
|
|
26
30
|
actions,
|
|
31
|
+
description,
|
|
27
32
|
include,
|
|
28
33
|
exclude,
|
|
34
|
+
saveButtonLabel,
|
|
29
35
|
}: {
|
|
30
36
|
children?: React.ReactNode;
|
|
31
|
-
|
|
32
|
-
inputSchema?: React.ReactNode;
|
|
33
|
-
title?: React.ReactNode;
|
|
37
|
+
title?: React.ReactNode | ((record: any) => React.ReactNode);
|
|
34
38
|
actions?: React.ReactNode;
|
|
39
|
+
description?: React.ReactNode | ((record: any) => React.ReactNode);
|
|
35
40
|
include?: string[];
|
|
36
41
|
exclude?: string[];
|
|
42
|
+
saveButtonLabel?: string;
|
|
37
43
|
}) => {
|
|
38
|
-
const
|
|
44
|
+
const { label, createTitle, createDescription } = useResourceSchema();
|
|
45
|
+
const translate = useTranslate();
|
|
46
|
+
const constructedTitle = useConstructedPageTitle('create', label);
|
|
47
|
+
|
|
48
|
+
const finalTitle = React.useMemo(() => {
|
|
49
|
+
if (typeof title === 'function') return title({});
|
|
50
|
+
if (React.isValidElement(title)) return title;
|
|
51
|
+
if (title) return translate(title as string);
|
|
52
|
+
if (React.isValidElement(createTitle)) return createTitle;
|
|
53
|
+
if (createTitle) return translate(createTitle as string);
|
|
54
|
+
return constructedTitle;
|
|
55
|
+
}, [title, createTitle, translate, constructedTitle]);
|
|
56
|
+
|
|
57
|
+
const finalDescription = React.useMemo(() => {
|
|
58
|
+
if (typeof description === 'function') return description({});
|
|
59
|
+
if (React.isValidElement(description)) return description;
|
|
60
|
+
if (description) return translate(description as string);
|
|
61
|
+
if (React.isValidElement(createDescription)) return createDescription;
|
|
62
|
+
if (createDescription) return translate(createDescription as string);
|
|
63
|
+
return undefined;
|
|
64
|
+
}, [description, createDescription, translate]);
|
|
65
|
+
|
|
66
|
+
const finalSaveButtonLabel = saveButtonLabel ? translate(saveButtonLabel) : translate('Create');
|
|
67
|
+
|
|
68
|
+
const finalChildren = children || <Form include={include} exclude={exclude} saveButtonLabel={finalSaveButtonLabel} />;
|
|
69
|
+
|
|
39
70
|
return (
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
</
|
|
71
|
+
<Container header={<CreateHeader title={finalTitle} description={finalDescription} actions={actions} />}>
|
|
72
|
+
{finalChildren}
|
|
73
|
+
</Container>
|
|
43
74
|
);
|
|
44
75
|
};
|
|
45
76
|
|
|
@@ -52,38 +83,38 @@ const CreateUI = ({
|
|
|
52
83
|
* <TextInput source="name" />
|
|
53
84
|
* </Form>
|
|
54
85
|
* </Create>
|
|
55
|
-
*
|
|
86
|
+
*
|
|
56
87
|
* @example
|
|
57
88
|
* // Using InputSchema from context
|
|
58
89
|
* <Create include={['name', 'price']} />
|
|
59
|
-
*
|
|
60
|
-
* @example
|
|
61
|
-
* // Passing a custom input schema
|
|
62
|
-
* <Create inputSchema={<InputSchema>...</InputSchema>}>
|
|
63
|
-
* <Form />
|
|
64
|
-
* </Create>
|
|
65
90
|
*/
|
|
66
91
|
export const Create = <RecordType extends RaRecord = RaRecord>({
|
|
67
92
|
children,
|
|
68
|
-
inputSchema,
|
|
69
93
|
title,
|
|
70
94
|
actions,
|
|
95
|
+
description,
|
|
71
96
|
include,
|
|
72
97
|
exclude,
|
|
98
|
+
redirect,
|
|
99
|
+
saveButtonLabel,
|
|
73
100
|
...props
|
|
74
101
|
}: CreateProps<RecordType>) => {
|
|
102
|
+
const resolve = useSettingValue();
|
|
103
|
+
const resolvedRedirect = redirect !== undefined ? redirect : resolve(undefined, 'createRedirect');
|
|
75
104
|
return (
|
|
76
|
-
<CreateBase {...props}>
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
<CreateBase redirect={resolvedRedirect} {...props}>
|
|
106
|
+
<ResourceSchemaProvider resource={props.resource}>
|
|
107
|
+
<CreateUI
|
|
108
|
+
title={title}
|
|
109
|
+
actions={actions}
|
|
110
|
+
description={description}
|
|
111
|
+
include={include}
|
|
112
|
+
exclude={exclude}
|
|
113
|
+
saveButtonLabel={saveButtonLabel}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
</CreateUI>
|
|
117
|
+
</ResourceSchemaProvider>
|
|
87
118
|
</CreateBase>
|
|
88
119
|
);
|
|
89
120
|
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import Header, { HeaderProps } from '@cloudscape-design/components/header';
|
|
3
|
-
import
|
|
4
|
-
import { useCreateContext, useTranslate } from '@strato-admin/core';
|
|
3
|
+
import { useCreateContext, useTranslate } from '@strato-admin/ra-core';
|
|
5
4
|
|
|
6
|
-
export interface CreateHeaderProps
|
|
5
|
+
export interface CreateHeaderProps
|
|
6
|
+
extends Pick<HeaderProps, 'variant' | 'counter' | 'actions' | 'description' | 'info' | 'headingTagOverride'> {
|
|
7
7
|
title?: React.ReactNode;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export const CreateHeader = ({ title, actions,
|
|
10
|
+
export const CreateHeader = ({ title, actions, description, counter, info, variant = 'h2', headingTagOverride }: CreateHeaderProps) => {
|
|
11
11
|
const translate = useTranslate();
|
|
12
12
|
const { defaultTitle } = useCreateContext();
|
|
13
13
|
|
|
@@ -18,14 +18,10 @@ export const CreateHeader = ({ title, actions, ...props }: CreateHeaderProps) =>
|
|
|
18
18
|
return defaultTitle;
|
|
19
19
|
}, [title, defaultTitle, translate]);
|
|
20
20
|
|
|
21
|
-
const headerActions = actions
|
|
22
|
-
<SpaceBetween direction="horizontal" size="xs">
|
|
23
|
-
{/* Add default create actions here if needed */}
|
|
24
|
-
</SpaceBetween>
|
|
25
|
-
);
|
|
21
|
+
const headerActions = actions ?? null;
|
|
26
22
|
|
|
27
23
|
return (
|
|
28
|
-
<Header variant=
|
|
24
|
+
<Header variant={variant} actions={headerActions} description={description} counter={counter} info={info} headingTagOverride={headingTagOverride}>
|
|
29
25
|
{headerTitle}
|
|
30
26
|
</Header>
|
|
31
27
|
);
|
package/src/defaults.tsx
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AdminSettings } from '@strato-admin/core';
|
|
2
|
+
import { Message } from './i18n/Message';
|
|
3
|
+
import { Table } from './list';
|
|
4
|
+
import { DetailHub } from './detail';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The framework's last-resort default values for all Admin-level configurable settings.
|
|
8
|
+
* Override any of these via the `settings` prop on <Admin>.
|
|
9
|
+
*/
|
|
10
|
+
export const FRAMEWORK_DEFAULTS: AdminSettings = {
|
|
11
|
+
listComponent: Table,
|
|
12
|
+
detailComponent: DetailHub,
|
|
13
|
+
editSuccessMessage: <Message>Element updated</Message>,
|
|
14
|
+
deleteSuccessMessage: <Message>Element deleted</Message>,
|
|
15
|
+
bulkDeleteSuccessMessage: (countDeleted: number) => (
|
|
16
|
+
<Message vars={{ countDeleted }}>
|
|
17
|
+
{'{countDeleted, plural, one {# element deleted} other {# elements deleted}}'}
|
|
18
|
+
</Message>
|
|
19
|
+
),
|
|
20
|
+
mutationMode: 'pessimistic',
|
|
21
|
+
createRedirect: 'list',
|
|
22
|
+
editRedirect: 'detail',
|
|
23
|
+
listPageSize: 25,
|
|
24
|
+
listPageSizes: [10, 25, 50, 100],
|
|
25
|
+
listPageSizeLabel: (pageSize: number) => (
|
|
26
|
+
<Message vars={{ pageSize }}>{'{pageSize, plural, one {# item} other {# items}}'}</Message>
|
|
27
|
+
),
|
|
28
|
+
};
|