@axinom/mosaic-ui 0.63.0-rc.0 → 0.63.0-rc.10
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/components/Accordion/Accordion.d.ts.map +1 -1
- package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts +1 -1
- package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts.map +1 -1
- package/dist/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.model.d.ts +1 -1
- package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
- package/dist/components/FieldSelection/FieldSelection.d.ts +2 -0
- package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -1
- package/dist/components/FormElements/DateTimeField/DateTimeText.d.ts +1 -1
- package/dist/components/FormElements/DateTimeField/DateTimeText.d.ts.map +1 -1
- package/dist/components/FormElements/DateTimeField/DateTimeTextField.d.ts.map +1 -1
- package/dist/components/FormStation/FormStation.d.ts +12 -1
- package/dist/components/FormStation/FormStation.d.ts.map +1 -1
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +3 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
- package/dist/components/FormStation/helpers/useDataProvider.d.ts +1 -1
- package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
- package/dist/components/Icons/Icons.d.ts.map +1 -1
- package/dist/components/Icons/Icons.models.d.ts +2 -1
- package/dist/components/Icons/Icons.models.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +2 -0
- package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
- package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.scss +14 -4
- package/src/components/Accordion/Accordion.spec.tsx +44 -64
- package/src/components/Accordion/Accordion.stories.tsx +52 -2
- package/src/components/Accordion/Accordion.tsx +9 -3
- package/src/components/Accordion/AccordionItem/AccordionItem.scss +1 -0
- package/src/components/Accordion/AccordionItem/AccordionItem.spec.tsx +46 -84
- package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.spec.tsx +22 -18
- package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.tsx +94 -20
- package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +6 -8
- package/src/components/Explorer/Explorer.model.ts +1 -1
- package/src/components/Explorer/Explorer.tsx +1 -0
- package/src/components/Explorer/helpers/useActions.ts +1 -1
- package/src/components/FieldSelection/FieldSelection.scss +7 -0
- package/src/components/FieldSelection/FieldSelection.spec.tsx +0 -2
- package/src/components/FieldSelection/FieldSelection.tsx +13 -11
- package/src/components/FormElements/DateTimeField/DateTimeText.tsx +8 -14
- package/src/components/FormElements/DateTimeField/DateTimeTextField.tsx +18 -5
- package/src/components/FormStation/Create/Create.stories.tsx +9 -0
- package/src/components/FormStation/FormStation.stories.tsx +2 -2
- package/src/components/FormStation/FormStation.tsx +16 -1
- package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +20 -3
- package/src/components/FormStation/helpers/useDataProvider.ts +6 -2
- package/src/components/Icons/Icons.models.ts +1 -0
- package/src/components/Icons/Icons.tsx +17 -0
- package/src/components/PageHeader/PageHeader.stories.tsx +8 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +2 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.scss +28 -0
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.spec.tsx +0 -10
- package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +4 -3
- package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +5 -1
- package/src/styles/variables.scss +3 -0
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Field } from 'formik';
|
|
2
|
-
import React from 'react';
|
|
1
|
+
import { Field, useFormikContext } from 'formik';
|
|
2
|
+
import React, { useEffect, useMemo } from 'react';
|
|
3
|
+
import { Data } from '../../../types';
|
|
4
|
+
import { FieldSelection } from '../../FieldSelection';
|
|
3
5
|
import {
|
|
4
6
|
CheckboxField,
|
|
5
7
|
CustomTagsField,
|
|
@@ -17,29 +19,101 @@ export const defaultComponentMap = {
|
|
|
17
19
|
export const BulkEditFormFieldsConfigConverter = (
|
|
18
20
|
config: BulkEditFieldConfigMap,
|
|
19
21
|
componentMap: Record<string, React.ElementType> = defaultComponentMap,
|
|
20
|
-
): JSX.Element
|
|
22
|
+
): JSX.Element => {
|
|
21
23
|
const keys = Object.keys(config);
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
const FormFields: React.FC = () => {
|
|
26
|
+
const {
|
|
27
|
+
setFieldValue,
|
|
28
|
+
setFieldTouched,
|
|
29
|
+
setErrors,
|
|
30
|
+
errors,
|
|
31
|
+
validateForm,
|
|
32
|
+
values,
|
|
33
|
+
} = useFormikContext<Data>();
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: fieldConfig.type;
|
|
35
|
+
const onFieldRemoved = (field: string): void => {
|
|
36
|
+
setFieldValue(field, undefined, false); // Clear the field value when removed
|
|
37
|
+
setFieldTouched(field, false, false); // Mark the field as not touched
|
|
31
38
|
|
|
32
|
-
|
|
39
|
+
if (errors[field]) {
|
|
40
|
+
// If there was an error for this field, clear it
|
|
41
|
+
const newErrors = { ...errors };
|
|
42
|
+
delete newErrors[field];
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return null; // Filter out null entries later
|
|
44
|
+
setErrors(newErrors);
|
|
45
|
+
|
|
46
|
+
validateForm();
|
|
38
47
|
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// Effect to clear empty fields
|
|
51
|
+
// This will set fields with empty strings or empty arrays to undefined
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
values &&
|
|
54
|
+
Object.keys(values).forEach((key) => {
|
|
55
|
+
if (values[key] === '' || values[key].length === 0) {
|
|
56
|
+
setFieldValue(key, undefined);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}, [setFieldValue, values]);
|
|
60
|
+
|
|
61
|
+
const onFieldAdded = (field: string): void => {
|
|
62
|
+
setFieldTouched(field, true); // Mark the field as touched when added
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const fields = useMemo(
|
|
66
|
+
() =>
|
|
67
|
+
keys
|
|
68
|
+
.map((key) => {
|
|
69
|
+
const fieldConfig = config[key];
|
|
70
|
+
|
|
71
|
+
// Determine the type of the field
|
|
72
|
+
const fieldType = Array.isArray(fieldConfig.type)
|
|
73
|
+
? 'Array' // Use 'Array' as the key for array types
|
|
74
|
+
: fieldConfig.type;
|
|
75
|
+
|
|
76
|
+
const Component =
|
|
77
|
+
componentMap[fieldType as keyof typeof componentMap];
|
|
78
|
+
|
|
79
|
+
if (!Component) {
|
|
80
|
+
// eslint-disable-next-line no-console
|
|
81
|
+
console.warn(`No component found for field type: ${fieldType}`);
|
|
82
|
+
return null; // Filter out null entries later
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Field
|
|
87
|
+
name={key}
|
|
88
|
+
key={key}
|
|
89
|
+
label={fieldConfig.label}
|
|
90
|
+
validate={(value: unknown) => {
|
|
91
|
+
if (fieldType === 'Array') {
|
|
92
|
+
// Array can be empty, so no validation needed
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (value === null || value === undefined || value === '') {
|
|
96
|
+
return 'This field is required';
|
|
97
|
+
}
|
|
98
|
+
}}
|
|
99
|
+
autoFocus={true}
|
|
100
|
+
as={Component}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
})
|
|
104
|
+
.filter((element): element is JSX.Element => element !== null),
|
|
105
|
+
[],
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<FieldSelection
|
|
110
|
+
onFieldRemoved={onFieldRemoved}
|
|
111
|
+
onFieldAdded={onFieldAdded}
|
|
112
|
+
>
|
|
113
|
+
{fields}
|
|
114
|
+
</FieldSelection>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
39
117
|
|
|
40
|
-
|
|
41
|
-
<Field name={key} key={key} label={fieldConfig.label} as={Component} />
|
|
42
|
-
);
|
|
43
|
-
})
|
|
44
|
-
.filter((element): element is JSX.Element => element !== null);
|
|
118
|
+
return <FormFields />;
|
|
45
119
|
};
|
|
@@ -6,7 +6,6 @@ import React, {
|
|
|
6
6
|
useState,
|
|
7
7
|
} from 'react';
|
|
8
8
|
import { Data } from '../../../types';
|
|
9
|
-
import { FieldSelection } from '../../FieldSelection';
|
|
10
9
|
import { FormStation } from '../../FormStation';
|
|
11
10
|
import { IconName } from '../../Icons';
|
|
12
11
|
import {
|
|
@@ -74,8 +73,9 @@ export const useBulkEdit = <T extends Data>({
|
|
|
74
73
|
() =>
|
|
75
74
|
bulkEditRegistration
|
|
76
75
|
? {
|
|
77
|
-
label: bulkEditRegistration.label,
|
|
76
|
+
label: bulkEditRegistration.label ?? 'Bulk Edit',
|
|
78
77
|
icon: bulkEditRegistration.icon ?? IconName.BulkEdit,
|
|
78
|
+
tag: 'BETA',
|
|
79
79
|
onClick: () => setIsBulkEditMode((prev) => !prev),
|
|
80
80
|
}
|
|
81
81
|
: undefined,
|
|
@@ -93,12 +93,8 @@ export const useBulkEdit = <T extends Data>({
|
|
|
93
93
|
if (bulkEditRegistration?.component) {
|
|
94
94
|
return bulkEditRegistration.component;
|
|
95
95
|
} else if (bulkEditRegistration?.config) {
|
|
96
|
-
return (
|
|
97
|
-
|
|
98
|
-
{BulkEditFormFieldsConfigConverter(
|
|
99
|
-
bulkEditRegistration?.config.fields,
|
|
100
|
-
)}
|
|
101
|
-
</FieldSelection>
|
|
96
|
+
return BulkEditFormFieldsConfigConverter(
|
|
97
|
+
bulkEditRegistration?.config.fields,
|
|
102
98
|
);
|
|
103
99
|
}
|
|
104
100
|
return null;
|
|
@@ -116,6 +112,8 @@ export const useBulkEdit = <T extends Data>({
|
|
|
116
112
|
: undefined
|
|
117
113
|
}
|
|
118
114
|
showSaveHeaderAction={!noItemsSelected}
|
|
115
|
+
saveHeaderActionConfig={{ label: 'Apply', icon: IconName.Checkmark }}
|
|
116
|
+
saveNotificationMessage="Your changes are being applied to the selected items."
|
|
119
117
|
>
|
|
120
118
|
{BulkEditContent}
|
|
121
119
|
</FormStation>
|
|
@@ -127,7 +127,7 @@ export interface QuickEditRegistration<T> {
|
|
|
127
127
|
|
|
128
128
|
export interface BulkEditRegistration<T extends Data> {
|
|
129
129
|
/** The label of the action. */
|
|
130
|
-
label
|
|
130
|
+
label?: string;
|
|
131
131
|
/** Optional built in icon. This prop also accepts an img src. */
|
|
132
132
|
icon?: IconName | string;
|
|
133
133
|
/** Component to render. This will override the component that is generated. */
|
|
@@ -487,6 +487,7 @@ export const Explorer = React.forwardRef(function Explorer<T extends Data>(
|
|
|
487
487
|
onItemSelected={itemSelectedHandler}
|
|
488
488
|
onRequestMoreData={onRequestMoreData}
|
|
489
489
|
onSortChanged={(sortData: SortData<T>) => {
|
|
490
|
+
listRef.current?.resetSelection();
|
|
490
491
|
onSortChanged(sortData);
|
|
491
492
|
setSortOrder(sortData);
|
|
492
493
|
}}
|
|
@@ -63,6 +63,7 @@ export const useActions = <T extends Data>({
|
|
|
63
63
|
}: UseActionsProps<T>): UseActionsReturnType => {
|
|
64
64
|
const bulkActionItems: PageHeaderJsActionProps[] = useMemo(
|
|
65
65
|
() => [
|
|
66
|
+
...(bulkEditAction ? [bulkEditAction] : []),
|
|
66
67
|
...(bulkActions ?? []).map((action) => ({
|
|
67
68
|
...action,
|
|
68
69
|
onClick: async () => {
|
|
@@ -101,7 +102,6 @@ export const useActions = <T extends Data>({
|
|
|
101
102
|
}
|
|
102
103
|
},
|
|
103
104
|
})),
|
|
104
|
-
...(bulkEditAction ? [bulkEditAction] : []),
|
|
105
105
|
],
|
|
106
106
|
[
|
|
107
107
|
bulkActions,
|
|
@@ -34,7 +34,6 @@ describe('FieldSelection', () => {
|
|
|
34
34
|
target: { value: 'field1' },
|
|
35
35
|
});
|
|
36
36
|
fireEvent.click(screen.getByText('Field 1'));
|
|
37
|
-
fireEvent.click(screen.getByTestId('add-field-button'));
|
|
38
37
|
|
|
39
38
|
expect(screen.getByTestId('field-field1')).toBeInTheDocument();
|
|
40
39
|
});
|
|
@@ -51,7 +50,6 @@ describe('FieldSelection', () => {
|
|
|
51
50
|
target: { value: 'field1' },
|
|
52
51
|
});
|
|
53
52
|
fireEvent.click(screen.getByText('Field 1'));
|
|
54
|
-
fireEvent.click(screen.getByTestId('add-field-button'));
|
|
55
53
|
|
|
56
54
|
expect(screen.getByTestId('field-field1')).toBeInTheDocument();
|
|
57
55
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { noop } from '../../helpers/utils';
|
|
2
3
|
import { Accordion, AccordionItem } from '../Accordion';
|
|
3
4
|
import { Button, ButtonContext } from '../Buttons';
|
|
4
5
|
import { Select } from '../FormElements';
|
|
@@ -8,6 +9,8 @@ import classes from './FieldSelection.scss';
|
|
|
8
9
|
|
|
9
10
|
interface FieldSelectionProps {
|
|
10
11
|
className?: string;
|
|
12
|
+
onFieldAdded?: (field: string) => void;
|
|
13
|
+
onFieldRemoved?: (field: string) => void;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
interface FieldDefinition {
|
|
@@ -19,6 +22,8 @@ interface FieldDefinition {
|
|
|
19
22
|
|
|
20
23
|
export const FieldSelection: React.FC<FieldSelectionProps> = ({
|
|
21
24
|
className,
|
|
25
|
+
onFieldAdded = noop,
|
|
26
|
+
onFieldRemoved = noop,
|
|
22
27
|
children,
|
|
23
28
|
}) => {
|
|
24
29
|
useEffect(() => {
|
|
@@ -70,14 +75,15 @@ export const FieldSelection: React.FC<FieldSelectionProps> = ({
|
|
|
70
75
|
...currentFields,
|
|
71
76
|
newField,
|
|
72
77
|
]);
|
|
78
|
+
onFieldAdded(newField.value);
|
|
73
79
|
}
|
|
74
80
|
}}
|
|
75
81
|
/>
|
|
76
82
|
}
|
|
77
83
|
>
|
|
78
|
-
{selectedFields.map((field
|
|
84
|
+
{selectedFields.map((field) => (
|
|
79
85
|
<AccordionItem
|
|
80
|
-
key={
|
|
86
|
+
key={field.value}
|
|
81
87
|
header={
|
|
82
88
|
<FieldSelectionItemHeader
|
|
83
89
|
label={field.label as string}
|
|
@@ -88,6 +94,7 @@ export const FieldSelection: React.FC<FieldSelectionProps> = ({
|
|
|
88
94
|
setAvailableFields((currentFields) =>
|
|
89
95
|
[...currentFields, field].sort((a, b) => a.index - b.index),
|
|
90
96
|
);
|
|
97
|
+
onFieldRemoved(field.value);
|
|
91
98
|
}}
|
|
92
99
|
/>
|
|
93
100
|
}
|
|
@@ -105,7 +112,7 @@ const FieldSelectionHeader: React.FC<{
|
|
|
105
112
|
}> = ({ fields, onAddField }) => {
|
|
106
113
|
const [value, setValue] = React.useState<string>();
|
|
107
114
|
|
|
108
|
-
|
|
115
|
+
useEffect(() => {
|
|
109
116
|
if (value) {
|
|
110
117
|
onAddField(value);
|
|
111
118
|
setValue(undefined);
|
|
@@ -113,22 +120,17 @@ const FieldSelectionHeader: React.FC<{
|
|
|
113
120
|
}, [onAddField, value]);
|
|
114
121
|
|
|
115
122
|
return (
|
|
116
|
-
<div className={classes.
|
|
123
|
+
<div className={classes.selectionHeader}>
|
|
117
124
|
<Select
|
|
118
125
|
label="Field"
|
|
119
126
|
name="field"
|
|
120
127
|
placeholder="Select Field"
|
|
121
128
|
options={fields}
|
|
129
|
+
disabled={fields.length === 0}
|
|
122
130
|
inlineMode={true}
|
|
123
131
|
onChange={(e) => setValue(e.currentTarget.value)}
|
|
124
132
|
value={value}
|
|
125
133
|
/>
|
|
126
|
-
<Button
|
|
127
|
-
icon={IconName.Plus}
|
|
128
|
-
buttonContext={value ? ButtonContext.Active : ButtonContext.Icon}
|
|
129
|
-
onButtonClicked={handleButtonClicked}
|
|
130
|
-
dataTestId="add-field-button"
|
|
131
|
-
/>
|
|
132
134
|
</div>
|
|
133
135
|
);
|
|
134
136
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import clsx from 'clsx';
|
|
2
2
|
import { DateTime } from 'luxon';
|
|
3
3
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { noop } from '../../../helpers/utils';
|
|
4
5
|
import { Button, ButtonContext } from '../../Buttons';
|
|
5
6
|
import { DateTimePicker } from '../../DateTime/DateTimePicker';
|
|
6
7
|
import { IconName } from '../../Icons';
|
|
@@ -18,7 +19,7 @@ export interface DateTimeTextProps extends BaseFormControl {
|
|
|
18
19
|
autoFocus?: boolean;
|
|
19
20
|
/** Whether or not the control supports auto complete */
|
|
20
21
|
autoComplete?: 'on' | 'off';
|
|
21
|
-
/** Whether the control modifies the time portion of the value */
|
|
22
|
+
/** Whether the control modifies the time portion of the value (default: true) */
|
|
22
23
|
modifyTime?: boolean;
|
|
23
24
|
/** Callback when the datepicker popup is closed or the date field value is changed */
|
|
24
25
|
onChange?: (value: string | null, isValidDate: boolean) => void;
|
|
@@ -39,7 +40,7 @@ export const DateTimeText: React.FC<DateTimeTextProps> = ({
|
|
|
39
40
|
error = undefined,
|
|
40
41
|
autoFocus = false,
|
|
41
42
|
autoComplete,
|
|
42
|
-
onChange,
|
|
43
|
+
onChange = noop,
|
|
43
44
|
modifyTime = true,
|
|
44
45
|
className = '',
|
|
45
46
|
...rest
|
|
@@ -60,34 +61,27 @@ export const DateTimeText: React.FC<DateTimeTextProps> = ({
|
|
|
60
61
|
|
|
61
62
|
const onBlurHandler = useCallback(
|
|
62
63
|
(e: React.FocusEvent<HTMLInputElement>) => {
|
|
63
|
-
// `display` represents the formatted version of the `value` prop and is updated whenever `value` changes.
|
|
64
|
-
// Comparing `e.target.value` with `display` ensures that onChange is only triggered when the input value has changed.
|
|
65
|
-
if (e.target.value === display) {
|
|
66
|
-
// If the value hasn't changed, do not trigger onChange
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
64
|
const textValue = e.target.value;
|
|
71
65
|
const parsedValue = modifyTime
|
|
72
66
|
? DateTime.fromFormat(textValue, 'f')
|
|
73
67
|
: DateTime.fromFormat(textValue, 'D');
|
|
74
68
|
|
|
75
69
|
if (parsedValue.isValid) {
|
|
76
|
-
onChange
|
|
70
|
+
onChange(parsedValue.toISO(), parsedValue.isValid);
|
|
77
71
|
} else if (textValue === '') {
|
|
78
|
-
onChange
|
|
72
|
+
onChange(null, true);
|
|
79
73
|
} else {
|
|
80
74
|
// Fallback parser if the user types in an ISO date
|
|
81
75
|
const isoParsedValue = DateTime.fromISO(textValue);
|
|
82
76
|
|
|
83
77
|
if (isoParsedValue.isValid) {
|
|
84
|
-
onChange
|
|
78
|
+
onChange(isoParsedValue.toISO(), isoParsedValue.isValid);
|
|
85
79
|
} else {
|
|
86
|
-
onChange
|
|
80
|
+
onChange(textValue, parsedValue.isValid);
|
|
87
81
|
}
|
|
88
82
|
}
|
|
89
83
|
},
|
|
90
|
-
[
|
|
84
|
+
[modifyTime, onChange],
|
|
91
85
|
);
|
|
92
86
|
|
|
93
87
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useField } from 'formik';
|
|
2
|
+
import { DateTime } from 'luxon';
|
|
2
3
|
import React from 'react';
|
|
3
4
|
import { useFormikError } from '../useFormikError';
|
|
4
5
|
import { DateTimeText, DateTimeTextProps } from './DateTimeText';
|
|
@@ -14,16 +15,28 @@ export const DateTimeTextField: React.FC<
|
|
|
14
15
|
const error = useFormikError(props.name);
|
|
15
16
|
const [, meta, helpers] = useField(props.name);
|
|
16
17
|
|
|
17
|
-
const handleChange = (value: string | null, valid
|
|
18
|
+
const handleChange = (value: string | null, valid: boolean): void => {
|
|
18
19
|
if (!valid) {
|
|
19
20
|
helpers.setError('Invalid Date');
|
|
20
21
|
helpers.setValue(value, false);
|
|
21
22
|
} else {
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
const initialDateTime = DateTime.fromISO(meta.initialValue);
|
|
24
|
+
const newDateTime = DateTime.fromISO(value ?? '');
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
if (newDateTime.setZone(initialDateTime.zone).equals(initialDateTime)) {
|
|
27
|
+
// If the new date is the same as the initial value and the timezone is the only change,
|
|
28
|
+
// set it to the initial value
|
|
29
|
+
helpers.setValue(meta.initialValue);
|
|
30
|
+
} else if (
|
|
31
|
+
(value === null || value === '') &&
|
|
32
|
+
(!meta.initialValue || meta.initialValue === '')
|
|
33
|
+
) {
|
|
34
|
+
// If the value is null or empty and the initial value is also null or empty,
|
|
35
|
+
// set it to the initial value
|
|
36
|
+
helpers.setValue(meta.initialValue);
|
|
37
|
+
} else {
|
|
38
|
+
helpers.setValue(value);
|
|
39
|
+
}
|
|
27
40
|
}
|
|
28
41
|
};
|
|
29
42
|
|
|
@@ -7,6 +7,7 @@ import * as Yup from 'yup';
|
|
|
7
7
|
import { createGroups } from '../../../helpers/storybook';
|
|
8
8
|
import {
|
|
9
9
|
CustomTagsField,
|
|
10
|
+
DateTimeTextField,
|
|
10
11
|
FileUpload,
|
|
11
12
|
SelectField,
|
|
12
13
|
SingleLineTextField,
|
|
@@ -23,6 +24,7 @@ interface CreateValues {
|
|
|
23
24
|
shortDescription?: string;
|
|
24
25
|
longDescription?: string;
|
|
25
26
|
cast?: string[];
|
|
27
|
+
released?: string;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
interface APIResponse {
|
|
@@ -108,6 +110,12 @@ export const Default: StoryObj<CreateStoryType> = {
|
|
|
108
110
|
as={SingleLineTextField}
|
|
109
111
|
type="password"
|
|
110
112
|
/>
|
|
113
|
+
<Field
|
|
114
|
+
name="released"
|
|
115
|
+
label="Released"
|
|
116
|
+
as={DateTimeTextField}
|
|
117
|
+
modifyTime={false}
|
|
118
|
+
/>
|
|
111
119
|
</>
|
|
112
120
|
),
|
|
113
121
|
title: 'Create a Movie',
|
|
@@ -127,6 +135,7 @@ export const Default: StoryObj<CreateStoryType> = {
|
|
|
127
135
|
shortDescription: '',
|
|
128
136
|
longDescription: '',
|
|
129
137
|
cast: [],
|
|
138
|
+
released: '',
|
|
130
139
|
},
|
|
131
140
|
},
|
|
132
141
|
cancelNavigationUrl: '/',
|
|
@@ -169,7 +169,7 @@ export const Extended: StoryObj<typeof Details> = (() => {
|
|
|
169
169
|
shortDescription: 'Some short abstract...',
|
|
170
170
|
longDescription: '',
|
|
171
171
|
cast: ['Jane Doe', 'John Doe'],
|
|
172
|
-
released: '2020-04-
|
|
172
|
+
released: '2020-04-03',
|
|
173
173
|
list: listData,
|
|
174
174
|
archived: false,
|
|
175
175
|
timestamp: '00:00:00.001',
|
|
@@ -227,7 +227,7 @@ export const Extended: StoryObj<typeof Details> = (() => {
|
|
|
227
227
|
name="released"
|
|
228
228
|
label="Released"
|
|
229
229
|
as={DateTimeTextField}
|
|
230
|
-
|
|
230
|
+
modifyTime={false}
|
|
231
231
|
/>
|
|
232
232
|
<Field
|
|
233
233
|
name="password"
|
|
@@ -5,6 +5,7 @@ import { OptionalObjectSchema } from 'yup/lib/object';
|
|
|
5
5
|
import { Data } from '../../types/data';
|
|
6
6
|
import { BulkEditContext } from '../Explorer/BulkEdit/BulkEditContext';
|
|
7
7
|
import { QuickEditContext } from '../Explorer/QuickEdit/QuickEditContext';
|
|
8
|
+
import { PageHeaderJsActionProps } from '../PageHeader/PageHeaderAction';
|
|
8
9
|
import { StationMessage } from '../models';
|
|
9
10
|
import {
|
|
10
11
|
FormActionData,
|
|
@@ -39,6 +40,16 @@ export interface FormStationProps<
|
|
|
39
40
|
actionsWidth?: string;
|
|
40
41
|
/** If set to true, the save header action is shown. (default: true) */
|
|
41
42
|
showSaveHeaderAction?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Optional configuration for the save header action button.
|
|
45
|
+
* Allows customizing the label and icon.
|
|
46
|
+
*/
|
|
47
|
+
saveHeaderActionConfig?: Pick<PageHeaderJsActionProps, 'label' | 'icon'>;
|
|
48
|
+
/**
|
|
49
|
+
* If set, this will override the default notification message shown
|
|
50
|
+
* after a successful save.
|
|
51
|
+
*/
|
|
52
|
+
saveNotificationMessage?: string;
|
|
42
53
|
/**
|
|
43
54
|
* An object containing the initial data of the form.
|
|
44
55
|
*/
|
|
@@ -91,6 +102,8 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
91
102
|
className = '',
|
|
92
103
|
setTabTitle = true,
|
|
93
104
|
showSaveHeaderAction = true,
|
|
105
|
+
saveHeaderActionConfig,
|
|
106
|
+
saveNotificationMessage,
|
|
94
107
|
}: PropsWithChildren<
|
|
95
108
|
FormStationProps<TValues, TSubmitResponse>
|
|
96
109
|
>): JSX.Element => {
|
|
@@ -103,7 +116,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
103
116
|
setStationError,
|
|
104
117
|
isFormSubmitting,
|
|
105
118
|
lastSubmittedResponse,
|
|
106
|
-
} = useDataProvider(initialData, saveData);
|
|
119
|
+
} = useDataProvider(initialData, saveData, saveNotificationMessage);
|
|
107
120
|
|
|
108
121
|
const { setValidationError, validationWatcher } = useValidationError(
|
|
109
122
|
stationError,
|
|
@@ -137,6 +150,8 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
|
|
|
137
150
|
className={classes.header}
|
|
138
151
|
setTabTitle={setTabTitle}
|
|
139
152
|
showSaveHeaderAction={showSaveHeaderAction}
|
|
153
|
+
saveHeaderActionConfig={saveHeaderActionConfig}
|
|
154
|
+
setValidationError={setValidationError}
|
|
140
155
|
/>
|
|
141
156
|
{!bulkEditContext && (
|
|
142
157
|
<SaveOnNavigate
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
PageHeaderActionType,
|
|
11
11
|
PageHeaderProps,
|
|
12
12
|
} from '../../PageHeader';
|
|
13
|
+
import { PageHeaderJsActionProps } from '../../PageHeader/PageHeaderAction';
|
|
13
14
|
import { useTitle } from '../helpers/useTitle';
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -22,6 +23,8 @@ export const FormStationHeader: React.FC<
|
|
|
22
23
|
cancelNavigationUrl?: string;
|
|
23
24
|
setTabTitle?: boolean;
|
|
24
25
|
showSaveHeaderAction?: boolean;
|
|
26
|
+
saveHeaderActionConfig?: Pick<PageHeaderJsActionProps, 'label' | 'icon'>;
|
|
27
|
+
setValidationError: () => void;
|
|
25
28
|
}
|
|
26
29
|
> = ({
|
|
27
30
|
titleProperty,
|
|
@@ -31,8 +34,14 @@ export const FormStationHeader: React.FC<
|
|
|
31
34
|
className,
|
|
32
35
|
setTabTitle,
|
|
33
36
|
showSaveHeaderAction,
|
|
37
|
+
saveHeaderActionConfig = {
|
|
38
|
+
label: 'Save',
|
|
39
|
+
icon: IconName.Save,
|
|
40
|
+
},
|
|
41
|
+
setValidationError,
|
|
34
42
|
}) => {
|
|
35
|
-
const { dirty, submitForm, resetForm } =
|
|
43
|
+
const { dirty, submitForm, resetForm, isValid } =
|
|
44
|
+
useFormikContext<FormikValues>();
|
|
36
45
|
const quickEditContext = useContext(QuickEditContext);
|
|
37
46
|
const bulkEditContext = useContext(BulkEditContext);
|
|
38
47
|
|
|
@@ -56,14 +65,18 @@ export const FormStationHeader: React.FC<
|
|
|
56
65
|
|
|
57
66
|
if (showSaveHeaderAction) {
|
|
58
67
|
actionItems.push({
|
|
59
|
-
label:
|
|
60
|
-
icon:
|
|
68
|
+
label: saveHeaderActionConfig.label,
|
|
69
|
+
icon: saveHeaderActionConfig.icon,
|
|
61
70
|
kind: 'action',
|
|
62
71
|
actionType: PageHeaderActionType.Context,
|
|
63
72
|
onClick: async () => {
|
|
64
73
|
if (quickEditContext?.isQuickEditMode) {
|
|
65
74
|
quickEditContext.refresh();
|
|
66
75
|
} else if (bulkEditContext?.isBulkEditMode) {
|
|
76
|
+
if (!isValid) {
|
|
77
|
+
setValidationError();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
67
80
|
await submitForm();
|
|
68
81
|
history.replace(history.location.pathname);
|
|
69
82
|
} else {
|
|
@@ -114,8 +127,12 @@ export const FormStationHeader: React.FC<
|
|
|
114
127
|
cancelNavigationUrl,
|
|
115
128
|
dirty,
|
|
116
129
|
history,
|
|
130
|
+
isValid,
|
|
117
131
|
quickEditContext,
|
|
118
132
|
resetForm,
|
|
133
|
+
saveHeaderActionConfig.icon,
|
|
134
|
+
saveHeaderActionConfig.label,
|
|
135
|
+
setValidationError,
|
|
119
136
|
showSaveHeaderAction,
|
|
120
137
|
submitForm,
|
|
121
138
|
]);
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
export type FormStationDataProvider = <TValues extends Data, TSubmitResponse>(
|
|
21
21
|
initialData: InitialFormData<TValues>,
|
|
22
22
|
saveData: SaveDataFunction<TValues, TSubmitResponse>,
|
|
23
|
+
saveNotificationMessage?: string,
|
|
23
24
|
) => {
|
|
24
25
|
onSubmit: (
|
|
25
26
|
values: TValues,
|
|
@@ -38,6 +39,7 @@ export const useDataProvider: FormStationDataProvider = <
|
|
|
38
39
|
>(
|
|
39
40
|
initialData: InitialFormData<TValues>,
|
|
40
41
|
saveData: SaveDataFunction<TValues, TSubmitResponse>,
|
|
42
|
+
saveNotificationMessage?: string,
|
|
41
43
|
) => {
|
|
42
44
|
const [stationError, setStationError] = useState<StationErrorStateType>();
|
|
43
45
|
const [isFormSubmitting, setIsFormSubmitting] = useState<boolean>(false);
|
|
@@ -92,7 +94,9 @@ export const useDataProvider: FormStationDataProvider = <
|
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
showNotification({
|
|
95
|
-
title:
|
|
97
|
+
title:
|
|
98
|
+
saveNotificationMessage ??
|
|
99
|
+
'Your changes were saved successfully.',
|
|
96
100
|
options: {
|
|
97
101
|
type: 'success',
|
|
98
102
|
autoClose: 1500,
|
|
@@ -115,7 +119,7 @@ export const useDataProvider: FormStationDataProvider = <
|
|
|
115
119
|
setIsFormSubmitting(false);
|
|
116
120
|
}
|
|
117
121
|
},
|
|
118
|
-
[isFormSubmitting, initialData, saveData,
|
|
122
|
+
[isFormSubmitting, initialData, saveData, saveNotificationMessage],
|
|
119
123
|
);
|
|
120
124
|
|
|
121
125
|
return {
|
|
@@ -10,6 +10,22 @@ export interface IconsProps {
|
|
|
10
10
|
className?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const AxiIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
14
|
+
<svg
|
|
15
|
+
className={clsx(classes.icons, className)}
|
|
16
|
+
version="1.1"
|
|
17
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
18
|
+
viewBox="0 0 40 40"
|
|
19
|
+
>
|
|
20
|
+
<path
|
|
21
|
+
vectorEffect="non-scaling-stroke"
|
|
22
|
+
fill="none"
|
|
23
|
+
strokeWidth="2"
|
|
24
|
+
d="M24.3,36h-8.5c-5,0-9.1-4.1-9.1-9.1v-5.3c0-5,4.1-9.1,9.1-9.1h8.5c5,0,9.1,4.1,9.1,9.1v5.3c0,5-4.1,9.1-9.1,9.1ZM6.8,20.4h-.9c-1.3,0-2.3,1.1-2.3,2.3v3c0,1.3,1.1,2.3,2.3,2.3h.9M33.3,28.2h.9c1.3,0,2.3-1.1,2.3-2.3v-3c0-1.3-1.1-2.3-2.3-2.3h-.9M14.1,20.4c-1.4,0-2.5,1.1-2.5,2.5s1.1,2.5,2.5,2.5,2.5-1.1,2.5-2.5-1.1-2.5-2.5-2.5ZM19.7,4c-1.4,0-2.5,1.1-2.5,2.5s1.1,2.5,2.5,2.5,2.5-1.1,2.5-2.5-1.1-2.5-2.5-2.5ZM25.9,20.4c-1.4,0-2.5,1.1-2.5,2.5s1.1,2.5,2.5,2.5,2.5-1.1,2.5-2.5-1.1-2.5-2.5-2.5ZM19.7,9v3.5M16.1,30.2h7.8"
|
|
25
|
+
/>
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
|
|
13
29
|
const ArchiveIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
14
30
|
<svg
|
|
15
31
|
className={clsx(classes.icons, className)}
|
|
@@ -924,6 +940,7 @@ const XIcon: React.FC<{ className?: string }> = ({ className }) => (
|
|
|
924
940
|
*/
|
|
925
941
|
export const Icons: React.FC<IconsProps> = ({ icon, className }) => {
|
|
926
942
|
const icons: { [key in IconName]: JSX.Element } = {
|
|
943
|
+
[IconName.Axi]: <AxiIcon className={className} />,
|
|
927
944
|
[IconName.Archive]: <ArchiveIcon className={className} />,
|
|
928
945
|
[IconName.Audio]: <AudioIcon className={className} />,
|
|
929
946
|
[IconName.DescriptiveAudio]: <DescriptiveAudioIcon className={className} />,
|