@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.61.0",
3
+ "version": "0.62.0-rc.0",
4
4
  "description": "UI components for building Axinom Mosaic applications",
5
5
  "author": "Axinom",
6
6
  "license": "PROPRIETARY",
@@ -38,6 +38,7 @@
38
38
  "@mui/base": "5.0.0-beta.40",
39
39
  "@popperjs/core": "^2.11.8",
40
40
  "clsx": "^1.1.0",
41
+ "json-to-graphql-query": "^2.3.0",
41
42
  "lodash": "^4.17.21",
42
43
  "luxon": "^3.3.0",
43
44
  "postcss": "^8.4.4",
@@ -76,6 +77,9 @@
76
77
  "@storybook/react-webpack5": "^7.0.6",
77
78
  "@storybook/testing-library": "^0.1.0",
78
79
  "@storybook/theming": "^7.0.6",
80
+ "@testing-library/jest-dom": "^5.16.5",
81
+ "@testing-library/react": "^12.1.5",
82
+ "@testing-library/react-hooks": "^8.0.1",
79
83
  "@types/luxon": "^3.2.0",
80
84
  "@types/react-beautiful-dnd": "^13.1.4",
81
85
  "@types/react-calendar": "^3.1.2",
@@ -108,5 +112,5 @@
108
112
  "publishConfig": {
109
113
  "access": "public"
110
114
  },
111
- "gitHead": "97b214138211f566643e82543e96ffcc5e8e2d5a"
115
+ "gitHead": "6cafaac031044050d3a80103fdfed0aa8fc5c3ab"
112
116
  }
@@ -60,21 +60,23 @@ export const Accordion: React.FC<AccordionProps> = ({
60
60
  const [expanded, setExpanded] = useState<ExpandedState>({});
61
61
 
62
62
  useEffect(() => {
63
- const updatedState: ExpandedState = { ...expanded };
64
- React.Children.map(children, (child, i) => {
65
- if (React.isValidElement(child)) {
66
- if (child.type === AccordionItem) {
67
- const keyString = String(child.key ?? i);
63
+ setExpanded((oldState) => {
64
+ const updatedState: ExpandedState = { ...oldState };
68
65
 
69
- if (!Object.keys(updatedState).includes(keyString)) {
70
- updatedState[keyString] = expandedByDefault;
66
+ React.Children.map(children, (child, i) => {
67
+ if (React.isValidElement(child)) {
68
+ if (child.type === AccordionItem) {
69
+ const keyString = String(child.key ?? i);
70
+
71
+ if (!Object.keys(updatedState).includes(keyString)) {
72
+ updatedState[keyString] = expandedByDefault;
73
+ }
71
74
  }
72
75
  }
73
- }
74
- });
76
+ });
75
77
 
76
- setExpanded(updatedState);
77
- // eslint-disable-next-line react-hooks/exhaustive-deps
78
+ return updatedState;
79
+ });
78
80
  }, [children, expandedByDefault]);
79
81
 
80
82
  useLayoutEffect(() => {
@@ -0,0 +1,21 @@
1
+ export interface BulkEditFieldConfig {
2
+ type: string | { [key: string]: unknown }[];
3
+ label: string;
4
+ originalFieldName: string;
5
+ action: string;
6
+ }
7
+
8
+ export interface BulkEditFieldConfigMap {
9
+ [key: string]: BulkEditFieldConfig;
10
+ }
11
+
12
+ export interface BulkEditConfig {
13
+ mutation: string;
14
+ keys?: {
15
+ add: string;
16
+ remove: string;
17
+ set: string;
18
+ filter: string;
19
+ };
20
+ fields: BulkEditFieldConfigMap;
21
+ }
@@ -0,0 +1,11 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import React from 'react';
3
+
4
+ export interface BulkEditContextType {
5
+ cancel: () => void;
6
+ isBulkEditMode: boolean;
7
+ }
8
+
9
+ export const BulkEditContext = React.createContext<BulkEditContextType>(
10
+ undefined as any,
11
+ );
@@ -0,0 +1,162 @@
1
+ import '@testing-library/jest-dom';
2
+ import { render } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { BulkEditFieldConfigMap } from './BulkEdit.model';
5
+ import { BulkEditFormFieldsConfigConverter } from './FormFieldsConfigConverter';
6
+
7
+ jest.mock('../../FormElements', () => ({
8
+ CheckboxField: jest.fn(({ name, label }) => (
9
+ <div data-testid="CheckboxField">{`${name}-${label}`}</div>
10
+ )),
11
+ CustomTagsField: jest.fn(({ name, label }) => (
12
+ <div data-testid="CustomTagsField">{`${name}-${label}`}</div>
13
+ )),
14
+ SingleLineTextField: jest.fn(({ name, label }) => (
15
+ <div data-testid="SingleLineTextField">{`${name}-${label}`}</div>
16
+ )),
17
+ }));
18
+
19
+ jest.mock('formik', () => ({
20
+ Field: jest.fn(({ name, label, as: Component }) => (
21
+ <div data-testid={`Field-${name}`}>
22
+ <Component name={name} label={label} />
23
+ </div>
24
+ )),
25
+ }));
26
+
27
+ describe('BulkEditFormFieldsConfigConverter', () => {
28
+ it('renders a Formik Field wrapping SingleLineTextField for String type', () => {
29
+ const config: BulkEditFieldConfigMap = {
30
+ title: {
31
+ type: 'String',
32
+ label: 'Title',
33
+ originalFieldName: 'title',
34
+ action: 'set',
35
+ },
36
+ };
37
+
38
+ const { getByTestId } = render(
39
+ <>{BulkEditFormFieldsConfigConverter(config)}</>,
40
+ );
41
+
42
+ expect(getByTestId('Field-title')).toBeInTheDocument();
43
+ expect(getByTestId('SingleLineTextField')).toHaveTextContent('title-Title');
44
+ });
45
+
46
+ it('renders a Formik Field wrapping CheckboxField for Boolean type', () => {
47
+ const config: BulkEditFieldConfigMap = {
48
+ isArchived: {
49
+ type: 'Boolean',
50
+ label: 'Is Archived',
51
+ originalFieldName: 'isArchived',
52
+ action: 'set',
53
+ },
54
+ };
55
+
56
+ const { getByTestId } = render(
57
+ <>{BulkEditFormFieldsConfigConverter(config)}</>,
58
+ );
59
+
60
+ expect(getByTestId('Field-isArchived')).toBeInTheDocument();
61
+ expect(getByTestId('CheckboxField')).toHaveTextContent(
62
+ 'isArchived-Is Archived',
63
+ );
64
+ });
65
+
66
+ it('renders a Formik Field wrapping CustomTagsField for Array type', () => {
67
+ const config: BulkEditFieldConfigMap = {
68
+ tags: {
69
+ type: [
70
+ {
71
+ name: 'String!',
72
+ },
73
+ ],
74
+ label: 'Tags',
75
+ originalFieldName: 'tags',
76
+ action: 'relatedEntitiesToAdd',
77
+ },
78
+ };
79
+
80
+ const { getByTestId } = render(
81
+ <>{BulkEditFormFieldsConfigConverter(config)}</>,
82
+ );
83
+
84
+ expect(getByTestId('Field-tags')).toBeInTheDocument();
85
+ expect(getByTestId('CustomTagsField')).toHaveTextContent('tags-Tags');
86
+ });
87
+
88
+ it('renders multiple Formik Fields for multiple fields', () => {
89
+ const config: BulkEditFieldConfigMap = {
90
+ title: {
91
+ type: 'String',
92
+ label: 'Title',
93
+ originalFieldName: 'title',
94
+ action: 'set',
95
+ },
96
+ isArchived: {
97
+ type: 'Boolean',
98
+ label: 'Is Archived',
99
+ originalFieldName: 'isArchived',
100
+ action: 'set',
101
+ },
102
+ tags: {
103
+ type: [
104
+ {
105
+ name: 'String!',
106
+ },
107
+ ],
108
+ label: 'Tags',
109
+ originalFieldName: 'tags',
110
+ action: 'relatedEntitiesToAdd',
111
+ },
112
+ };
113
+
114
+ const { getByTestId } = render(
115
+ <>{BulkEditFormFieldsConfigConverter(config)}</>,
116
+ );
117
+
118
+ expect(getByTestId('Field-title')).toBeInTheDocument();
119
+ expect(getByTestId('SingleLineTextField')).toHaveTextContent('title-Title');
120
+
121
+ expect(getByTestId('Field-isArchived')).toBeInTheDocument();
122
+ expect(getByTestId('CheckboxField')).toHaveTextContent(
123
+ 'isArchived-Is Archived',
124
+ );
125
+
126
+ expect(getByTestId('Field-tags')).toBeInTheDocument();
127
+ expect(getByTestId('CustomTagsField')).toHaveTextContent('tags-Tags');
128
+ });
129
+
130
+ it('logs a warning for unsupported field types', () => {
131
+ const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
132
+ const config: BulkEditFieldConfigMap = {
133
+ unsupportedField: {
134
+ type: 'UnsupportedType' as any,
135
+ label: 'Unsupported Field',
136
+ originalFieldName: 'unsupportedField',
137
+ action: 'set',
138
+ },
139
+ };
140
+
141
+ const { container } = render(
142
+ <>{BulkEditFormFieldsConfigConverter(config)}</>,
143
+ );
144
+
145
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
146
+ 'No component found for field type: UnsupportedType',
147
+ );
148
+ expect(container.firstChild).toBeNull();
149
+
150
+ consoleWarnSpy.mockRestore();
151
+ });
152
+
153
+ it('handles empty configuration gracefully', () => {
154
+ const config: BulkEditFieldConfigMap = {};
155
+
156
+ const { container } = render(
157
+ <>{BulkEditFormFieldsConfigConverter(config)}</>,
158
+ );
159
+
160
+ expect(container.firstChild).toBeNull();
161
+ });
162
+ });
@@ -0,0 +1,45 @@
1
+ import { Field } from 'formik';
2
+ import React from 'react';
3
+ import {
4
+ CheckboxField,
5
+ CustomTagsField,
6
+ SingleLineTextField,
7
+ } from '../../FormElements';
8
+ import { BulkEditFieldConfigMap } from './BulkEdit.model';
9
+
10
+ export const defaultComponentMap = {
11
+ String: SingleLineTextField,
12
+ BigFloat: SingleLineTextField,
13
+ Boolean: CheckboxField,
14
+ Array: CustomTagsField, // Map array types to CustomTagsComponent
15
+ };
16
+
17
+ export const BulkEditFormFieldsConfigConverter = (
18
+ config: BulkEditFieldConfigMap,
19
+ componentMap: Record<string, React.ElementType> = defaultComponentMap,
20
+ ): JSX.Element[] => {
21
+ const keys = Object.keys(config);
22
+
23
+ return keys
24
+ .map((key) => {
25
+ const fieldConfig = config[key];
26
+
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;
31
+
32
+ const Component = componentMap[fieldType as keyof typeof componentMap];
33
+
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
38
+ }
39
+
40
+ return (
41
+ <Field name={key} key={key} label={fieldConfig.label} as={Component} />
42
+ );
43
+ })
44
+ .filter((element): element is JSX.Element => element !== null);
45
+ };
@@ -0,0 +1,141 @@
1
+ import { BulkEditConfig } from './BulkEdit.model';
2
+ import { generateBulkEditMutation } from './GenerateMutation';
3
+
4
+ describe('generateBulkEditMutation', () => {
5
+ const BulkEditImagesAsyncFormFieldsConfig: BulkEditConfig = {
6
+ mutation: 'bulkEditImagesAsync',
7
+ fields: {
8
+ imagesTagsAdd: {
9
+ type: [
10
+ {
11
+ name: 'String!',
12
+ },
13
+ ],
14
+ label: 'imagesTags ( Add )',
15
+ originalFieldName: 'imagesTags',
16
+ action: 'relatedEntitiesToAdd',
17
+ },
18
+ imagesTagsRemove: {
19
+ type: [
20
+ {
21
+ name: 'String!',
22
+ },
23
+ ],
24
+ label: 'imagesTags ( Remove )',
25
+ originalFieldName: 'imagesTags',
26
+ action: 'relatedEntitiesToRemove',
27
+ },
28
+ altText: {
29
+ type: 'String',
30
+ label: 'altText',
31
+ originalFieldName: 'altText',
32
+ action: 'set',
33
+ },
34
+ focalX: {
35
+ type: 'BigFloat',
36
+ label: 'focalX',
37
+ originalFieldName: 'focalX',
38
+ action: 'set',
39
+ },
40
+ focalY: {
41
+ type: 'BigFloat',
42
+ label: 'focalY',
43
+ originalFieldName: 'focalY',
44
+ action: 'set',
45
+ },
46
+ isArchived: {
47
+ type: 'Boolean',
48
+ label: 'isArchived',
49
+ originalFieldName: 'isArchived',
50
+ action: 'set',
51
+ },
52
+ title: {
53
+ type: 'String',
54
+ label: 'title',
55
+ originalFieldName: 'title',
56
+ action: 'set',
57
+ },
58
+ },
59
+ };
60
+
61
+ it('should generate a mutation string with all fields populated', () => {
62
+ const sampleValues = {
63
+ imagesTagsAdd: ['tag1', 'tag2'],
64
+ imagesTagsRemove: ['tag3', 'tag4'],
65
+ altText: 'Sample Alt Text',
66
+ focalX: 0.5,
67
+ focalY: 0.5,
68
+ isArchived: false,
69
+ title: 'Sample Title',
70
+ };
71
+
72
+ const result = generateBulkEditMutation(
73
+ BulkEditImagesAsyncFormFieldsConfig,
74
+ sampleValues,
75
+ );
76
+
77
+ expect(result).toBe(
78
+ `mutation { bulkEditImagesAsync (relatedEntitiesToAdd: {imagesTags: [{name: "tag1"}, {name: "tag2"}]}, relatedEntitiesToRemove: {imagesTags: [{name: "tag3"}, {name: "tag4"}]}, set: {altText: "Sample Alt Text", focalX: 0.5, focalY: 0.5, isArchived: false, title: "Sample Title"}) { filterMatchedIds } }`,
79
+ );
80
+ });
81
+
82
+ it('should generate a mutation string with only relatedEntitiesToAdd', () => {
83
+ const sampleValues = {
84
+ imagesTagsAdd: ['tag1', 'tag2'],
85
+ };
86
+
87
+ const result = generateBulkEditMutation(
88
+ BulkEditImagesAsyncFormFieldsConfig,
89
+ sampleValues,
90
+ );
91
+
92
+ expect(result).toBe(
93
+ `mutation { bulkEditImagesAsync (relatedEntitiesToAdd: {imagesTags: [{name: "tag1"}, {name: "tag2"}]}) { filterMatchedIds } }`,
94
+ );
95
+ });
96
+
97
+ it('should generate a mutation string with only relatedEntitiesToRemove', () => {
98
+ const sampleValues = {
99
+ imagesTagsRemove: ['tag3', 'tag4'],
100
+ };
101
+
102
+ const result = generateBulkEditMutation(
103
+ BulkEditImagesAsyncFormFieldsConfig,
104
+ sampleValues,
105
+ );
106
+
107
+ expect(result).toBe(
108
+ `mutation { bulkEditImagesAsync (relatedEntitiesToRemove: {imagesTags: [{name: "tag3"}, {name: "tag4"}]}) { filterMatchedIds } }`,
109
+ );
110
+ });
111
+
112
+ it('should generate a mutation string with only set fields', () => {
113
+ const sampleValues = {
114
+ altText: 'Sample Alt Text',
115
+ focalX: 0.5,
116
+ focalY: 0.5,
117
+ isArchived: false,
118
+ title: 'Sample Title',
119
+ };
120
+
121
+ const result = generateBulkEditMutation(
122
+ BulkEditImagesAsyncFormFieldsConfig,
123
+ sampleValues,
124
+ );
125
+
126
+ expect(result).toBe(
127
+ `mutation { bulkEditImagesAsync (set: {altText: "Sample Alt Text", focalX: 0.5, focalY: 0.5, isArchived: false, title: "Sample Title"}) { filterMatchedIds } }`,
128
+ );
129
+ });
130
+
131
+ it('should generate an empty mutation string if no values are provided', () => {
132
+ const sampleValues = {};
133
+
134
+ expect(() =>
135
+ generateBulkEditMutation(
136
+ BulkEditImagesAsyncFormFieldsConfig,
137
+ sampleValues,
138
+ ),
139
+ ).toThrow('No valid fields to generate mutation');
140
+ });
141
+ });
@@ -0,0 +1,90 @@
1
+ import { jsonToGraphQLQuery } from 'json-to-graphql-query';
2
+ import { Data } from '../../../types';
3
+ import { BulkEditConfig } from './BulkEdit.model';
4
+
5
+ export const generateBulkEditMutation = <T extends Data>(
6
+ config: BulkEditConfig,
7
+ values: T,
8
+ filter?: object,
9
+ ): string => {
10
+ const { mutation, fields, keys } = config;
11
+
12
+ const relatedEntitiesToAddKey = keys?.add ?? 'relatedEntitiesToAdd';
13
+ const relatedEntitiesToRemoveKey = keys?.remove ?? 'relatedEntitiesToRemove';
14
+ const setKey = keys?.set ?? 'set';
15
+ const filterKey = keys?.filter ?? 'filter';
16
+
17
+ const relatedEntitiesToAdd = Object.entries(fields)
18
+ .filter(([_, field]) => field.action === relatedEntitiesToAddKey)
19
+ .reduce((acc, [key, field]) => {
20
+ const fieldValues = values[key];
21
+
22
+ if (fieldValues) {
23
+ acc = {
24
+ ...acc,
25
+ [field.originalFieldName]: fieldValues.map((item: string) => ({
26
+ name: item,
27
+ })),
28
+ };
29
+ }
30
+ return acc;
31
+ }, {} as Record<string, unknown>);
32
+
33
+ const relatedEntitiesToRemove = Object.entries(fields)
34
+ .filter(([_, field]) => field.action === relatedEntitiesToRemoveKey)
35
+ .reduce((acc, [key, field]) => {
36
+ const fieldValues = values[key];
37
+
38
+ if (fieldValues) {
39
+ acc = {
40
+ ...acc,
41
+ [field.originalFieldName]: fieldValues.map((item: string) => ({
42
+ name: item,
43
+ })),
44
+ };
45
+ }
46
+ return acc;
47
+ }, {} as Record<string, unknown>);
48
+
49
+ const set = Object.entries(fields)
50
+ .filter(([_, field]) => field.action === setKey)
51
+ .reduce((acc, [key, field]) => {
52
+ const fieldValues = values[key];
53
+
54
+ if (fieldValues !== undefined && fieldValues !== null) {
55
+ acc = {
56
+ ...acc,
57
+ [field.originalFieldName]: fieldValues,
58
+ };
59
+ }
60
+ return acc;
61
+ }, {} as Record<string, unknown>);
62
+
63
+ if (
64
+ Object.keys(relatedEntitiesToAdd).length === 0 &&
65
+ Object.keys(relatedEntitiesToRemove).length === 0 &&
66
+ Object.keys(set).length === 0
67
+ ) {
68
+ throw new Error('No valid fields to generate mutation');
69
+ }
70
+
71
+ return jsonToGraphQLQuery({
72
+ mutation: {
73
+ [mutation]: {
74
+ __args: {
75
+ ...(Object.keys(relatedEntitiesToAdd).length > 0 && {
76
+ [relatedEntitiesToAddKey]: relatedEntitiesToAdd,
77
+ }),
78
+ ...(Object.keys(relatedEntitiesToRemove).length > 0 && {
79
+ [relatedEntitiesToRemoveKey]: relatedEntitiesToRemove,
80
+ }),
81
+ ...(Object.keys(set).length > 0 && {
82
+ [setKey]: set,
83
+ }),
84
+ ...(filter && { [filterKey]: filter }),
85
+ },
86
+ filterMatchedIds: true,
87
+ },
88
+ },
89
+ });
90
+ };
@@ -0,0 +1,8 @@
1
+ export type * from './BulkEdit.model';
2
+ export { BulkEditContext, BulkEditContextType } from './BulkEditContext';
3
+ export {
4
+ BulkEditFormFieldsConfigConverter,
5
+ defaultComponentMap,
6
+ } from './FormFieldsConfigConverter';
7
+ export { generateBulkEditMutation } from './GenerateMutation';
8
+ export { useBulkEdit } from './useBulkEdit';
@@ -0,0 +1,132 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import { Data } from '../../../types';
9
+ import { FieldSelection } from '../../FieldSelection';
10
+ import { FormStation } from '../../FormStation';
11
+ import { IconName } from '../../Icons';
12
+ import {
13
+ BulkEditRegistration,
14
+ ExplorerBulkAction,
15
+ ItemSelection,
16
+ } from '../Explorer.model';
17
+ import { BulkEditContext } from './BulkEditContext';
18
+ import { BulkEditFormFieldsConfigConverter } from './FormFieldsConfigConverter';
19
+
20
+ export interface useBulkEditProps<T extends Data> {
21
+ bulkEditRegistration?: BulkEditRegistration<T>;
22
+ getBulkActionSelection: () => ItemSelection<T>;
23
+ }
24
+
25
+ export interface useBulkEditReturnType<T extends Data> {
26
+ readonly isBulkEditMode: boolean;
27
+ readonly BulkEditComponent: JSX.Element | null;
28
+ readonly BulkEditContextProvider: React.FC;
29
+ readonly bulkEditAction?: ExplorerBulkAction<T>;
30
+ readonly closeBulkEdit: () => void;
31
+ }
32
+
33
+ export const useBulkEdit = <T extends Data>({
34
+ bulkEditRegistration,
35
+ getBulkActionSelection,
36
+ }: useBulkEditProps<T>): useBulkEditReturnType<T> => {
37
+ const [isBulkEditMode, setIsBulkEditMode] = useState<boolean>(false);
38
+ const [noItemsSelected, setNoItemsSelected] = useState<boolean>(false);
39
+
40
+ // Keep selection in a ref to avoid re-renders when the selection changes
41
+ const selection = useRef<ItemSelection<T>>(getBulkActionSelection());
42
+
43
+ useEffect(() => {
44
+ selection.current = getBulkActionSelection();
45
+
46
+ if (
47
+ selection.current?.mode === 'SINGLE_ITEMS' &&
48
+ selection.current.items &&
49
+ selection.current.items.length === 0
50
+ ) {
51
+ setNoItemsSelected(true);
52
+ } else {
53
+ setNoItemsSelected(false);
54
+ }
55
+ }, [getBulkActionSelection]);
56
+
57
+ const BulkEditContextProvider: React.FC = useCallback(
58
+ ({ children }) => {
59
+ return (
60
+ <BulkEditContext.Provider
61
+ value={{
62
+ cancel: () => setIsBulkEditMode(false),
63
+ isBulkEditMode,
64
+ }}
65
+ >
66
+ {children}
67
+ </BulkEditContext.Provider>
68
+ );
69
+ },
70
+ [isBulkEditMode],
71
+ );
72
+
73
+ const bulkEditAction: ExplorerBulkAction<T> | undefined = useMemo(
74
+ () =>
75
+ bulkEditRegistration
76
+ ? {
77
+ label: bulkEditRegistration.label,
78
+ icon: bulkEditRegistration.icon ?? IconName.BulkEdit,
79
+ onClick: () => setIsBulkEditMode((prev) => !prev),
80
+ }
81
+ : undefined,
82
+ [bulkEditRegistration],
83
+ );
84
+
85
+ const handleSave = useCallback(
86
+ async (data) => {
87
+ await bulkEditRegistration?.saveData(data, selection.current);
88
+ },
89
+ [bulkEditRegistration],
90
+ );
91
+
92
+ const BulkEditContent = useMemo(() => {
93
+ if (bulkEditRegistration?.component) {
94
+ return bulkEditRegistration.component;
95
+ } else if (bulkEditRegistration?.config) {
96
+ return (
97
+ <FieldSelection>
98
+ {BulkEditFormFieldsConfigConverter(
99
+ bulkEditRegistration?.config.fields,
100
+ )}
101
+ </FieldSelection>
102
+ );
103
+ }
104
+ return null;
105
+ }, [bulkEditRegistration?.component, bulkEditRegistration?.config]);
106
+
107
+ const BulkEditComponent = useMemo(() => {
108
+ return (
109
+ <FormStation<T>
110
+ defaultTitle="Bulk Edit Properties"
111
+ saveData={handleSave}
112
+ initialData={{ loading: false }}
113
+ stationMessage={
114
+ noItemsSelected
115
+ ? { title: 'No items selected', type: 'warning' }
116
+ : undefined
117
+ }
118
+ showSaveHeaderAction={!noItemsSelected}
119
+ >
120
+ {BulkEditContent}
121
+ </FormStation>
122
+ );
123
+ }, [BulkEditContent, handleSave, noItemsSelected]);
124
+
125
+ return {
126
+ isBulkEditMode,
127
+ BulkEditComponent,
128
+ BulkEditContextProvider,
129
+ bulkEditAction,
130
+ closeBulkEdit: () => setIsBulkEditMode(false),
131
+ };
132
+ };