@axinom/mosaic-ui 0.61.0 → 0.62.0-rc.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.
Files changed (71) hide show
  1. package/dist/components/Accordion/Accordion.d.ts.map +1 -1
  2. package/dist/components/Explorer/BulkEdit/BulkEdit.model.d.ts +22 -0
  3. package/dist/components/Explorer/BulkEdit/BulkEdit.model.d.ts.map +1 -0
  4. package/dist/components/Explorer/BulkEdit/BulkEditContext.d.ts +7 -0
  5. package/dist/components/Explorer/BulkEdit/BulkEditContext.d.ts.map +1 -0
  6. package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts +10 -0
  7. package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts.map +1 -0
  8. package/dist/components/Explorer/BulkEdit/GenerateMutation.d.ts +4 -0
  9. package/dist/components/Explorer/BulkEdit/GenerateMutation.d.ts.map +1 -0
  10. package/dist/components/Explorer/BulkEdit/index.d.ts +6 -0
  11. package/dist/components/Explorer/BulkEdit/index.d.ts.map +1 -0
  12. package/dist/components/Explorer/BulkEdit/useBulkEdit.d.ts +16 -0
  13. package/dist/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -0
  14. package/dist/components/Explorer/Explorer.d.ts +4 -1
  15. package/dist/components/Explorer/Explorer.d.ts.map +1 -1
  16. package/dist/components/Explorer/Explorer.model.d.ts +13 -0
  17. package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
  18. package/dist/components/Explorer/helpers/useActions.d.ts +3 -1
  19. package/dist/components/Explorer/helpers/useActions.d.ts.map +1 -1
  20. package/dist/components/Explorer/helpers/useSubtitle.d.ts +13 -0
  21. package/dist/components/Explorer/helpers/useSubtitle.d.ts.map +1 -0
  22. package/dist/components/Explorer/index.d.ts +1 -0
  23. package/dist/components/Explorer/index.d.ts.map +1 -1
  24. package/dist/components/FieldSelection/FieldSelection.d.ts +7 -0
  25. package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -0
  26. package/dist/components/FieldSelection/index.d.ts +2 -0
  27. package/dist/components/FieldSelection/index.d.ts.map +1 -0
  28. package/dist/components/FormStation/FormStation.d.ts.map +1 -1
  29. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  30. package/dist/components/Icons/Icons.d.ts.map +1 -1
  31. package/dist/components/Icons/Icons.models.d.ts +47 -46
  32. package/dist/components/Icons/Icons.models.d.ts.map +1 -1
  33. package/dist/components/List/List.d.ts.map +1 -1
  34. package/dist/components/index.d.ts +1 -0
  35. package/dist/components/index.d.ts.map +1 -1
  36. package/dist/helpers/testing.d.ts +3 -1
  37. package/dist/helpers/testing.d.ts.map +1 -1
  38. package/dist/index.es.js +4 -4
  39. package/dist/index.es.js.map +1 -1
  40. package/dist/index.js +4 -4
  41. package/dist/index.js.map +1 -1
  42. package/package.json +6 -2
  43. package/src/components/Accordion/Accordion.tsx +13 -11
  44. package/src/components/Explorer/BulkEdit/BulkEdit.model.ts +21 -0
  45. package/src/components/Explorer/BulkEdit/BulkEditContext.tsx +11 -0
  46. package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.spec.tsx +162 -0
  47. package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.tsx +45 -0
  48. package/src/components/Explorer/BulkEdit/GenerateMutation.spec.tsx +141 -0
  49. package/src/components/Explorer/BulkEdit/GenerateMutation.tsx +90 -0
  50. package/src/components/Explorer/BulkEdit/index.ts +8 -0
  51. package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +132 -0
  52. package/src/components/Explorer/Explorer.model.ts +14 -0
  53. package/src/components/Explorer/Explorer.stories.tsx +82 -0
  54. package/src/components/Explorer/Explorer.tsx +41 -57
  55. package/src/components/Explorer/helpers/useActions.ts +21 -5
  56. package/src/components/Explorer/helpers/useFilters.spec.tsx +140 -0
  57. package/src/components/Explorer/helpers/useStationMessage.spec.tsx +91 -0
  58. package/src/components/Explorer/helpers/useSubtitle.spec.tsx +115 -0
  59. package/src/components/Explorer/helpers/useSubtitle.tsx +52 -0
  60. package/src/components/Explorer/index.ts +10 -0
  61. package/src/components/FieldSelection/FieldSelection.scss +18 -0
  62. package/src/components/FieldSelection/FieldSelection.spec.tsx +62 -0
  63. package/src/components/FieldSelection/FieldSelection.stories.tsx +30 -0
  64. package/src/components/FieldSelection/FieldSelection.tsx +154 -0
  65. package/src/components/FieldSelection/index.ts +1 -0
  66. package/src/components/FormStation/FormStation.tsx +8 -4
  67. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +22 -3
  68. package/src/components/Icons/Icons.models.ts +1 -0
  69. package/src/components/Icons/Icons.tsx +17 -0
  70. package/src/components/List/List.tsx +11 -0
  71. package/src/components/index.ts +1 -0
@@ -0,0 +1,115 @@
1
+ import { renderHook } from '@testing-library/react-hooks';
2
+ import { ItemSelectEventArgs, ListSelectMode } from '../../List/List.model';
3
+ import { ResultCounts } from './useDataProvider';
4
+ import { useSubtitle } from './useSubtitle';
5
+
6
+ describe('useSubtitle', () => {
7
+ it('should return "Showing 1 Element" when there is 1 total and 1 filtered result', () => {
8
+ const mode = ListSelectMode.Single;
9
+ const itemSelection: ItemSelectEventArgs<never> = {
10
+ mode: 'SINGLE_ITEMS',
11
+ items: [],
12
+ };
13
+ const results: ResultCounts = { total: 1, filtered: 1 };
14
+
15
+ const { result } = renderHook(() =>
16
+ useSubtitle(mode, itemSelection, results),
17
+ );
18
+
19
+ expect(result.current.resultsTitle).toBe('Showing 1 Element');
20
+ });
21
+
22
+ it('should return "Showing 0 of 1 Element" when there is 1 total and 0 filtered result', () => {
23
+ const mode = ListSelectMode.Single;
24
+ const itemSelection: ItemSelectEventArgs<never> = {
25
+ mode: 'SINGLE_ITEMS',
26
+ items: [],
27
+ };
28
+ const results: ResultCounts = { total: 1, filtered: 0 };
29
+
30
+ const { result } = renderHook(() =>
31
+ useSubtitle(mode, itemSelection, results),
32
+ );
33
+
34
+ expect(result.current.resultsTitle).toBe('Showing 0 of 1 Element');
35
+ });
36
+
37
+ it('should return "Showing all of X Elements" when filtered results equal total results', () => {
38
+ const mode = ListSelectMode.Single;
39
+ const itemSelection: ItemSelectEventArgs<never> = {
40
+ mode: 'SINGLE_ITEMS',
41
+ items: [],
42
+ };
43
+ const results: ResultCounts = { total: 5, filtered: 5 };
44
+
45
+ const { result } = renderHook(() =>
46
+ useSubtitle(mode, itemSelection, results),
47
+ );
48
+
49
+ expect(result.current.resultsTitle).toBe('Showing all of 5 Elements');
50
+ });
51
+
52
+ it('should return "Showing X Elements" when filtered results are undefined', () => {
53
+ const mode = ListSelectMode.Single;
54
+ const itemSelection: ItemSelectEventArgs<never> = {
55
+ mode: 'SINGLE_ITEMS',
56
+ items: [],
57
+ };
58
+ const results: ResultCounts = { total: 5, filtered: undefined };
59
+
60
+ const { result } = renderHook(() =>
61
+ useSubtitle(mode, itemSelection, results),
62
+ );
63
+
64
+ expect(result.current.resultsTitle).toBe('Showing 5 Elements');
65
+ });
66
+
67
+ it('should return "Showing X of Y Elements" when filtered results are less than total results', () => {
68
+ const mode = ListSelectMode.Single;
69
+ const itemSelection: ItemSelectEventArgs<never> = {
70
+ mode: 'SINGLE_ITEMS',
71
+ items: [],
72
+ };
73
+ const results: ResultCounts = { total: 10, filtered: 5 };
74
+
75
+ const { result } = renderHook(() =>
76
+ useSubtitle(mode, itemSelection, results),
77
+ );
78
+
79
+ expect(result.current.resultsTitle).toBe('Showing 5 of 10 Elements');
80
+ });
81
+
82
+ it('should append selected items count when mode is Multi and selection mode is SINGLE_ITEMS', () => {
83
+ const mode = ListSelectMode.Multi;
84
+ const itemSelection: ItemSelectEventArgs<{ id: number }> = {
85
+ mode: 'SINGLE_ITEMS',
86
+ items: [{ id: 1 }, { id: 2 }],
87
+ };
88
+ const results: ResultCounts = { total: 10, filtered: 5 };
89
+
90
+ const { result } = renderHook(() =>
91
+ useSubtitle(mode, itemSelection, results),
92
+ );
93
+
94
+ expect(result.current.resultsTitle).toBe(
95
+ 'Showing 5 of 10 Elements, Selected: 2',
96
+ );
97
+ });
98
+
99
+ it('should append filtered results count as selected items when mode is Multi and selection mode is SELECT_ALL', () => {
100
+ const mode = ListSelectMode.Multi;
101
+ const itemSelection: ItemSelectEventArgs<never> = {
102
+ mode: 'SELECT_ALL',
103
+ items: [],
104
+ };
105
+ const results: ResultCounts = { total: 10, filtered: 5 };
106
+
107
+ const { result } = renderHook(() =>
108
+ useSubtitle(mode, itemSelection, results),
109
+ );
110
+
111
+ expect(result.current.resultsTitle).toBe(
112
+ 'Showing 5 of 10 Elements, Selected: 5',
113
+ );
114
+ });
115
+ });
@@ -0,0 +1,52 @@
1
+ import { Data } from '../../../types';
2
+ import { ItemSelectEventArgs, ListSelectMode } from '../../List/List.model';
3
+ import { ResultCounts } from './useDataProvider';
4
+
5
+ /**
6
+ * Sets PageHeader subtitle
7
+ * @param mode current ListSelectMode
8
+ * @param itemSelection current item selection
9
+ * @param results total results
10
+ */
11
+ export function useSubtitle<T extends Data>(
12
+ mode: ListSelectMode,
13
+ itemSelection: ItemSelectEventArgs<T>,
14
+ results?: ResultCounts,
15
+ ): {
16
+ readonly resultsTitle: string;
17
+ } {
18
+ let resultsTitle = ''; // default to an empty string while results is being fetched
19
+
20
+ if (results !== undefined) {
21
+ switch (true) {
22
+ case results.total === 1 && results.filtered === 1:
23
+ resultsTitle = `Showing 1 Element`;
24
+ break;
25
+ case results.total === 1 && results.filtered === 0:
26
+ resultsTitle = `Showing 0 of 1 Element`;
27
+ break;
28
+ case results.filtered === results.total:
29
+ resultsTitle = `Showing all of ${results.total} Elements`;
30
+ break;
31
+ case results.filtered === undefined:
32
+ resultsTitle = `Showing ${results.total} Elements`;
33
+ break;
34
+ default:
35
+ resultsTitle = `Showing ${results.filtered} of ${results.total} Elements`;
36
+ }
37
+
38
+ // Append Selected items if list selection is in Multi mode
39
+ if (mode === ListSelectMode.Multi) {
40
+ if (itemSelection.mode === 'SINGLE_ITEMS') {
41
+ resultsTitle = `${resultsTitle}, Selected: ${
42
+ itemSelection.items?.length ?? 0
43
+ }`;
44
+ } else {
45
+ // Show filtered results as selected results if 'SELECT_ALL' is active
46
+ resultsTitle = `${resultsTitle}, Selected: ${results.filtered}`;
47
+ }
48
+ }
49
+ }
50
+
51
+ return { resultsTitle } as const;
52
+ }
@@ -1,3 +1,13 @@
1
+ export {
2
+ BulkEditConfig,
3
+ BulkEditContext,
4
+ BulkEditContextType,
5
+ BulkEditFieldConfig,
6
+ BulkEditFieldConfigMap,
7
+ BulkEditFormFieldsConfigConverter,
8
+ defaultComponentMap,
9
+ generateBulkEditMutation,
10
+ } from './BulkEdit';
1
11
  export { Explorer, ExplorerProps } from './Explorer';
2
12
  export {
3
13
  ExplorerBulkAction,
@@ -0,0 +1,18 @@
1
+ @import '../../styles/common.scss';
2
+
3
+ .header {
4
+ display: grid;
5
+ grid-template-columns: 1fr 50px;
6
+ gap: 10px;
7
+ align-items: center;
8
+
9
+ div:has(> input) {
10
+ padding: 2px;
11
+ max-width: none !important;
12
+ width: 100% !important;
13
+ }
14
+ }
15
+
16
+ .content {
17
+ padding: 10px;
18
+ }
@@ -0,0 +1,62 @@
1
+ import '@testing-library/jest-dom';
2
+ import { configure, fireEvent, render, screen } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { FieldSelection } from './FieldSelection';
5
+
6
+ configure({ testIdAttribute: 'data-test-id' });
7
+
8
+ const MockField: React.FC<{ name: string; label: string }> = ({
9
+ name,
10
+ label,
11
+ }) => <div data-test-id={`field-${name}`}>{label}</div>;
12
+
13
+ describe('FieldSelection', () => {
14
+ it('should render without crashing', () => {
15
+ render(
16
+ <FieldSelection>
17
+ <MockField name="field1" label="Field 1" />
18
+ <MockField name="field2" label="Field 2" />
19
+ </FieldSelection>,
20
+ );
21
+
22
+ expect(screen.getByPlaceholderText('Select Field')).toBeInTheDocument();
23
+ });
24
+
25
+ it('should add a field to the selected fields when the add button is clicked', () => {
26
+ render(
27
+ <FieldSelection>
28
+ <MockField name="field1" label="Field 1" />
29
+ <MockField name="field2" label="Field 2" />
30
+ </FieldSelection>,
31
+ );
32
+
33
+ fireEvent.change(screen.getByPlaceholderText('Select Field'), {
34
+ target: { value: 'field1' },
35
+ });
36
+ fireEvent.click(screen.getByText('Field 1'));
37
+ fireEvent.click(screen.getByTestId('add-field-button'));
38
+
39
+ expect(screen.getByTestId('field-field1')).toBeInTheDocument();
40
+ });
41
+
42
+ it('should remove a field from the selected fields when the remove button is clicked', () => {
43
+ render(
44
+ <FieldSelection>
45
+ <MockField name="field1" label="Field 1" />
46
+ <MockField name="field2" label="Field 2" />
47
+ </FieldSelection>,
48
+ );
49
+
50
+ fireEvent.change(screen.getByPlaceholderText('Select Field'), {
51
+ target: { value: 'field1' },
52
+ });
53
+ fireEvent.click(screen.getByText('Field 1'));
54
+ fireEvent.click(screen.getByTestId('add-field-button'));
55
+
56
+ expect(screen.getByTestId('field-field1')).toBeInTheDocument();
57
+
58
+ fireEvent.click(screen.getByTestId('remove-field-button'));
59
+
60
+ expect(screen.queryByTestId('field-field1')).not.toBeInTheDocument();
61
+ });
62
+ });
@@ -0,0 +1,30 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ import { Field, Formik } from 'formik';
3
+ import React from 'react';
4
+ import { noop } from '../../helpers/utils';
5
+ import { CustomTagsField, SingleLineTextField } from '../FormElements';
6
+ import { FieldSelection } from './FieldSelection';
7
+
8
+ const meta: Meta<typeof FieldSelection> = {
9
+ title: 'Other Components/FieldSelection',
10
+ component: FieldSelection,
11
+ };
12
+ export default meta;
13
+
14
+ export const Default: StoryObj<typeof FieldSelection> = {
15
+ render: () => (
16
+ <Formik initialValues={{}} onSubmit={noop}>
17
+ <>
18
+ <FieldSelection>
19
+ <Field label="Title" name={'title'} as={SingleLineTextField} />
20
+ <Field label="Tags ( Add )" name={'tagsAdd'} as={CustomTagsField} />
21
+ <Field
22
+ label="Tags ( Remove )"
23
+ name={'tagsRemove'}
24
+ as={CustomTagsField}
25
+ />
26
+ </FieldSelection>
27
+ </>
28
+ </Formik>
29
+ ),
30
+ };
@@ -0,0 +1,154 @@
1
+ import React, { useCallback, useEffect } from 'react';
2
+ import { Accordion, AccordionItem } from '../Accordion';
3
+ import { Button, ButtonContext } from '../Buttons';
4
+ import { Select } from '../FormElements';
5
+ import { SelectOption } from '../FormElements/Select/Select';
6
+ import { IconName } from '../Icons';
7
+ import classes from './FieldSelection.scss';
8
+
9
+ interface FieldSelectionProps {
10
+ className?: string;
11
+ }
12
+
13
+ interface FieldDefinition {
14
+ index: number;
15
+ value: string;
16
+ label: string;
17
+ child: JSX.Element;
18
+ }
19
+
20
+ export const FieldSelection: React.FC<FieldSelectionProps> = ({
21
+ className,
22
+ children,
23
+ }) => {
24
+ useEffect(() => {
25
+ const fields: FieldDefinition[] = [];
26
+ React.Children.forEach(children, (child, index) => {
27
+ if (
28
+ React.isValidElement<{ label: string; name: string }>(child) &&
29
+ child.props.name
30
+ ) {
31
+ fields.push({
32
+ index,
33
+ value: child.props.name,
34
+ label: child.props.label,
35
+ child: React.cloneElement<{ label: string }>(child, {
36
+ label: 'Value',
37
+ }),
38
+ });
39
+ }
40
+ });
41
+
42
+ setAvailableFields(fields);
43
+ }, [children]);
44
+
45
+ const [availableFields, setAvailableFields] = React.useState<
46
+ FieldDefinition[]
47
+ >([]);
48
+
49
+ const [selectedFields, setSelectedFields] = React.useState<FieldDefinition[]>(
50
+ [],
51
+ );
52
+
53
+ return (
54
+ <Accordion
55
+ className={className}
56
+ expandedByDefault={true}
57
+ header={
58
+ <FieldSelectionHeader
59
+ fields={availableFields}
60
+ onAddField={(value) => {
61
+ const newField = availableFields.find(
62
+ (field) => field.value === value,
63
+ );
64
+
65
+ if (newField) {
66
+ setAvailableFields((currentFields) =>
67
+ currentFields.filter((field) => field.value !== value),
68
+ );
69
+ setSelectedFields((currentFields) => [
70
+ ...currentFields,
71
+ newField,
72
+ ]);
73
+ }
74
+ }}
75
+ />
76
+ }
77
+ >
78
+ {selectedFields.map((field, i) => (
79
+ <AccordionItem
80
+ key={i}
81
+ header={
82
+ <FieldSelectionItemHeader
83
+ label={field.label as string}
84
+ onRemoveField={() => {
85
+ setSelectedFields((currentFields) =>
86
+ currentFields.filter((f) => f.value !== field.value),
87
+ );
88
+ setAvailableFields((currentFields) =>
89
+ [...currentFields, field].sort((a, b) => a.index - b.index),
90
+ );
91
+ }}
92
+ />
93
+ }
94
+ >
95
+ <div className={classes.content}>{field.child}</div>
96
+ </AccordionItem>
97
+ ))}
98
+ </Accordion>
99
+ );
100
+ };
101
+
102
+ const FieldSelectionHeader: React.FC<{
103
+ fields: SelectOption[];
104
+ onAddField: (value: string) => void;
105
+ }> = ({ fields, onAddField }) => {
106
+ const [value, setValue] = React.useState<string>();
107
+
108
+ const handleButtonClicked = useCallback(() => {
109
+ if (value) {
110
+ onAddField(value);
111
+ setValue(undefined);
112
+ }
113
+ }, [onAddField, value]);
114
+
115
+ return (
116
+ <div className={classes.header}>
117
+ <Select
118
+ label="Field"
119
+ name="field"
120
+ placeholder="Select Field"
121
+ options={fields}
122
+ inlineMode={true}
123
+ onChange={(e) => setValue(e.currentTarget.value)}
124
+ value={value}
125
+ />
126
+ <Button
127
+ icon={IconName.Plus}
128
+ buttonContext={value ? ButtonContext.Active : ButtonContext.Icon}
129
+ onButtonClicked={handleButtonClicked}
130
+ dataTestId="add-field-button"
131
+ />
132
+ </div>
133
+ );
134
+ };
135
+
136
+ const FieldSelectionItemHeader: React.FC<{
137
+ label: string;
138
+ onRemoveField: () => void;
139
+ }> = ({ label, onRemoveField }) => {
140
+ return (
141
+ <div className={classes.header}>
142
+ {label}
143
+ <Button
144
+ icon={IconName.X}
145
+ buttonContext={ButtonContext.Icon}
146
+ onButtonClicked={(e) => {
147
+ e.stopPropagation();
148
+ onRemoveField();
149
+ }}
150
+ dataTestId="remove-field-button"
151
+ />
152
+ </div>
153
+ );
154
+ };
@@ -0,0 +1 @@
1
+ export { FieldSelection } from './FieldSelection';
@@ -3,6 +3,7 @@ import { Formik, FormikValues } from 'formik';
3
3
  import React, { PropsWithChildren, useContext } from 'react';
4
4
  import { OptionalObjectSchema } from 'yup/lib/object';
5
5
  import { Data } from '../../types/data';
6
+ import { BulkEditContext } from '../Explorer/BulkEdit/BulkEditContext';
6
7
  import { QuickEditContext } from '../Explorer/QuickEdit/QuickEditContext';
7
8
  import { StationMessage } from '../models';
8
9
  import {
@@ -94,6 +95,7 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
94
95
  FormStationProps<TValues, TSubmitResponse>
95
96
  >): JSX.Element => {
96
97
  const quickEditContext = useContext(QuickEditContext);
98
+ const bulkEditContext = useContext(BulkEditContext);
97
99
 
98
100
  const {
99
101
  onSubmit,
@@ -136,10 +138,12 @@ export const FormStation = <TValues extends Data, TSubmitResponse = unknown>({
136
138
  setTabTitle={setTabTitle}
137
139
  showSaveHeaderAction={showSaveHeaderAction}
138
140
  />
139
- <SaveOnNavigate
140
- isSubmitting={isFormSubmitting}
141
- onNavigationCancelled={setValidationError}
142
- />
141
+ {!bulkEditContext && (
142
+ <SaveOnNavigate
143
+ isSubmitting={isFormSubmitting}
144
+ onNavigationCancelled={setValidationError}
145
+ />
146
+ )}
143
147
  {quickEditContext && (
144
148
  <SaveOnDemand
145
149
  isSubmitting={isFormSubmitting}
@@ -1,7 +1,8 @@
1
1
  import { FormikValues, useFormikContext } from 'formik';
2
2
  import React, { useContext, useMemo } from 'react';
3
3
  import { useHistory } from 'react-router-dom';
4
- import { QuickEditContext } from '../../Explorer';
4
+ import { BulkEditContext } from '../../Explorer/BulkEdit/BulkEditContext';
5
+ import { QuickEditContext } from '../../Explorer/QuickEdit/QuickEditContext';
5
6
  import { IconName } from '../../Icons';
6
7
  import {
7
8
  PageHeader,
@@ -31,8 +32,9 @@ export const FormStationHeader: React.FC<
31
32
  setTabTitle,
32
33
  showSaveHeaderAction,
33
34
  }) => {
34
- const { dirty, resetForm } = useFormikContext<FormikValues>();
35
+ const { dirty, submitForm, resetForm } = useFormikContext<FormikValues>();
35
36
  const quickEditContext = useContext(QuickEditContext);
37
+ const bulkEditContext = useContext(BulkEditContext);
36
38
 
37
39
  const history = useHistory();
38
40
 
@@ -58,9 +60,12 @@ export const FormStationHeader: React.FC<
58
60
  icon: IconName.Save,
59
61
  kind: 'action',
60
62
  actionType: PageHeaderActionType.Context,
61
- onClick: () => {
63
+ onClick: async () => {
62
64
  if (quickEditContext?.isQuickEditMode) {
63
65
  quickEditContext.refresh();
66
+ } else if (bulkEditContext?.isBulkEditMode) {
67
+ await submitForm();
68
+ history.replace(history.location.pathname);
64
69
  } else {
65
70
  history.replace(history.location.pathname);
66
71
  }
@@ -78,6 +83,18 @@ export const FormStationHeader: React.FC<
78
83
  });
79
84
  }
80
85
 
86
+ if (bulkEditContext?.isBulkEditMode) {
87
+ actionItems.push({
88
+ label: 'Cancel',
89
+ icon: IconName.X,
90
+ kind: 'action',
91
+ onClick: () => {
92
+ resetForm();
93
+ bulkEditContext.cancel();
94
+ },
95
+ });
96
+ }
97
+
81
98
  if (cancelNavigationUrl) {
82
99
  actionItems.push({
83
100
  label: 'Cancel',
@@ -93,12 +110,14 @@ export const FormStationHeader: React.FC<
93
110
 
94
111
  return actionItems;
95
112
  }, [
113
+ bulkEditContext,
96
114
  cancelNavigationUrl,
97
115
  dirty,
98
116
  history,
99
117
  quickEditContext,
100
118
  resetForm,
101
119
  showSaveHeaderAction,
120
+ submitForm,
102
121
  ]);
103
122
 
104
123
  return (
@@ -9,6 +9,7 @@ export enum IconName {
9
9
  BackwardTen,
10
10
  Block,
11
11
  Bulk,
12
+ BulkEdit,
12
13
  Calendar,
13
14
  Checkmark,
14
15
  ChevronDown,
@@ -129,6 +129,22 @@ const BulkIcon: React.FC<{ className?: string }> = ({ className }) => (
129
129
  </svg>
130
130
  );
131
131
 
132
+ const BulkEditIcon: React.FC<{ className?: string }> = ({ className }) => (
133
+ <svg
134
+ className={clsx(classes.icons, className)}
135
+ version="1.1"
136
+ xmlns="http://www.w3.org/2000/svg"
137
+ viewBox="0 0 40 40"
138
+ >
139
+ <path
140
+ vectorEffect="non-scaling-stroke"
141
+ fill="none"
142
+ strokeWidth="2"
143
+ d="M32.7,18.5l4.4,4.4-10.5,10.5-4.4-4.4,10.5-10.5ZM32.2,12.9v-7.7H10M8.5,16.5h12.9M8.5,21h12.9M8.5,25.5h7M27.2,18.5v-8.6H2.9v24.7h14.4M22.3,29l-1.6,5.8,6-1.4"
144
+ />
145
+ </svg>
146
+ );
147
+
132
148
  const CalendarIcon: React.FC<{ className?: string }> = ({ className }) => (
133
149
  <svg
134
150
  className={clsx(classes.icons, className)}
@@ -915,6 +931,7 @@ export const Icons: React.FC<IconsProps> = ({ icon, className }) => {
915
931
  [IconName.BackwardTen]: <BackwardTen className={className} />,
916
932
  [IconName.Block]: <BlockIcon className={className} />,
917
933
  [IconName.Bulk]: <BulkIcon className={className} />,
934
+ [IconName.BulkEdit]: <BulkEditIcon className={className} />,
918
935
  [IconName.Calendar]: <CalendarIcon className={className} />,
919
936
  [IconName.Checkmark]: <CheckmarkIcon className={className} />,
920
937
  [IconName.ChevronDown]: <ChevronDownIcon className={className} />,
@@ -279,6 +279,17 @@ const ListRenderer = <T extends Data>(
279
279
  },
280
280
  }));
281
281
 
282
+ useEffect(() => {
283
+ if (selectionMode !== ListSelectMode.Multi) {
284
+ setListItems((prevState) =>
285
+ prevState.map((i) => ({
286
+ selected: false,
287
+ data: i.data,
288
+ })),
289
+ );
290
+ }
291
+ }, [selectionMode]);
292
+
282
293
  return (
283
294
  <div
284
295
  className={clsx(classes.wrapper, 'list-wrapper', className)}
@@ -5,6 +5,7 @@ export * from './Buttons';
5
5
  export * from './DynamicDataList';
6
6
  export * from './EmptyStation';
7
7
  export * from './Explorer';
8
+ export * from './FieldSelection';
8
9
  export * from './Filters';
9
10
  export * from './FormElements';
10
11
  export * from './FormStation';