@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,87 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useInput, useResourceContext, ValidationError } from '@strato-admin/core';
|
|
4
|
+
import CloudscapeFormField from '@cloudscape-design/components/form-field';
|
|
5
|
+
import { FieldTitle } from './FieldTitle';
|
|
6
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
7
|
+
import { InputProps } from './types';
|
|
8
|
+
|
|
9
|
+
export interface FormFieldProps extends InputProps {
|
|
10
|
+
children: React.ReactNode;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const FormField = (props: FormFieldProps) => {
|
|
14
|
+
const {
|
|
15
|
+
children,
|
|
16
|
+
label,
|
|
17
|
+
source,
|
|
18
|
+
defaultValue,
|
|
19
|
+
validate,
|
|
20
|
+
description,
|
|
21
|
+
constraintText,
|
|
22
|
+
info,
|
|
23
|
+
secondaryControl,
|
|
24
|
+
stretch,
|
|
25
|
+
i18nStrings,
|
|
26
|
+
...rest
|
|
27
|
+
} = props;
|
|
28
|
+
const resource = useResourceContext();
|
|
29
|
+
const context = useFormFieldContext();
|
|
30
|
+
const inputState =
|
|
31
|
+
context ??
|
|
32
|
+
useInput({
|
|
33
|
+
source,
|
|
34
|
+
defaultValue,
|
|
35
|
+
validate,
|
|
36
|
+
...rest,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const contextValue = React.useMemo(() => {
|
|
40
|
+
if (!inputState) return undefined;
|
|
41
|
+
return {
|
|
42
|
+
...inputState,
|
|
43
|
+
source: source || context?.source || '',
|
|
44
|
+
};
|
|
45
|
+
}, [inputState, source, context?.source]);
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
id,
|
|
49
|
+
fieldState: { isTouched, invalid, error },
|
|
50
|
+
formState: { isSubmitted },
|
|
51
|
+
isRequired,
|
|
52
|
+
} = inputState;
|
|
53
|
+
|
|
54
|
+
const errorToProcess = (error as any)?.message || (error as any)?.root?.message;
|
|
55
|
+
const errorText =
|
|
56
|
+
(isTouched || isSubmitted) && invalid && typeof errorToProcess === 'string' ? (
|
|
57
|
+
<ValidationError error={errorToProcess} />
|
|
58
|
+
) : undefined;
|
|
59
|
+
|
|
60
|
+
const content = (
|
|
61
|
+
<CloudscapeFormField
|
|
62
|
+
id={id}
|
|
63
|
+
label={
|
|
64
|
+
label === false ? undefined : (
|
|
65
|
+
<FieldTitle label={label} source={source} resource={resource} isRequired={isRequired} />
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
description={description}
|
|
69
|
+
constraintText={constraintText}
|
|
70
|
+
info={info}
|
|
71
|
+
secondaryControl={secondaryControl}
|
|
72
|
+
stretch={stretch}
|
|
73
|
+
i18nStrings={i18nStrings}
|
|
74
|
+
errorText={errorText}
|
|
75
|
+
>
|
|
76
|
+
{children}
|
|
77
|
+
</CloudscapeFormField>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (context) {
|
|
81
|
+
return content;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return <FormFieldContext.Provider value={contextValue}>{content}</FormFieldContext.Provider>;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export default FormField;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import { UseInputValue } from '@strato-admin/core';
|
|
3
|
+
|
|
4
|
+
export interface FormFieldContextValue extends UseInputValue {
|
|
5
|
+
source?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const FormFieldContext = createContext<FormFieldContextValue | undefined>(undefined);
|
|
9
|
+
export const useFormFieldContext = () => useContext(FormFieldContext);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
|
|
2
|
+
import { useInput } from '@strato-admin/core';
|
|
3
|
+
import CloudscapeInput, { InputProps as CloudscapeInputProps } from '@cloudscape-design/components/input';
|
|
4
|
+
import { FormField } from './FormField';
|
|
5
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
|
+
import { InputProps } from './types';
|
|
7
|
+
|
|
8
|
+
export interface NumberInputProps
|
|
9
|
+
extends Omit<CloudscapeInputProps, 'onChange' | 'value' | 'onBlur' | 'type'>,
|
|
10
|
+
InputProps {
|
|
11
|
+
type?: CloudscapeInputProps['type'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NumberInput = (props: NumberInputProps) => {
|
|
15
|
+
const { label, source, defaultValue, validate, type = 'number', ...rest } = props;
|
|
16
|
+
const context = useFormFieldContext();
|
|
17
|
+
const inputState =
|
|
18
|
+
context ??
|
|
19
|
+
useInput({
|
|
20
|
+
source,
|
|
21
|
+
defaultValue,
|
|
22
|
+
validate,
|
|
23
|
+
...rest,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const { id, field } = inputState;
|
|
27
|
+
|
|
28
|
+
const handleChange = (value: string) => {
|
|
29
|
+
const floatValue = parseFloat(value);
|
|
30
|
+
field.onChange(isNaN(floatValue) ? null : floatValue);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const inner = (
|
|
34
|
+
<CloudscapeInput
|
|
35
|
+
{...rest}
|
|
36
|
+
{...field}
|
|
37
|
+
id={id}
|
|
38
|
+
type={type}
|
|
39
|
+
value={field.value?.toString() || ''}
|
|
40
|
+
onChange={(event) => handleChange(event.detail.value)}
|
|
41
|
+
onBlur={() => field.onBlur()}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (context) {
|
|
46
|
+
return inner;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<FormFieldContext.Provider value={inputState}>
|
|
51
|
+
<FormField {...props}>{inner}</FormField>
|
|
52
|
+
</FormFieldContext.Provider>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default NumberInput;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect } from 'vitest';
|
|
4
|
+
import { ReferenceInput } from './ReferenceInput';
|
|
5
|
+
|
|
6
|
+
// Mock ra-core
|
|
7
|
+
vi.mock('@strato-admin/core', () => ({
|
|
8
|
+
ReferenceInputBase: ({ children }: any) => <div data-testid="reference-input-base">{children}</div>,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
describe('ReferenceInput', () => {
|
|
12
|
+
it('should propagate source prop to children', () => {
|
|
13
|
+
const Child = ({ source }: any) => <div data-testid="child">{source}</div>;
|
|
14
|
+
|
|
15
|
+
const { getByTestId } = render(
|
|
16
|
+
<ReferenceInput reference="products" source="productId">
|
|
17
|
+
<Child />
|
|
18
|
+
</ReferenceInput>,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(getByTestId('child').textContent).toBe('productId');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should overwrite child source prop with its own source prop', () => {
|
|
25
|
+
const Child = ({ source }: any) => <div data-testid="child">{source}</div>;
|
|
26
|
+
|
|
27
|
+
const { getByTestId } = render(
|
|
28
|
+
<ReferenceInput reference="products" source="prefixed.productId">
|
|
29
|
+
<Child source="productId" />
|
|
30
|
+
</ReferenceInput>,
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
expect(getByTestId('child').textContent).toBe('prefixed.productId');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { ReferenceInputBase, type ReferenceInputBaseProps } from '@strato-admin/core';
|
|
4
|
+
import { useFormFieldContext } from './FormFieldContext';
|
|
5
|
+
import { AutocompleteInput } from './AutocompleteInput';
|
|
6
|
+
|
|
7
|
+
export const ReferenceInput = (props: ReferenceInputBaseProps) => {
|
|
8
|
+
const { children, source: sourceProp, reference, ...rest } = props;
|
|
9
|
+
const context = useFormFieldContext();
|
|
10
|
+
|
|
11
|
+
// If we have a context, we use the source from it.
|
|
12
|
+
const source = sourceProp || context?.source;
|
|
13
|
+
|
|
14
|
+
if (!source) {
|
|
15
|
+
throw new Error('ReferenceInput requires a source prop or a parent FormField Master');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const finalChildren = children || <AutocompleteInput source={source} />;
|
|
19
|
+
|
|
20
|
+
const inner = (
|
|
21
|
+
<ReferenceInputBase source={source} reference={reference} {...rest}>
|
|
22
|
+
{React.isValidElement(finalChildren)
|
|
23
|
+
? React.cloneElement(finalChildren as React.ReactElement<any>, {
|
|
24
|
+
source,
|
|
25
|
+
})
|
|
26
|
+
: (finalChildren as any)}
|
|
27
|
+
</ReferenceInputBase>
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// ReferenceInput is unique because it's a wrapper.
|
|
31
|
+
// It doesn't use FormFieldContext for its state directly (ReferenceInputBase does),
|
|
32
|
+
// but it needs to ensure its children can consume the state it provides via ReferenceInputBase.
|
|
33
|
+
return inner;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default ReferenceInput;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import {
|
|
4
|
+
useInput,
|
|
5
|
+
useResourceContext,
|
|
6
|
+
useChoicesContext,
|
|
7
|
+
useGetRecordRepresentation,
|
|
8
|
+
} from '@strato-admin/core';
|
|
9
|
+
import CloudscapeSelect, { SelectProps as CloudscapeSelectProps } from '@cloudscape-design/components/select';
|
|
10
|
+
import { FormField } from './FormField';
|
|
11
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
12
|
+
import { InputProps } from './types';
|
|
13
|
+
|
|
14
|
+
export interface SelectInputProps
|
|
15
|
+
extends Omit<CloudscapeSelectProps, 'onChange' | 'selectedOption' | 'options' | 'onBlur'>,
|
|
16
|
+
InputProps {
|
|
17
|
+
choices?: Array<{ id: string | number; [key: string]: any }>;
|
|
18
|
+
/**
|
|
19
|
+
* The text to display for the empty option when isRequired is false.
|
|
20
|
+
* @default "-"
|
|
21
|
+
*/
|
|
22
|
+
emptyText?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SelectInput = (props: SelectInputProps) => {
|
|
26
|
+
const { label, source, defaultValue, validate, choices: choicesProp, emptyText = '-', ...rest } = props;
|
|
27
|
+
const resource = useResourceContext();
|
|
28
|
+
const { allChoices, isPending } = useChoicesContext(props);
|
|
29
|
+
const getRecordRepresentation = useGetRecordRepresentation(resource);
|
|
30
|
+
const context = useFormFieldContext();
|
|
31
|
+
const inputState =
|
|
32
|
+
context ??
|
|
33
|
+
useInput({
|
|
34
|
+
source,
|
|
35
|
+
defaultValue,
|
|
36
|
+
validate,
|
|
37
|
+
...rest,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { id, field, isRequired } = inputState;
|
|
41
|
+
|
|
42
|
+
const choices = choicesProp || allChoices || [];
|
|
43
|
+
|
|
44
|
+
const options = React.useMemo(() => {
|
|
45
|
+
const opts = choices.map((choice) => ({
|
|
46
|
+
label: String(getRecordRepresentation(choice)),
|
|
47
|
+
value: String(choice.id),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
if (!isRequired) {
|
|
51
|
+
opts.unshift({ label: emptyText, value: '__EMPTY__' });
|
|
52
|
+
}
|
|
53
|
+
return opts;
|
|
54
|
+
}, [choices, getRecordRepresentation, isRequired, emptyText]);
|
|
55
|
+
|
|
56
|
+
const selectedOption =
|
|
57
|
+
options.find((option) => {
|
|
58
|
+
if (!field.value) {
|
|
59
|
+
return option.value === '__EMPTY__';
|
|
60
|
+
}
|
|
61
|
+
return option.value === String(field.value);
|
|
62
|
+
}) || null;
|
|
63
|
+
|
|
64
|
+
const inner = (
|
|
65
|
+
<CloudscapeSelect
|
|
66
|
+
{...rest}
|
|
67
|
+
id={id}
|
|
68
|
+
options={options}
|
|
69
|
+
selectedOption={selectedOption}
|
|
70
|
+
statusType={isPending ? 'loading' : 'finished'}
|
|
71
|
+
expandToViewport={true}
|
|
72
|
+
onChange={({ detail }) => {
|
|
73
|
+
const value = detail.selectedOption.value === '__EMPTY__' ? null : detail.selectedOption.value;
|
|
74
|
+
field.onChange(value);
|
|
75
|
+
}}
|
|
76
|
+
onBlur={() => field.onBlur()}
|
|
77
|
+
/>
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (context) {
|
|
81
|
+
return inner;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<FormFieldContext.Provider value={inputState}>
|
|
86
|
+
<FormField {...props}>{inner}</FormField>
|
|
87
|
+
</FormFieldContext.Provider>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export default SelectInput;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, fireEvent, cleanup } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
4
|
+
import { useInput, useResourceContext } from '@strato-admin/core';
|
|
5
|
+
import { SliderInput } from './SliderInput';
|
|
6
|
+
|
|
7
|
+
// Mock strato-core
|
|
8
|
+
vi.mock('@strato-admin/core', () => ({
|
|
9
|
+
useInput: vi.fn(),
|
|
10
|
+
useResourceContext: vi.fn(),
|
|
11
|
+
useTranslate: () => (key: string) => key,
|
|
12
|
+
ValidationError: ({ error }: any) => <span>{error}</span>,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock FieldTitle
|
|
16
|
+
vi.mock('./FieldTitle', () => ({
|
|
17
|
+
FieldTitle: ({ label, source }: any) => <span data-testid="field-title">{label || source}</span>,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock Cloudscape components
|
|
21
|
+
vi.mock('@cloudscape-design/components/slider', () => ({
|
|
22
|
+
default: ({ value, onChange, id, min, max, step }: any) => (
|
|
23
|
+
<input
|
|
24
|
+
type="range"
|
|
25
|
+
data-testid="cloudscape-slider"
|
|
26
|
+
id={id}
|
|
27
|
+
value={value}
|
|
28
|
+
min={min}
|
|
29
|
+
max={max}
|
|
30
|
+
step={step}
|
|
31
|
+
onChange={(e) => onChange({ detail: { value: Number(e.target.value) } })}
|
|
32
|
+
/>
|
|
33
|
+
),
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
vi.mock('@cloudscape-design/components/form-field', () => ({
|
|
37
|
+
default: ({ children, label, errorText, id }: any) => (
|
|
38
|
+
<div data-testid="form-field" id={id}>
|
|
39
|
+
<label>{label}</label>
|
|
40
|
+
{children}
|
|
41
|
+
{errorText && <span data-testid="error-text">{errorText}</span>}
|
|
42
|
+
</div>
|
|
43
|
+
),
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
describe('SliderInput', () => {
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
cleanup();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should render correctly', () => {
|
|
56
|
+
(useInput as any).mockReturnValue({
|
|
57
|
+
id: 'test-slider',
|
|
58
|
+
field: { value: 50, onChange: vi.fn(), onBlur: vi.fn() },
|
|
59
|
+
fieldState: { isTouched: false, invalid: false, error: null },
|
|
60
|
+
formState: { isSubmitted: false },
|
|
61
|
+
isRequired: false,
|
|
62
|
+
});
|
|
63
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
64
|
+
|
|
65
|
+
const { getByTestId, getByText } = render(<SliderInput source="rating" label="Rating" min={0} max={100} />);
|
|
66
|
+
|
|
67
|
+
expect(getByTestId('cloudscape-slider')).toBeDefined();
|
|
68
|
+
expect((getByTestId('cloudscape-slider') as HTMLInputElement).value).toBe('50');
|
|
69
|
+
expect(getByText('Rating')).toBeDefined();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should use min value if field value is null', () => {
|
|
73
|
+
(useInput as any).mockReturnValue({
|
|
74
|
+
id: 'test-slider',
|
|
75
|
+
field: { value: null, onChange: vi.fn(), onBlur: vi.fn() },
|
|
76
|
+
fieldState: { isTouched: false, invalid: false, error: null },
|
|
77
|
+
formState: { isSubmitted: false },
|
|
78
|
+
isRequired: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const { getByTestId } = render(<SliderInput source="rating" min={10} />);
|
|
82
|
+
|
|
83
|
+
expect((getByTestId('cloudscape-slider') as HTMLInputElement).value).toBe('10');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should call onChange with numeric value', () => {
|
|
87
|
+
const onChange = vi.fn();
|
|
88
|
+
(useInput as any).mockReturnValue({
|
|
89
|
+
id: 'test-slider',
|
|
90
|
+
field: { value: 50, onChange, onBlur: vi.fn() },
|
|
91
|
+
fieldState: { isTouched: false, invalid: false, error: null },
|
|
92
|
+
formState: { isSubmitted: false },
|
|
93
|
+
isRequired: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const { getByTestId } = render(<SliderInput source="rating" />);
|
|
97
|
+
const slider = getByTestId('cloudscape-slider');
|
|
98
|
+
|
|
99
|
+
fireEvent.change(slider, { target: { value: '75' } });
|
|
100
|
+
|
|
101
|
+
expect(onChange).toHaveBeenCalledWith(75);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useInput } from '@strato-admin/core';
|
|
3
|
+
import CloudscapeSlider, { SliderProps as CloudscapeSliderProps } from '@cloudscape-design/components/slider';
|
|
4
|
+
import { FormField } from './FormField';
|
|
5
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
|
+
import { InputProps } from './types';
|
|
7
|
+
|
|
8
|
+
export interface SliderInputProps
|
|
9
|
+
extends Omit<CloudscapeSliderProps, 'onChange' | 'value' | 'i18nStrings'>,
|
|
10
|
+
InputProps {}
|
|
11
|
+
|
|
12
|
+
export const SliderInput = (props: SliderInputProps) => {
|
|
13
|
+
const { label, source, defaultValue, validate, ...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 : props.min ?? 0;
|
|
28
|
+
|
|
29
|
+
const inner = (
|
|
30
|
+
<CloudscapeSlider
|
|
31
|
+
{...(rest as any)}
|
|
32
|
+
id={id}
|
|
33
|
+
value={value}
|
|
34
|
+
onChange={(event) => field.onChange(event.detail.value)}
|
|
35
|
+
/>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (context) {
|
|
39
|
+
return inner;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<FormFieldContext.Provider value={inputState}>
|
|
44
|
+
<FormField {...props}>{inner}</FormField>
|
|
45
|
+
</FormFieldContext.Provider>
|
|
46
|
+
);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export default SliderInput;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
import { useInput } from '@strato-admin/core';
|
|
3
|
+
import CloudscapeTextarea, { TextareaProps as CloudscapeTextareaProps } from '@cloudscape-design/components/textarea';
|
|
4
|
+
import { FormField } from './FormField';
|
|
5
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
|
+
import { InputProps } from './types';
|
|
7
|
+
|
|
8
|
+
export interface TextAreaInputProps
|
|
9
|
+
extends Omit<CloudscapeTextareaProps, 'onChange' | 'value' | 'onBlur'>,
|
|
10
|
+
InputProps {}
|
|
11
|
+
|
|
12
|
+
export const TextAreaInput = (props: TextAreaInputProps) => {
|
|
13
|
+
const { label, source, defaultValue, validate, ...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
|
+
const inner = (
|
|
27
|
+
<CloudscapeTextarea
|
|
28
|
+
{...rest}
|
|
29
|
+
{...field}
|
|
30
|
+
id={id}
|
|
31
|
+
value={field.value || ''}
|
|
32
|
+
onChange={(event) => field.onChange(event.detail.value)}
|
|
33
|
+
onBlur={() => field.onBlur()}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (context) {
|
|
38
|
+
return inner;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<FormFieldContext.Provider value={inputState}>
|
|
43
|
+
<FormField {...props}>{inner}</FormField>
|
|
44
|
+
</FormFieldContext.Provider>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default TextAreaInput;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import { vi, describe, it, expect, beforeEach } from 'vitest';
|
|
4
|
+
import { useInput, useResourceContext } from '@strato-admin/core';
|
|
5
|
+
import { TextInput } from './TextInput';
|
|
6
|
+
|
|
7
|
+
// Mock ra-core
|
|
8
|
+
vi.mock('@strato-admin/core', () => ({
|
|
9
|
+
useInput: vi.fn(),
|
|
10
|
+
useResourceContext: vi.fn(),
|
|
11
|
+
useTranslate: () => (key: string) => key,
|
|
12
|
+
ValidationError: ({ error }: any) => <span>{error}</span>,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
// Mock FieldTitle
|
|
16
|
+
vi.mock('./FieldTitle', () => ({
|
|
17
|
+
FieldTitle: ({ label, source }: any) => <span data-testid="field-title">{label || source}</span>,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// Mock Cloudscape components
|
|
21
|
+
vi.mock('@cloudscape-design/components/input', () => ({
|
|
22
|
+
default: ({ value, onChange, id }: any) => (
|
|
23
|
+
<input
|
|
24
|
+
data-testid="cloudscape-input"
|
|
25
|
+
id={id}
|
|
26
|
+
value={value}
|
|
27
|
+
onChange={(e) => onChange({ detail: { value: e.target.value } })}
|
|
28
|
+
/>
|
|
29
|
+
),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
vi.mock('@cloudscape-design/components/form-field', () => ({
|
|
33
|
+
default: ({ children, label, errorText, id }: any) => (
|
|
34
|
+
<div data-testid="form-field" id={id}>
|
|
35
|
+
<label>{label}</label>
|
|
36
|
+
{children}
|
|
37
|
+
{errorText && <span data-testid="error-text">{errorText}</span>}
|
|
38
|
+
</div>
|
|
39
|
+
),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe('TextInput', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.clearAllMocks();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should render correctly', () => {
|
|
48
|
+
(useInput as any).mockReturnValue({
|
|
49
|
+
id: 'test-input',
|
|
50
|
+
field: { value: 'test value', onChange: vi.fn(), onBlur: vi.fn() },
|
|
51
|
+
fieldState: { isTouched: false, invalid: false, error: null },
|
|
52
|
+
formState: { isSubmitted: false },
|
|
53
|
+
isRequired: false,
|
|
54
|
+
});
|
|
55
|
+
(useResourceContext as any).mockReturnValue('products');
|
|
56
|
+
|
|
57
|
+
const { getByTestId, getByText } = render(<TextInput source="title" label="Product Title" />);
|
|
58
|
+
|
|
59
|
+
expect(getByTestId('cloudscape-input')).toBeDefined();
|
|
60
|
+
expect((getByTestId('cloudscape-input') as HTMLInputElement).value).toBe('test value');
|
|
61
|
+
expect(getByText('Product Title')).toBeDefined();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should show error when touched and invalid', () => {
|
|
65
|
+
(useInput as any).mockReturnValue({
|
|
66
|
+
id: 'test-input',
|
|
67
|
+
field: { value: '', onChange: vi.fn(), onBlur: vi.fn() },
|
|
68
|
+
fieldState: { isTouched: true, invalid: true, error: { message: 'Required' } },
|
|
69
|
+
formState: { isSubmitted: false },
|
|
70
|
+
isRequired: true,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const { getByTestId } = render(<TextInput source="title" />);
|
|
74
|
+
|
|
75
|
+
expect(getByTestId('error-text').textContent).toBe('Required');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should show error when submitted and invalid', () => {
|
|
79
|
+
(useInput as any).mockReturnValue({
|
|
80
|
+
id: 'test-input',
|
|
81
|
+
field: { value: '', onChange: vi.fn(), onBlur: vi.fn() },
|
|
82
|
+
fieldState: { isTouched: false, invalid: true, error: { message: 'Required' } },
|
|
83
|
+
formState: { isSubmitted: true },
|
|
84
|
+
isRequired: true,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const { getByTestId } = render(<TextInput source="title" />);
|
|
88
|
+
|
|
89
|
+
expect(getByTestId('error-text').textContent).toBe('Required');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
|
|
2
|
+
import { useInput } from '@strato-admin/core';
|
|
3
|
+
import CloudscapeInput, { InputProps as CloudscapeInputProps } from '@cloudscape-design/components/input';
|
|
4
|
+
import { FormField } from './FormField';
|
|
5
|
+
import { FormFieldContext, useFormFieldContext } from './FormFieldContext';
|
|
6
|
+
import { InputProps } from './types';
|
|
7
|
+
|
|
8
|
+
export interface TextInputProps
|
|
9
|
+
extends Omit<CloudscapeInputProps, 'onChange' | 'value' | 'onBlur' | 'type'>,
|
|
10
|
+
InputProps {
|
|
11
|
+
type?: CloudscapeInputProps['type'];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const TextInput = (props: TextInputProps) => {
|
|
15
|
+
const { label, source, defaultValue, validate, type = 'text', ...rest } = props;
|
|
16
|
+
const context = useFormFieldContext();
|
|
17
|
+
const inputState =
|
|
18
|
+
context ??
|
|
19
|
+
useInput({
|
|
20
|
+
source,
|
|
21
|
+
defaultValue,
|
|
22
|
+
validate,
|
|
23
|
+
...rest,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const { id, field } = inputState;
|
|
27
|
+
|
|
28
|
+
const inner = (
|
|
29
|
+
<CloudscapeInput
|
|
30
|
+
{...rest}
|
|
31
|
+
{...field}
|
|
32
|
+
id={id}
|
|
33
|
+
type={type}
|
|
34
|
+
value={field.value || ''}
|
|
35
|
+
onChange={(event) => field.onChange(event.detail.value)}
|
|
36
|
+
onBlur={() => field.onBlur()}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (context) {
|
|
41
|
+
return inner;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<FormFieldContext.Provider value={inputState}>
|
|
46
|
+
<FormField {...props}>{inner}</FormField>
|
|
47
|
+
</FormFieldContext.Provider>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default TextInput;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './TextInput';
|
|
3
|
+
export * from './TextAreaInput';
|
|
4
|
+
export * from './NumberInput';
|
|
5
|
+
export * from './AttributeEditor';
|
|
6
|
+
export * from './SelectInput';
|
|
7
|
+
export * from './AutocompleteInput';
|
|
8
|
+
export * from './ReferenceInput';
|
|
9
|
+
export * from './SliderInput';
|
|
10
|
+
export * from './FormFieldContext';
|
|
11
|
+
export * from './FormField';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { InputProps as InputPropsBase } from '@strato-admin/core';
|
|
3
|
+
import { FormFieldProps as CloudscapeFormFieldProps } from '@cloudscape-design/components/form-field';
|
|
4
|
+
|
|
5
|
+
export interface StratoInputProps<T = any>
|
|
6
|
+
extends Omit<InputPropsBase<T>, 'label'>,
|
|
7
|
+
Pick<
|
|
8
|
+
CloudscapeFormFieldProps,
|
|
9
|
+
'description' | 'constraintText' | 'info' | 'secondaryControl' | 'stretch' | 'i18nStrings'
|
|
10
|
+
> {
|
|
11
|
+
label?: string | false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type InputProps<T = any> = StratoInputProps<T>;
|