@axinom/mosaic-ui 0.63.0-rc.1 → 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.
Files changed (60) hide show
  1. package/dist/components/Accordion/Accordion.d.ts.map +1 -1
  2. package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts +1 -1
  3. package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts.map +1 -1
  4. package/dist/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
  5. package/dist/components/Explorer/Explorer.d.ts.map +1 -1
  6. package/dist/components/Explorer/Explorer.model.d.ts +1 -1
  7. package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
  8. package/dist/components/FieldSelection/FieldSelection.d.ts +2 -0
  9. package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -1
  10. package/dist/components/FormElements/DateTimeField/DateTimeText.d.ts +1 -1
  11. package/dist/components/FormElements/DateTimeField/DateTimeText.d.ts.map +1 -1
  12. package/dist/components/FormElements/DateTimeField/DateTimeTextField.d.ts.map +1 -1
  13. package/dist/components/FormStation/FormStation.d.ts +12 -1
  14. package/dist/components/FormStation/FormStation.d.ts.map +1 -1
  15. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +3 -0
  16. package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
  17. package/dist/components/FormStation/helpers/useDataProvider.d.ts +1 -1
  18. package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
  19. package/dist/components/Icons/Icons.d.ts.map +1 -1
  20. package/dist/components/Icons/Icons.models.d.ts +2 -1
  21. package/dist/components/Icons/Icons.models.d.ts.map +1 -1
  22. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.d.ts.map +1 -1
  23. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts +2 -0
  24. package/dist/components/PageHeader/PageHeaderAction/PageHeaderAction.model.d.ts.map +1 -1
  25. package/dist/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.d.ts.map +1 -1
  26. package/dist/index.es.js +4 -4
  27. package/dist/index.es.js.map +1 -1
  28. package/dist/index.js +4 -4
  29. package/dist/index.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/components/Accordion/Accordion.scss +14 -4
  32. package/src/components/Accordion/Accordion.spec.tsx +44 -64
  33. package/src/components/Accordion/Accordion.stories.tsx +8 -0
  34. package/src/components/Accordion/Accordion.tsx +9 -3
  35. package/src/components/Accordion/AccordionItem/AccordionItem.spec.tsx +46 -84
  36. package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.spec.tsx +22 -18
  37. package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.tsx +94 -20
  38. package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +6 -8
  39. package/src/components/Explorer/Explorer.model.ts +1 -1
  40. package/src/components/Explorer/Explorer.tsx +1 -0
  41. package/src/components/Explorer/helpers/useActions.ts +1 -1
  42. package/src/components/FieldSelection/FieldSelection.scss +7 -0
  43. package/src/components/FieldSelection/FieldSelection.spec.tsx +0 -2
  44. package/src/components/FieldSelection/FieldSelection.tsx +13 -11
  45. package/src/components/FormElements/DateTimeField/DateTimeText.tsx +8 -14
  46. package/src/components/FormElements/DateTimeField/DateTimeTextField.tsx +18 -5
  47. package/src/components/FormStation/Create/Create.stories.tsx +9 -0
  48. package/src/components/FormStation/FormStation.stories.tsx +2 -2
  49. package/src/components/FormStation/FormStation.tsx +16 -1
  50. package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +20 -3
  51. package/src/components/FormStation/helpers/useDataProvider.ts +6 -2
  52. package/src/components/Icons/Icons.models.ts +1 -0
  53. package/src/components/Icons/Icons.tsx +17 -0
  54. package/src/components/PageHeader/PageHeader.stories.tsx +8 -0
  55. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.model.ts +2 -0
  56. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.scss +28 -0
  57. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.spec.tsx +0 -10
  58. package/src/components/PageHeader/PageHeaderAction/PageHeaderAction.tsx +4 -3
  59. package/src/components/PageHeader/PageHeaderActionsGroup/PageHeaderActionsGroup.tsx +5 -1
  60. package/src/styles/variables.scss +3 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axinom/mosaic-ui",
3
- "version": "0.63.0-rc.1",
3
+ "version": "0.63.0-rc.10",
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": "6425b41d87ac24779772f202eb544e2574d9f614"
115
+ "gitHead": "1190cb91471f96c64d9c8c49574a4963f7b560c8"
116
116
  }
@@ -42,6 +42,7 @@
42
42
 
43
43
  .button {
44
44
  transition: transform 200ms ease;
45
+
45
46
  svg {
46
47
  height: 40%;
47
48
  }
@@ -49,10 +50,19 @@
49
50
  svg * {
50
51
  stroke: var(--accordion-item-arrow-color, $accordion-item-arrow-color);
51
52
  }
53
+
54
+ &.disabled {
55
+ svg * {
56
+ stroke: var(
57
+ --accordion-item-arrow-disabled-color,
58
+ $accordion-item-arrow-disabled-color
59
+ );
60
+ }
61
+ }
62
+
63
+ &.rotated {
64
+ transform: rotate(90deg);
65
+ }
52
66
  }
53
67
  }
54
68
  }
55
-
56
- .rotated {
57
- transform: rotate(90deg);
58
- }
@@ -1,59 +1,30 @@
1
- import { mount, shallow } from 'enzyme';
1
+ import '@testing-library/jest-dom';
2
+ import { configure, fireEvent, render, screen } from '@testing-library/react';
2
3
  import React from 'react';
3
- import { Button } from '../Buttons';
4
- import { IconName } from '../Icons';
5
4
  import { Accordion } from './Accordion';
6
5
  import { AccordionItem } from './AccordionItem/AccordionItem';
7
6
 
7
+ configure({ testIdAttribute: 'data-test-id' });
8
+
8
9
  describe('Accordion', () => {
9
10
  it('renders the component without crashing', () => {
10
- const wrapper = shallow(<Accordion />);
11
-
12
- expect(wrapper).toBeTruthy();
13
- });
11
+ render(<Accordion />);
14
12
 
15
- it('contains a wrapper', () => {
16
- const wrapper = shallow(<Accordion header={<b>Header</b>} />);
17
-
18
- const accWrapper = wrapper.find('.container > div');
19
-
20
- expect(accWrapper.hasClass('wrapper')).toBe(true);
13
+ expect(screen.getByTestId('accordion')).toBeInTheDocument();
21
14
  });
22
15
 
23
16
  it('renders the component header', () => {
24
- const wrapper = shallow(<Accordion header={<b>Header</b>} />);
25
-
26
- expect(wrapper.find('.header').exists()).toBeTruthy();
17
+ render(<Accordion header={<b>Header</b>} />);
18
+ expect(screen.getByTestId('accordion-header')).toBeInTheDocument();
27
19
  });
28
20
 
29
21
  it('does not render a header when the prop is not provided', () => {
30
- const wrapper = shallow(<Accordion />);
31
-
32
- expect(wrapper.find('.header').exists()).toBeFalsy();
33
- });
34
-
35
- it('populates toggleExpanded and isExpanded props of child AccordionItem components', () => {
36
- const wrapper = mount(
37
- <Accordion header={<b>Header</b>}>
38
- <div id="extra"></div>
39
- <AccordionItem header={<b>Item 1</b>}>
40
- <p>Content 1</p>
41
- </AccordionItem>
42
- </Accordion>,
43
- );
44
-
45
- const item = wrapper.find(AccordionItem);
46
- const extra = wrapper.find('#extra');
47
-
48
- expect(item.prop('isExpanded')).toBeDefined();
49
- expect(item.prop('toggleExpanded')).toBeDefined();
50
-
51
- expect(extra.prop('isExpanded')).toBeUndefined();
52
- expect(extra.prop('toggleExpanded')).toBeUndefined();
22
+ render(<Accordion />);
23
+ expect(screen.queryByTestId('accordion-header')).not.toBeInTheDocument();
53
24
  });
54
25
 
55
26
  it('expands / collapses all children when the button is clicked', () => {
56
- const wrapper = mount(
27
+ render(
57
28
  <Accordion header={<b>Header</b>}>
58
29
  <AccordionItem header={<b>Item 1</b>}>
59
30
  <p>Content 1</p>
@@ -67,26 +38,31 @@ describe('Accordion', () => {
67
38
  </Accordion>,
68
39
  );
69
40
 
70
- const button = wrapper.find('.header .button').first();
71
- button.simulate('click');
41
+ // Find the expand/collapse all button (assuming it's the first button in header)
42
+ const headerButton = screen.getByTestId('accordion-toggle-all');
43
+ fireEvent.click(headerButton);
72
44
 
73
- let items = wrapper.find(AccordionItem);
45
+ // All items should be expanded (content visible)
46
+ let rows = screen.getAllByTestId('accordion-item-content');
47
+ expect(rows).toHaveLength(3);
74
48
 
75
- items.forEach((item) => {
76
- expect(item.prop('isExpanded')).toBe(true);
49
+ rows.forEach((row) => {
50
+ expect(row).toHaveClass('expanded');
77
51
  });
78
52
 
79
- button.simulate('click');
53
+ fireEvent.click(headerButton);
80
54
 
81
- items = wrapper.find(AccordionItem);
55
+ // All items should be collapsed (content not visible)
56
+ rows = screen.getAllByTestId('accordion-item-content');
57
+ expect(rows).toHaveLength(3);
82
58
 
83
- items.forEach((item) => {
84
- expect(item.prop('isExpanded')).toBe(false);
59
+ rows.forEach((row) => {
60
+ expect(row).not.toHaveClass('expanded');
85
61
  });
86
62
  });
87
63
 
88
64
  it('displays rotated ChevronRight icon when one or more children are expanded', () => {
89
- const wrapper = mount(
65
+ render(
90
66
  <Accordion header={<b>Header</b>}>
91
67
  <AccordionItem header={<b>Item 1</b>}>
92
68
  <p>Content 1</p>
@@ -100,25 +76,29 @@ describe('Accordion', () => {
100
76
  </Accordion>,
101
77
  );
102
78
 
103
- let button = wrapper.find(Button).first();
104
-
105
- expect(button.prop('icon')).toBe(IconName.ChevronRight);
106
- expect(button.hasClass('rotated')).toBe(false);
79
+ // The button in the header should have the ChevronRight icon and not be rotated initially
80
+ const headerButton = screen.getByTestId('accordion-toggle-all');
81
+ expect(headerButton.querySelector('.rotated')).not.toBeInTheDocument();
107
82
 
108
- const item = wrapper.find(AccordionItem).last();
83
+ // Expand the last item
84
+ const lastItemButton = screen.getAllByRole('button')[2];
85
+ fireEvent.click(lastItemButton);
109
86
 
110
- item.find('button').simulate('click');
87
+ // Now the header button should have the rotated class
88
+ expect(headerButton).toHaveClass('rotated');
111
89
 
112
- button = wrapper.find(Button).first();
90
+ // Collapse the last item
91
+ fireEvent.click(lastItemButton);
113
92
 
114
- expect(button.prop('icon')).toBe(IconName.ChevronRight);
115
- expect(button.hasClass('rotated')).toBe(true);
116
-
117
- item.find('button').simulate('click');
93
+ // The header button should not have the rotated class anymore
94
+ expect(headerButton).not.toHaveClass('rotated');
95
+ });
118
96
 
119
- button = wrapper.find(Button).first();
97
+ it('disables the expand/collapse all button when there are no children', () => {
98
+ render(<Accordion header={<b>Header</b>} />);
120
99
 
121
- expect(button.prop('icon')).toBe(IconName.ChevronRight);
122
- expect(button.hasClass('rotated')).toBe(false);
100
+ // The button should be disabled when there are no children
101
+ const headerButton = screen.getByTestId('accordion-toggle-all');
102
+ expect(headerButton).toBeDisabled();
123
103
  });
124
104
  });
@@ -145,3 +145,11 @@ export const NestedSticky: StoryObj<typeof Accordion> = {
145
145
  },
146
146
  name: 'Nested Sticky',
147
147
  };
148
+
149
+ export const NoChildren: StoryObj<typeof Accordion> = {
150
+ args: {
151
+ ...Default.args,
152
+ children: undefined,
153
+ },
154
+ name: 'No children',
155
+ };
@@ -58,18 +58,21 @@ export const Accordion: React.FC<AccordionProps> = ({
58
58
  }) => {
59
59
  const expandAll = useExpand();
60
60
  const [expanded, setExpanded] = useState<ExpandedState>({});
61
+ const disabled = !React.Children.count(children);
61
62
 
62
63
  useEffect(() => {
63
64
  setExpanded((oldState) => {
64
- const updatedState: ExpandedState = { ...oldState };
65
+ const updatedState: ExpandedState = {};
65
66
 
66
- React.Children.map(children, (child, i) => {
67
+ React.Children.forEach(children, (child, i) => {
67
68
  if (React.isValidElement(child)) {
68
69
  if (child.type === AccordionItem) {
69
70
  const keyString = String(child.key ?? i);
70
71
 
71
- if (!Object.keys(updatedState).includes(keyString)) {
72
+ if (!Object.keys(oldState).includes(keyString)) {
72
73
  updatedState[keyString] = expandedByDefault;
74
+ } else {
75
+ updatedState[keyString] = oldState[keyString];
73
76
  }
74
77
  }
75
78
  }
@@ -110,8 +113,11 @@ export const Accordion: React.FC<AccordionProps> = ({
110
113
  }}
111
114
  className={clsx(classes.button, {
112
115
  [classes.rotated]: expandAll.isExpanded,
116
+ [classes.disabled]: disabled,
113
117
  })}
114
118
  buttonContext={ButtonContext.None}
119
+ disabled={disabled}
120
+ dataTestId="accordion-toggle-all"
115
121
  ></Button>
116
122
  {header}
117
123
  </div>
@@ -1,111 +1,73 @@
1
- import { mount, shallow } from 'enzyme';
1
+ import '@testing-library/jest-dom';
2
+ import { configure, fireEvent, render, screen } from '@testing-library/react';
2
3
  import React from 'react';
3
4
  import { AccordionItem } from './AccordionItem';
4
5
 
5
- describe('AccordionItem', () => {
6
- it('renders the component without crashing', () => {
7
- const wrapper = shallow(<AccordionItem header={<b>Item 1</b>} />);
8
-
9
- expect(wrapper).toBeTruthy();
10
- });
6
+ configure({ testIdAttribute: 'data-test-id' });
11
7
 
12
- it('renders expanded content', () => {
13
- const wrapper = shallow(
14
- <AccordionItem header={<b>Item 1</b>} isExpanded={true} />,
15
- );
16
-
17
- const content = wrapper.find('.expanded');
8
+ describe('AccordionItem', () => {
9
+ const header = <span>Header</span>;
10
+ const content = <div>Accordion Content</div>;
18
11
 
19
- expect(content).toHaveLength(1);
12
+ it('renders header and children', () => {
13
+ render(<AccordionItem header={header}>{content}</AccordionItem>);
14
+ expect(screen.getByText('Header')).toBeInTheDocument();
15
+ expect(screen.getByText('Accordion Content')).toBeInTheDocument();
20
16
  });
21
17
 
22
- it('does not render expanded content when isExpanded is false', () => {
23
- const wrapper = shallow(
24
- <AccordionItem header={<b>Item 1</b>} isExpanded={false} />,
18
+ it('calls toggleExpanded when row is clicked', () => {
19
+ const toggleExpanded = jest.fn();
20
+ render(
21
+ <AccordionItem header={header} toggleExpanded={toggleExpanded}>
22
+ {content}
23
+ </AccordionItem>,
25
24
  );
26
-
27
- const content = wrapper.find('.expanded');
28
-
29
- expect(content).toHaveLength(0);
25
+ fireEvent.click(screen.getByTestId('accordion-item-row'));
26
+ expect(toggleExpanded).toHaveBeenCalled();
30
27
  });
31
28
 
32
- it('triggers toggleExpanded callback when the expand button is pressed', () => {
33
- const spy = jest.fn();
34
- const wrapper = mount(
35
- <AccordionItem header={<b>Item 1</b>} toggleExpanded={spy} />,
36
- );
37
-
38
- const button = wrapper.find('button');
39
-
40
- button.simulate('click');
41
-
42
- expect(spy).toHaveBeenCalledTimes(1);
43
- });
44
-
45
- it('adds 50px spacing element by default', () => {
46
- const wrapper = shallow(
47
- <AccordionItem header={<b>Item 1</b>}>
48
- <div></div>
29
+ it('shows expanded styles and rotates icon when isExpanded is true', () => {
30
+ render(
31
+ <AccordionItem header={header} isExpanded>
32
+ {content}
49
33
  </AccordionItem>,
50
34
  );
51
-
52
- const contentStyles = wrapper
53
- .find('.content')
54
- .prop('style') as React.CSSProperties;
55
- const children = wrapper.find('.content > div');
56
-
57
- expect(contentStyles.gridTemplateColumns).toBeUndefined();
58
- expect(children).toHaveLength(2);
35
+ const row = screen.getByTestId('accordion-item-row');
36
+ expect(row.className).toMatch(/rowExpanded/);
37
+ const button = row.querySelector('button');
38
+ expect(button?.className).toMatch(/rotated/);
39
+ const contentDiv = screen.getByTestId('accordion-item-content');
40
+ expect(contentDiv.className).toMatch(/expanded/);
59
41
  });
60
42
 
61
- it(`doesn't add 50px spacing element if 'allowLeftSpacing' is set to false`, () => {
62
- const wrapper = shallow(
63
- <AccordionItem header={<b>Item 1</b>} allowLeftSpacing={false}>
64
- <div></div>
43
+ it('applies sticky class when sticky is true', () => {
44
+ render(
45
+ <AccordionItem header={header} sticky>
46
+ {content}
65
47
  </AccordionItem>,
66
48
  );
67
-
68
- const contentStyles = wrapper
69
- .find('.content')
70
- .prop('style') as React.CSSProperties;
71
- const children = wrapper.find('.content > div');
72
-
73
- expect(contentStyles.gridTemplateColumns).toBe('1fr');
74
- expect(children).toHaveLength(1);
49
+ const row = screen.getByTestId('accordion-item-row');
50
+ expect(row.className).toMatch(/sticky/);
75
51
  });
76
52
 
77
- it(`should set expanded class when 'isExpanded' is true`, () => {
78
- const wrapper = mount(
79
- <AccordionItem header={<b>Item 1</b>} isExpanded={true}>
80
- <div></div>
53
+ it('does not add left spacing when allowLeftSpacing is false', () => {
54
+ render(
55
+ <AccordionItem header={header} allowLeftSpacing={false}>
56
+ {content}
81
57
  </AccordionItem>,
82
58
  );
83
-
84
- const expanded = wrapper.find('.expanded');
85
-
86
- expect(expanded).toHaveLength(1);
59
+ const contentDiv = screen.getByText('Accordion Content').parentElement;
60
+ expect(contentDiv).toHaveStyle('grid-template-columns: 1fr');
87
61
  });
88
62
 
89
- it(`should not set expanded class when 'isExpanded' is true`, () => {
90
- const wrapper = mount(
91
- <AccordionItem header={<b>Item 1</b>} isExpanded={false}>
92
- <div></div>
63
+ it('applies custom className to root element', () => {
64
+ render(
65
+ <AccordionItem header={header} className="custom-class">
66
+ {content}
93
67
  </AccordionItem>,
94
68
  );
95
-
96
- const expanded = wrapper.find('.expanded');
97
-
98
- expect(expanded).toHaveLength(0);
99
- });
100
-
101
- it('creates a class based off of the className prop', () => {
102
- const mockClassName = 'test-class';
103
- const wrapper = shallow(
104
- <AccordionItem header={<b>Item 1</b>} className={mockClassName} />,
69
+ expect(screen.getByTestId('accordion-item').className).toContain(
70
+ 'custom-class',
105
71
  );
106
-
107
- const item = wrapper.find('.test-class');
108
-
109
- expect(item.hasClass(mockClassName)).toBe(true);
110
72
  });
111
73
  });
@@ -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
  };