@strato-admin/cloudscape 0.1.1 → 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 +2 -1
- package/dist/button/BulkDeleteButton.js +17 -11
- 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 +8 -5
- 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 +17 -4
- package/src/button/BulkDeleteButton.tsx +24 -29
- 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,11 +1,11 @@
|
|
|
1
|
-
|
|
2
1
|
import React from 'react';
|
|
3
|
-
import { ReferenceInputBase, type ReferenceInputBaseProps } from '@strato-admin/core';
|
|
4
|
-
import {
|
|
2
|
+
import { ReferenceInputBase, type ReferenceInputBaseProps, useInput } from '@strato-admin/ra-core';
|
|
3
|
+
import { FormField } from './FormField';
|
|
4
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
5
5
|
import { AutocompleteInput } from './AutocompleteInput';
|
|
6
6
|
|
|
7
7
|
export const ReferenceInput = (props: ReferenceInputBaseProps) => {
|
|
8
|
-
const { children, source: sourceProp, reference, ...rest } = props;
|
|
8
|
+
const { children, source: sourceProp, reference, isRequired, validate, defaultValue, label, ...rest } = props;
|
|
9
9
|
const context = useFormFieldContext();
|
|
10
10
|
|
|
11
11
|
// If we have a context, we use the source from it.
|
|
@@ -15,22 +15,40 @@ export const ReferenceInput = (props: ReferenceInputBaseProps) => {
|
|
|
15
15
|
throw new Error('ReferenceInput requires a source prop or a parent FormField Master');
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const inputState =
|
|
19
|
+
context ??
|
|
20
|
+
useInput({
|
|
21
|
+
source,
|
|
22
|
+
defaultValue,
|
|
23
|
+
validate,
|
|
24
|
+
isRequired,
|
|
25
|
+
...rest,
|
|
26
|
+
});
|
|
27
|
+
|
|
18
28
|
const finalChildren = children || <AutocompleteInput source={source} />;
|
|
19
29
|
|
|
20
30
|
const inner = (
|
|
21
|
-
<ReferenceInputBase source={source} reference={reference} {...rest}>
|
|
22
|
-
{
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
<ReferenceInputBase source={source} reference={reference} isRequired={isRequired} {...rest}>
|
|
32
|
+
<FormFieldContext.Provider value={inputState}>
|
|
33
|
+
{React.isValidElement(finalChildren)
|
|
34
|
+
? React.cloneElement(finalChildren as React.ReactElement<any>, {
|
|
35
|
+
source,
|
|
36
|
+
isRequired,
|
|
37
|
+
})
|
|
38
|
+
: (finalChildren as any)}
|
|
39
|
+
</FormFieldContext.Provider>
|
|
27
40
|
</ReferenceInputBase>
|
|
28
41
|
);
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
if (context) {
|
|
44
|
+
return inner;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<FormFieldContext.Provider value={inputState}>
|
|
49
|
+
<FormField {...(props as any)}>{inner}</FormField>
|
|
50
|
+
</FormFieldContext.Provider>
|
|
51
|
+
);
|
|
34
52
|
};
|
|
35
53
|
|
|
36
54
|
export default ReferenceInput;
|
|
@@ -1,29 +1,23 @@
|
|
|
1
|
-
|
|
2
1
|
import React from 'react';
|
|
3
|
-
import {
|
|
4
|
-
useInput,
|
|
5
|
-
useResourceContext,
|
|
6
|
-
useChoicesContext,
|
|
7
|
-
useGetRecordRepresentation,
|
|
8
|
-
} from '@strato-admin/core';
|
|
2
|
+
import { useInput, useResourceContext, useChoicesContext, useGetRecordRepresentation } from '@strato-admin/ra-core';
|
|
9
3
|
import CloudscapeSelect, { SelectProps as CloudscapeSelectProps } from '@cloudscape-design/components/select';
|
|
10
4
|
import { FormField } from './FormField';
|
|
11
5
|
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
12
6
|
import { InputProps } from './types';
|
|
13
7
|
|
|
14
8
|
export interface SelectInputProps
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
9
|
+
extends InputProps,
|
|
10
|
+
Pick<CloudscapeSelectProps, 'filteringType' | 'placeholder' | 'disabled' | 'readOnly'> {
|
|
11
|
+
choices?: Array<{ id: string | number; [key: string]: any }>;
|
|
12
|
+
/**
|
|
13
|
+
* The text to display for the empty option when isRequired is false.
|
|
14
|
+
* @default "-"
|
|
15
|
+
*/
|
|
16
|
+
emptyText?: string;
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
export const SelectInput = (props: SelectInputProps) => {
|
|
26
|
-
|
|
20
|
+
const { label, source, defaultValue, validate, choices: choicesProp, emptyText = '-', filteringType, placeholder, disabled, readOnly, ...rest } = props;
|
|
27
21
|
const resource = useResourceContext();
|
|
28
22
|
const { allChoices, isPending } = useChoicesContext(props);
|
|
29
23
|
const getRecordRepresentation = useGetRecordRepresentation(resource);
|
|
@@ -63,8 +57,11 @@ export const SelectInput = (props: SelectInputProps) => {
|
|
|
63
57
|
|
|
64
58
|
const inner = (
|
|
65
59
|
<CloudscapeSelect
|
|
66
|
-
{...rest}
|
|
67
60
|
id={id}
|
|
61
|
+
filteringType={filteringType}
|
|
62
|
+
placeholder={placeholder}
|
|
63
|
+
disabled={disabled}
|
|
64
|
+
readOnly={readOnly}
|
|
68
65
|
options={options}
|
|
69
66
|
selectedOption={selectedOption}
|
|
70
67
|
statusType={isPending ? 'loading' : 'finished'}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render, fireEvent, cleanup } from '@testing-library/react';
|
|
3
3
|
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
|
-
import { useInput, useResourceContext } from '@strato-admin/core';
|
|
4
|
+
import { useInput, useResourceContext } from '@strato-admin/ra-core';
|
|
5
5
|
import { SliderInput } from './SliderInput';
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
vi.mock('@strato-admin/core', () => ({
|
|
7
|
+
vi.mock('@strato-admin/ra-core', () => ({
|
|
9
8
|
useInput: vi.fn(),
|
|
10
9
|
useResourceContext: vi.fn(),
|
|
11
10
|
useTranslate: () => (key: string) => key,
|
|
@@ -1,49 +1,59 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { useInput } from '@strato-admin/core';
|
|
1
|
+
import { useInput } from '@strato-admin/ra-core';
|
|
3
2
|
import CloudscapeSlider, { SliderProps as CloudscapeSliderProps } from '@cloudscape-design/components/slider';
|
|
4
3
|
import { FormField } from './FormField';
|
|
5
4
|
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
5
|
import { InputProps } from './types';
|
|
7
6
|
|
|
8
7
|
export interface SliderInputProps
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
extends InputProps,
|
|
9
|
+
Partial<Pick<CloudscapeSliderProps, 'min' | 'max'>>,
|
|
10
|
+
Pick<CloudscapeSliderProps, 'step' | 'disabled' | 'readOnly' | 'valueFormatter' | 'tickMarks' | 'referenceValues' | 'hideFillLine' | 'ariaLabel' | 'ariaDescription'> {}
|
|
11
11
|
|
|
12
12
|
export const SliderInput = (props: SliderInputProps) => {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
13
|
+
const { label, source, defaultValue, validate, min, max, step, disabled, readOnly, valueFormatter, tickMarks, referenceValues, hideFillLine, ariaLabel, ariaDescription, ...rest } = props;
|
|
14
|
+
const context = useFormFieldContext();
|
|
15
|
+
const inputState =
|
|
16
|
+
context ??
|
|
17
|
+
useInput({
|
|
18
|
+
source,
|
|
19
|
+
defaultValue,
|
|
20
|
+
validate,
|
|
21
|
+
...rest,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const { id, field } = inputState;
|
|
25
|
+
|
|
26
|
+
// Cloudscape Slider requires a number value
|
|
27
|
+
const value = typeof field.value === 'number' ? field.value : (min ?? 0);
|
|
28
|
+
|
|
29
|
+
const inner = (
|
|
30
|
+
<CloudscapeSlider
|
|
31
|
+
id={id}
|
|
32
|
+
min={min ?? 0}
|
|
33
|
+
max={max ?? 100}
|
|
34
|
+
step={step}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
readOnly={readOnly}
|
|
37
|
+
valueFormatter={valueFormatter}
|
|
38
|
+
tickMarks={tickMarks}
|
|
39
|
+
referenceValues={referenceValues}
|
|
40
|
+
hideFillLine={hideFillLine}
|
|
41
|
+
ariaLabel={ariaLabel}
|
|
42
|
+
ariaDescription={ariaDescription}
|
|
43
|
+
value={value}
|
|
44
|
+
onChange={(event) => field.onChange(event.detail.value)}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (context) {
|
|
49
|
+
return inner;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<FormFieldContext.Provider value={inputState}>
|
|
54
|
+
<FormField {...props}>{inner}</FormField>
|
|
55
|
+
</FormFieldContext.Provider>
|
|
56
|
+
);
|
|
47
57
|
};
|
|
48
58
|
|
|
49
59
|
export default SliderInput;
|
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
|
|
2
|
-
import { useInput } from '@strato-admin/core';
|
|
1
|
+
import { useInput } from '@strato-admin/ra-core';
|
|
3
2
|
import CloudscapeTextarea, { TextareaProps as CloudscapeTextareaProps } from '@cloudscape-design/components/textarea';
|
|
4
3
|
import { FormField } from './FormField';
|
|
5
4
|
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
5
|
import { InputProps } from './types';
|
|
7
6
|
|
|
8
7
|
export interface TextAreaInputProps
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
extends InputProps,
|
|
9
|
+
Pick<CloudscapeTextareaProps, 'placeholder' | 'disabled' | 'readOnly' | 'rows' | 'autoFocus' | 'spellcheck'> {}
|
|
11
10
|
|
|
12
11
|
export const TextAreaInput = (props: TextAreaInputProps) => {
|
|
13
|
-
|
|
12
|
+
const { label, source, defaultValue, validate, placeholder, disabled, readOnly, rows, autoFocus, spellcheck, ...rest } = props;
|
|
14
13
|
const context = useFormFieldContext();
|
|
15
14
|
const inputState =
|
|
16
15
|
context ??
|
|
@@ -25,9 +24,14 @@ export const TextAreaInput = (props: TextAreaInputProps) => {
|
|
|
25
24
|
|
|
26
25
|
const inner = (
|
|
27
26
|
<CloudscapeTextarea
|
|
28
|
-
{...rest}
|
|
29
27
|
{...field}
|
|
30
28
|
id={id}
|
|
29
|
+
placeholder={placeholder}
|
|
30
|
+
disabled={disabled}
|
|
31
|
+
readOnly={readOnly}
|
|
32
|
+
rows={rows}
|
|
33
|
+
autoFocus={autoFocus}
|
|
34
|
+
spellcheck={spellcheck}
|
|
31
35
|
value={field.value || ''}
|
|
32
36
|
onChange={(event) => field.onChange(event.detail.value)}
|
|
33
37
|
onBlur={() => field.onBlur()}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
|
|
2
1
|
import { render } from '@testing-library/react';
|
|
3
2
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
-
import { useInput, useResourceContext } from '@strato-admin/core';
|
|
3
|
+
import { useInput, useResourceContext } from '@strato-admin/ra-core';
|
|
5
4
|
import { TextInput } from './TextInput';
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
vi.mock('@strato-admin/core', () => ({
|
|
6
|
+
vi.mock('@strato-admin/ra-core', () => ({
|
|
9
7
|
useInput: vi.fn(),
|
|
10
8
|
useResourceContext: vi.fn(),
|
|
11
9
|
useTranslate: () => (key: string) => key,
|
package/src/input/TextInput.tsx
CHANGED
|
@@ -1,36 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
import { useInput } from '@strato-admin/core';
|
|
1
|
+
import { useInput } from '@strato-admin/ra-core';
|
|
3
2
|
import CloudscapeInput, { InputProps as CloudscapeInputProps } from '@cloudscape-design/components/input';
|
|
4
3
|
import { FormField } from './FormField';
|
|
5
4
|
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
5
|
import { InputProps } from './types';
|
|
7
6
|
|
|
8
7
|
export interface TextInputProps
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
extends InputProps,
|
|
9
|
+
Pick<CloudscapeInputProps, 'placeholder' | 'disabled' | 'readOnly' | 'autoFocus' | 'autoComplete' | 'spellcheck' | 'inputMode'> {
|
|
10
|
+
type?: CloudscapeInputProps['type'];
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export const TextInput = (props: TextInputProps) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
14
|
+
const {
|
|
15
|
+
label,
|
|
16
|
+
source,
|
|
17
|
+
defaultValue,
|
|
18
|
+
validate,
|
|
19
|
+
description,
|
|
20
|
+
constraintText,
|
|
21
|
+
info,
|
|
22
|
+
secondaryControl,
|
|
23
|
+
type = 'text',
|
|
24
|
+
placeholder,
|
|
25
|
+
disabled,
|
|
26
|
+
readOnly,
|
|
27
|
+
autoFocus,
|
|
28
|
+
autoComplete,
|
|
29
|
+
spellcheck,
|
|
30
|
+
inputMode,
|
|
27
31
|
|
|
32
|
+
} = props;
|
|
33
|
+
const context = useFormFieldContext();
|
|
34
|
+
const inputState = context ?? useInput({ source, defaultValue, validate });
|
|
35
|
+
const { field } = inputState;
|
|
28
36
|
const inner = (
|
|
29
37
|
<CloudscapeInput
|
|
30
|
-
{...rest}
|
|
31
38
|
{...field}
|
|
32
|
-
id={id}
|
|
33
39
|
type={type}
|
|
40
|
+
placeholder={placeholder}
|
|
41
|
+
disabled={disabled}
|
|
42
|
+
readOnly={readOnly}
|
|
43
|
+
autoFocus={autoFocus}
|
|
44
|
+
autoComplete={autoComplete}
|
|
45
|
+
spellcheck={spellcheck}
|
|
46
|
+
inputMode={inputMode}
|
|
34
47
|
value={field.value || ''}
|
|
35
48
|
onChange={(event) => field.onChange(event.detail.value)}
|
|
36
49
|
onBlur={() => field.onBlur()}
|
|
@@ -43,7 +56,9 @@ export const TextInput = (props: TextInputProps) => {
|
|
|
43
56
|
|
|
44
57
|
return (
|
|
45
58
|
<FormFieldContext.Provider value={inputState}>
|
|
46
|
-
<FormField {
|
|
59
|
+
<FormField source={source} label={label} description={description} constraintText={constraintText} info={info} secondaryControl={secondaryControl}>
|
|
60
|
+
{inner}
|
|
61
|
+
</FormField>
|
|
47
62
|
</FormFieldContext.Provider>
|
|
48
63
|
);
|
|
49
64
|
};
|
package/src/input/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from './types';
|
|
2
|
+
export * from './BooleanInput';
|
|
2
3
|
export * from './TextInput';
|
|
3
4
|
export * from './TextAreaInput';
|
|
4
5
|
export * from './NumberInput';
|
|
5
|
-
export * from './
|
|
6
|
+
export * from './ArrayInput';
|
|
6
7
|
export * from './SelectInput';
|
|
7
8
|
export * from './AutocompleteInput';
|
|
8
9
|
export * from './ReferenceInput';
|
package/src/input/types.ts
CHANGED
|
@@ -1,14 +1,46 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { InputProps as InputPropsBase } from '@strato-admin/core';
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { InputProps as InputPropsBase, Validator } from '@strato-admin/ra-core';
|
|
3
3
|
import { FormFieldProps as CloudscapeFormFieldProps } from '@cloudscape-design/components/form-field';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Common props shared by all Strato Admin input components.
|
|
7
|
+
*/
|
|
8
|
+
export interface StratoCommonInputProps<T = any> {
|
|
9
|
+
/** The field name in the record. Used to read and write the value. */
|
|
10
|
+
source: string;
|
|
11
|
+
/** Override the auto-generated label. Pass `false` to hide the label entirely. */
|
|
11
12
|
label?: string | false;
|
|
13
|
+
/** The initial value when no record value exists. */
|
|
14
|
+
defaultValue?: any;
|
|
15
|
+
/** Validation rule or array of rules. Use built-in validators (`required()`, `minLength()`, etc.) or provide a custom function. */
|
|
16
|
+
validate?: Validator | Validator[];
|
|
17
|
+
/** Transforms the record value before passing it to the input (record → input). */
|
|
18
|
+
format?: (value: T) => any;
|
|
19
|
+
/** Transforms the input value before saving to the record (input → record). */
|
|
20
|
+
parse?: (value: any) => T;
|
|
21
|
+
/** When `true`, the value is visible but cannot be changed. */
|
|
22
|
+
readOnly?: boolean;
|
|
23
|
+
/** When `true`, the input is non-interactive and grayed out. */
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
/** Helper text displayed below the label. */
|
|
26
|
+
description?: React.ReactNode;
|
|
27
|
+
/** Additional constraint text. Appended with "(optional)" when the field is not required. */
|
|
28
|
+
constraintText?: React.ReactNode;
|
|
29
|
+
/** Info link displayed next to the label (Cloudscape `info` slot). */
|
|
30
|
+
info?: React.ReactNode;
|
|
31
|
+
/** Secondary control displayed to the right of the input (Cloudscape `secondaryControl` slot). */
|
|
32
|
+
secondaryControl?: React.ReactNode;
|
|
33
|
+
/** When `true`, the form field stretches to fill its container width. */
|
|
34
|
+
stretch?: boolean;
|
|
12
35
|
}
|
|
13
36
|
|
|
37
|
+
export interface StratoInputProps<T = any>
|
|
38
|
+
extends
|
|
39
|
+
StratoCommonInputProps<T>,
|
|
40
|
+
Omit<
|
|
41
|
+
InputPropsBase<T>,
|
|
42
|
+
'label' | 'source' | 'defaultValue' | 'validate' | 'format' | 'parse' | 'readOnly' | 'disabled'
|
|
43
|
+
>,
|
|
44
|
+
Pick<CloudscapeFormFieldProps, 'i18nStrings'> {}
|
|
45
|
+
|
|
14
46
|
export type InputProps<T = any> = StratoInputProps<T>;
|
|
@@ -2,10 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
4
|
import { AppLayout } from './AppLayout';
|
|
5
|
-
import { useDefaultTitle, useResourceDefinitions } from '@strato-admin/core';
|
|
5
|
+
import { useDefaultTitle, useResourceDefinitions } from '@strato-admin/ra-core';
|
|
6
6
|
import { TopNavigation } from './TopNavigation';
|
|
7
|
+
import SideNavigationMock from '@cloudscape-design/components/side-navigation';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
9
10
|
vi.mock('@strato-admin/core', () => import('../__mocks__/strato-core'));
|
|
10
11
|
|
|
11
12
|
// Mock global-styles
|
|
@@ -35,7 +36,7 @@ vi.mock('@cloudscape-design/components/app-layout', () => ({
|
|
|
35
36
|
}));
|
|
36
37
|
|
|
37
38
|
vi.mock('@cloudscape-design/components/side-navigation', () => ({
|
|
38
|
-
default: () => <div data-testid="side-navigation"
|
|
39
|
+
default: vi.fn(() => <div data-testid="side-navigation" />),
|
|
39
40
|
}));
|
|
40
41
|
|
|
41
42
|
describe('AppLayout', () => {
|
|
@@ -43,6 +44,25 @@ describe('AppLayout', () => {
|
|
|
43
44
|
vi.clearAllMocks();
|
|
44
45
|
});
|
|
45
46
|
|
|
47
|
+
it('should only show resources with hasList: true in SideNavigation', () => {
|
|
48
|
+
(useResourceDefinitions as any).mockReturnValue({
|
|
49
|
+
posts: { name: 'posts', hasList: true },
|
|
50
|
+
comments: { name: 'comments', hasList: false },
|
|
51
|
+
users: { name: 'users', hasList: true },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
render(
|
|
55
|
+
<AppLayout>
|
|
56
|
+
<div>Content</div>
|
|
57
|
+
</AppLayout>,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const sideNavProps = vi.mocked(SideNavigationMock).mock.calls[0][0];
|
|
61
|
+
expect(sideNavProps.items).toHaveLength(2);
|
|
62
|
+
expect(sideNavProps.items[0]).toMatchObject({ text: 'posts', href: '/posts' });
|
|
63
|
+
expect(sideNavProps.items[1]).toMatchObject({ text: 'users', href: '/users' });
|
|
64
|
+
});
|
|
65
|
+
|
|
46
66
|
it('should use title from useDefaultTitle hook if no title prop provided', () => {
|
|
47
67
|
(useDefaultTitle as any).mockReturnValue('Hook Title');
|
|
48
68
|
|
package/src/layout/AppLayout.tsx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
2
|
import CloudscapeAppLayout from '@cloudscape-design/components/app-layout';
|
|
3
3
|
import SideNavigation from '@cloudscape-design/components/side-navigation';
|
|
4
|
-
import { useResourceDefinitions, useDefaultTitle, useGetResourceLabel } from '@strato-admin/core';
|
|
4
|
+
import { useResourceDefinitions, useDefaultTitle, useGetResourceLabel } from '@strato-admin/ra-core';
|
|
5
5
|
import { useNavigate } from 'react-router-dom';
|
|
6
6
|
import { TopNavigation } from './TopNavigation';
|
|
7
|
+
import { Notifications } from './Notifications';
|
|
7
8
|
import ThemeManager from '../theme/ThemeManager';
|
|
8
9
|
|
|
9
10
|
export interface AppLayoutProps {
|
|
@@ -19,14 +20,15 @@ export const AppLayout = ({ children, header, title }: AppLayoutProps) => {
|
|
|
19
20
|
const defaultTitle = useDefaultTitle();
|
|
20
21
|
const [navigationOpen, setNavigationOpen] = useState(true);
|
|
21
22
|
|
|
22
|
-
const appTitle =
|
|
23
|
-
title ?? (typeof defaultTitle === 'string' ? defaultTitle : '');
|
|
23
|
+
const appTitle = title ?? (typeof defaultTitle === 'string' ? defaultTitle : '');
|
|
24
24
|
|
|
25
|
-
const items = Object.values(resources)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
const items = Object.values(resources)
|
|
26
|
+
.filter((resource) => resource.hasList)
|
|
27
|
+
.map((resource) => ({
|
|
28
|
+
type: 'link' as const,
|
|
29
|
+
text: getResourceLabel(resource.name),
|
|
30
|
+
href: `/${resource.name}`,
|
|
31
|
+
}));
|
|
30
32
|
|
|
31
33
|
return (
|
|
32
34
|
<>
|
|
@@ -51,6 +53,7 @@ export const AppLayout = ({ children, header, title }: AppLayoutProps) => {
|
|
|
51
53
|
}}
|
|
52
54
|
/>
|
|
53
55
|
}
|
|
56
|
+
notifications={<Notifications />}
|
|
54
57
|
content={<div>{children}</div>}
|
|
55
58
|
/>
|
|
56
59
|
</>
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { Notifications } from './Notifications';
|
|
5
|
+
import { useNotificationContext, useTranslate } from '@strato-admin/ra-core';
|
|
6
|
+
|
|
7
|
+
vi.mock('@strato-admin/ra-core', () => import('../__mocks__/ra-core'));
|
|
8
|
+
|
|
9
|
+
vi.mock('@cloudscape-design/components/flashbar', () => ({
|
|
10
|
+
default: ({ items }: any) => (
|
|
11
|
+
<div data-testid="flashbar">
|
|
12
|
+
{items.map((item: any) => (
|
|
13
|
+
<div key={item.id} data-testid={`flash-item-${item.id}`} data-type={item.type}>
|
|
14
|
+
<span data-testid="flash-content">{item.content}</span>
|
|
15
|
+
{item.dismissible && (
|
|
16
|
+
<button data-testid={`dismiss-${item.id}`} onClick={item.onDismiss}>
|
|
17
|
+
Dismiss
|
|
18
|
+
</button>
|
|
19
|
+
)}
|
|
20
|
+
</div>
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
describe('Notifications', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('renders nothing when there are no notifications', () => {
|
|
32
|
+
(useNotificationContext as any).mockReturnValue({ notifications: [], setNotifications: vi.fn() });
|
|
33
|
+
const { container } = render(<Notifications />);
|
|
34
|
+
expect(container.firstChild).toBeNull();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('renders a Flashbar item for each notification', () => {
|
|
38
|
+
const n1 = { message: 'Saved', type: 'success' as const };
|
|
39
|
+
const n2 = { message: 'Error occurred', type: 'error' as const };
|
|
40
|
+
(useNotificationContext as any).mockReturnValue({
|
|
41
|
+
notifications: [n1, n2],
|
|
42
|
+
setNotifications: vi.fn(),
|
|
43
|
+
});
|
|
44
|
+
render(<Notifications />);
|
|
45
|
+
expect(screen.getByTestId('flashbar')).toBeDefined();
|
|
46
|
+
expect(screen.getAllByTestId('flash-content')).toHaveLength(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('passes the correct type to each Flashbar item', () => {
|
|
50
|
+
const types = ['success', 'info', 'warning', 'error'] as const;
|
|
51
|
+
const notifications = types.map((type) => ({ message: type, type }));
|
|
52
|
+
(useNotificationContext as any).mockReturnValue({ notifications, setNotifications: vi.fn() });
|
|
53
|
+
render(<Notifications />);
|
|
54
|
+
const items = screen.getAllByTestId(/^flash-item-/);
|
|
55
|
+
expect(items.map((el) => el.getAttribute('data-type'))).toEqual(types);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('translates string messages', () => {
|
|
59
|
+
(useTranslate as any).mockReturnValue((key: string) => `translated:${key}`);
|
|
60
|
+
(useNotificationContext as any).mockReturnValue({
|
|
61
|
+
notifications: [{ message: 'ra.action.save', type: 'success' as const }],
|
|
62
|
+
setNotifications: vi.fn(),
|
|
63
|
+
});
|
|
64
|
+
render(<Notifications />);
|
|
65
|
+
expect(screen.getByTestId('flash-content').textContent).toBe('translated:ra.action.save');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('calls setNotifications to remove the dismissed notification', () => {
|
|
69
|
+
const notification = { message: 'Done', type: 'success' as const };
|
|
70
|
+
const setNotifications = vi.fn();
|
|
71
|
+
(useNotificationContext as any).mockReturnValue({
|
|
72
|
+
notifications: [notification],
|
|
73
|
+
setNotifications,
|
|
74
|
+
});
|
|
75
|
+
render(<Notifications />);
|
|
76
|
+
const dismissButtons = screen.getAllByTestId(/^dismiss-/);
|
|
77
|
+
fireEvent.click(dismissButtons[0]);
|
|
78
|
+
expect(setNotifications).toHaveBeenCalled();
|
|
79
|
+
const updater = setNotifications.mock.calls[0][0];
|
|
80
|
+
const result = updater([notification, { message: 'Other', type: 'info' as const }]);
|
|
81
|
+
expect(result).toHaveLength(1);
|
|
82
|
+
expect(result[0].message).toBe('Other');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('does not auto-hide when autoHideDuration is null', () => {
|
|
86
|
+
vi.useFakeTimers();
|
|
87
|
+
const notification = {
|
|
88
|
+
message: 'Persistent',
|
|
89
|
+
type: 'info' as const,
|
|
90
|
+
notificationOptions: { autoHideDuration: null },
|
|
91
|
+
};
|
|
92
|
+
const setNotifications = vi.fn();
|
|
93
|
+
(useNotificationContext as any).mockReturnValue({
|
|
94
|
+
notifications: [notification],
|
|
95
|
+
setNotifications,
|
|
96
|
+
});
|
|
97
|
+
render(<Notifications />);
|
|
98
|
+
vi.advanceTimersByTime(10000);
|
|
99
|
+
expect(setNotifications).not.toHaveBeenCalled();
|
|
100
|
+
vi.useRealTimers();
|
|
101
|
+
});
|
|
102
|
+
});
|