@axinom/mosaic-ui 0.63.0-rc.4 → 0.63.0-rc.6

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.
Files changed (26) hide show
  1. package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts +1 -1
  2. package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts.map +1 -1
  3. package/dist/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
  4. package/dist/components/Explorer/Explorer.model.d.ts +1 -1
  5. package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
  6. package/dist/components/FieldSelection/FieldSelection.d.ts +2 -0
  7. package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -1
  8. package/dist/components/FormStation/FormStation.d.ts +12 -1
  9. package/dist/components/FormStation/FormStation.d.ts.map +1 -1
  10. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +3 -0
  11. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  12. package/dist/components/FormStation/helpers/useDataProvider.d.ts +1 -1
  13. package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
  14. package/dist/index.es.js +2 -2
  15. package/dist/index.es.js.map +1 -1
  16. package/dist/index.js +2 -2
  17. package/dist/index.js.map +1 -1
  18. package/package.json +2 -2
  19. package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.spec.tsx +22 -18
  20. package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.tsx +94 -20
  21. package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +5 -8
  22. package/src/components/Explorer/Explorer.model.ts +1 -1
  23. package/src/components/FieldSelection/FieldSelection.tsx +7 -0
  24. package/src/components/FormStation/FormStation.tsx +16 -1
  25. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +20 -3
  26. package/src/components/FormStation/helpers/useDataProvider.ts +6 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.63.0-rc.4",
3
+ "version": "0.63.0-rc.6",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -112,5 +112,5 @@
112
112
  "publishConfig": {
113
113
  "access": "public"
114
114
  },
115
- "gitHead": "490e0fb462b6c1dd2024b1f30076f647a14216ce"
115
+ "gitHead": "37f68c4083df80f25b1fd94ec348c5bbf54a639c"
116
116
  }
@@ -16,12 +16,22 @@ jest.mock('../../FormElements', () => ({
16
16
  )),
17
17
  }));
18
18
 
19
+ jest.mock('../../FieldSelection', () => ({
20
+ FieldSelection: jest.fn(({ children }) => (
21
+ <div data-testid="FieldSelection">{children}</div>
22
+ )),
23
+ }));
24
+
19
25
  jest.mock('formik', () => ({
20
26
  Field: jest.fn(({ name, label, as: Component }) => (
21
27
  <div data-testid={`Field-${name}`}>
22
28
  <Component name={name} label={label} />
23
29
  </div>
24
30
  )),
31
+ useFormikContext: jest.fn(() => ({
32
+ setFieldValue: jest.fn(),
33
+ setFieldTouched: jest.fn(),
34
+ })),
25
35
  }));
26
36
 
27
37
  describe('BulkEditFormFieldsConfigConverter', () => {
@@ -35,9 +45,7 @@ describe('BulkEditFormFieldsConfigConverter', () => {
35
45
  },
36
46
  };
37
47
 
38
- const { getByTestId } = render(
39
- <>{BulkEditFormFieldsConfigConverter(config)}</>,
40
- );
48
+ const { getByTestId } = render(BulkEditFormFieldsConfigConverter(config));
41
49
 
42
50
  expect(getByTestId('Field-title')).toBeInTheDocument();
43
51
  expect(getByTestId('SingleLineTextField')).toHaveTextContent('title-Title');
@@ -53,9 +61,7 @@ describe('BulkEditFormFieldsConfigConverter', () => {
53
61
  },
54
62
  };
55
63
 
56
- const { getByTestId } = render(
57
- <>{BulkEditFormFieldsConfigConverter(config)}</>,
58
- );
64
+ const { getByTestId } = render(BulkEditFormFieldsConfigConverter(config));
59
65
 
60
66
  expect(getByTestId('Field-isArchived')).toBeInTheDocument();
61
67
  expect(getByTestId('CheckboxField')).toHaveTextContent(
@@ -77,9 +83,7 @@ describe('BulkEditFormFieldsConfigConverter', () => {
77
83
  },
78
84
  };
79
85
 
80
- const { getByTestId } = render(
81
- <>{BulkEditFormFieldsConfigConverter(config)}</>,
82
- );
86
+ const { getByTestId } = render(BulkEditFormFieldsConfigConverter(config));
83
87
 
84
88
  expect(getByTestId('Field-tags')).toBeInTheDocument();
85
89
  expect(getByTestId('CustomTagsField')).toHaveTextContent('tags-Tags');
@@ -111,9 +115,7 @@ describe('BulkEditFormFieldsConfigConverter', () => {
111
115
  },
112
116
  };
113
117
 
114
- const { getByTestId } = render(
115
- <>{BulkEditFormFieldsConfigConverter(config)}</>,
116
- );
118
+ const { getByTestId } = render(BulkEditFormFieldsConfigConverter(config));
117
119
 
118
120
  expect(getByTestId('Field-title')).toBeInTheDocument();
119
121
  expect(getByTestId('SingleLineTextField')).toHaveTextContent('title-Title');
@@ -138,14 +140,15 @@ describe('BulkEditFormFieldsConfigConverter', () => {
138
140
  },
139
141
  };
140
142
 
141
- const { container } = render(
142
- <>{BulkEditFormFieldsConfigConverter(config)}</>,
143
+ const { container, getByTestId } = render(
144
+ BulkEditFormFieldsConfigConverter(config),
143
145
  );
144
146
 
145
147
  expect(consoleWarnSpy).toHaveBeenCalledWith(
146
148
  'No component found for field type: UnsupportedType',
147
149
  );
148
- expect(container.firstChild).toBeNull();
150
+ expect(getByTestId('FieldSelection')).toBeInTheDocument();
151
+ expect(container.firstChild).toBeEmptyDOMElement();
149
152
 
150
153
  consoleWarnSpy.mockRestore();
151
154
  });
@@ -153,10 +156,11 @@ describe('BulkEditFormFieldsConfigConverter', () => {
153
156
  it('handles empty configuration gracefully', () => {
154
157
  const config: BulkEditFieldConfigMap = {};
155
158
 
156
- const { container } = render(
157
- <>{BulkEditFormFieldsConfigConverter(config)}</>,
159
+ const { container, getByTestId } = render(
160
+ BulkEditFormFieldsConfigConverter(config),
158
161
  );
159
162
 
160
- expect(container.firstChild).toBeNull();
163
+ expect(getByTestId('FieldSelection')).toBeInTheDocument();
164
+ expect(container.firstChild).toBeEmptyDOMElement();
161
165
  });
162
166
  });
@@ -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
- return keys
24
- .map((key) => {
25
- const fieldConfig = config[key];
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
- // Determine the type of the field
28
- const fieldType = Array.isArray(fieldConfig.type)
29
- ? 'Array' // Use 'Array' as the key for array types
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
- const Component = componentMap[fieldType as keyof typeof componentMap];
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
- if (!Component) {
35
- // eslint-disable-next-line no-console
36
- console.warn(`No component found for field type: ${fieldType}`);
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
- return (
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,7 +73,7 @@ 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,
79
78
  onClick: () => setIsBulkEditMode((prev) => !prev),
80
79
  }
@@ -93,12 +92,8 @@ export const useBulkEdit = <T extends Data>({
93
92
  if (bulkEditRegistration?.component) {
94
93
  return bulkEditRegistration.component;
95
94
  } else if (bulkEditRegistration?.config) {
96
- return (
97
- <FieldSelection>
98
- {BulkEditFormFieldsConfigConverter(
99
- bulkEditRegistration?.config.fields,
100
- )}
101
- </FieldSelection>
95
+ return BulkEditFormFieldsConfigConverter(
96
+ bulkEditRegistration?.config.fields,
102
97
  );
103
98
  }
104
99
  return null;
@@ -116,6 +111,8 @@ export const useBulkEdit = <T extends Data>({
116
111
  : undefined
117
112
  }
118
113
  showSaveHeaderAction={!noItemsSelected}
114
+ saveHeaderActionConfig={{ label: 'Apply', icon: IconName.Checkmark }}
115
+ saveNotificationMessage="Your changes are being applied to the selected items."
119
116
  >
120
117
  {BulkEditContent}
121
118
  </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: string;
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. */
@@ -1,4 +1,5 @@
1
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,6 +75,7 @@ export const FieldSelection: React.FC<FieldSelectionProps> = ({
70
75
  ...currentFields,
71
76
  newField,
72
77
  ]);
78
+ onFieldAdded(newField.value);
73
79
  }
74
80
  }}
75
81
  />
@@ -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
  }
@@ -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 } = useFormikContext<FormikValues>();
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: 'Save',
60
- icon: IconName.Save,
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: 'Your changes were saved successfully.',
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, setStationError],
122
+ [isFormSubmitting, initialData, saveData, saveNotificationMessage],
119
123
  );
120
124
 
121
125
  return {