@axinom/mosaic-ui 0.63.0-rc.3 → 0.63.0-rc.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/Accordion/Accordion.d.ts.map +1 -1
- package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts +1 -1
- package/dist/components/Explorer/BulkEdit/FormFieldsConfigConverter.d.ts.map +1 -1
- package/dist/components/Explorer/BulkEdit/useBulkEdit.d.ts.map +1 -1
- package/dist/components/Explorer/Explorer.model.d.ts +1 -1
- package/dist/components/Explorer/Explorer.model.d.ts.map +1 -1
- package/dist/components/FieldSelection/FieldSelection.d.ts +2 -0
- package/dist/components/FieldSelection/FieldSelection.d.ts.map +1 -1
- package/dist/components/FormStation/FormStation.d.ts +11 -1
- package/dist/components/FormStation/FormStation.d.ts.map +1 -1
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts +1 -0
- package/dist/components/FormStation/FormStationHeader/FormStationHeader.d.ts.map +1 -1
- package/dist/components/FormStation/helpers/useDataProvider.d.ts +1 -1
- package/dist/components/FormStation/helpers/useDataProvider.d.ts.map +1 -1
- package/dist/index.es.js +4 -4
- package/dist/index.es.js.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Accordion/Accordion.scss +14 -4
- package/src/components/Accordion/Accordion.spec.tsx +44 -64
- package/src/components/Accordion/Accordion.stories.tsx +8 -0
- package/src/components/Accordion/Accordion.tsx +4 -0
- package/src/components/Accordion/AccordionItem/AccordionItem.spec.tsx +46 -84
- package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.spec.tsx +22 -18
- package/src/components/Explorer/BulkEdit/FormFieldsConfigConverter.tsx +59 -21
- package/src/components/Explorer/BulkEdit/useBulkEdit.tsx +5 -8
- package/src/components/Explorer/Explorer.model.ts +1 -1
- package/src/components/FieldSelection/FieldSelection.tsx +7 -0
- package/src/components/FormStation/FormStation.tsx +14 -1
- package/src/components/FormStation/FormStationHeader/FormStationHeader.tsx +4 -1
- package/src/components/FormStation/helpers/useDataProvider.ts +6 -2
- package/src/styles/variables.scss +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axinom/mosaic-ui",
|
|
3
|
-
"version": "0.63.0-rc.
|
|
3
|
+
"version": "0.63.0-rc.5",
|
|
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": "
|
|
115
|
+
"gitHead": "61dacf2b863f576ca0680aaa3431b5a195686e7e"
|
|
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
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
expect(wrapper).toBeTruthy();
|
|
13
|
-
});
|
|
11
|
+
render(<Accordion />);
|
|
14
12
|
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
45
|
+
// All items should be expanded (content visible)
|
|
46
|
+
let rows = screen.getAllByTestId('accordion-item-content');
|
|
47
|
+
expect(rows).toHaveLength(3);
|
|
74
48
|
|
|
75
|
-
|
|
76
|
-
expect(
|
|
49
|
+
rows.forEach((row) => {
|
|
50
|
+
expect(row).toHaveClass('expanded');
|
|
77
51
|
});
|
|
78
52
|
|
|
79
|
-
|
|
53
|
+
fireEvent.click(headerButton);
|
|
80
54
|
|
|
81
|
-
items
|
|
55
|
+
// All items should be collapsed (content not visible)
|
|
56
|
+
rows = screen.getAllByTestId('accordion-item-content');
|
|
57
|
+
expect(rows).toHaveLength(3);
|
|
82
58
|
|
|
83
|
-
|
|
84
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
expect(
|
|
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
|
-
|
|
83
|
+
// Expand the last item
|
|
84
|
+
const lastItemButton = screen.getAllByRole('button')[2];
|
|
85
|
+
fireEvent.click(lastItemButton);
|
|
109
86
|
|
|
110
|
-
|
|
87
|
+
// Now the header button should have the rotated class
|
|
88
|
+
expect(headerButton).toHaveClass('rotated');
|
|
111
89
|
|
|
112
|
-
|
|
90
|
+
// Collapse the last item
|
|
91
|
+
fireEvent.click(lastItemButton);
|
|
113
92
|
|
|
114
|
-
|
|
115
|
-
expect(
|
|
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
|
-
|
|
97
|
+
it('disables the expand/collapse all button when there are no children', () => {
|
|
98
|
+
render(<Accordion header={<b>Header</b>} />);
|
|
120
99
|
|
|
121
|
-
|
|
122
|
-
|
|
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,6 +58,7 @@ 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) => {
|
|
@@ -110,8 +111,11 @@ export const Accordion: React.FC<AccordionProps> = ({
|
|
|
110
111
|
}}
|
|
111
112
|
className={clsx(classes.button, {
|
|
112
113
|
[classes.rotated]: expandAll.isExpanded,
|
|
114
|
+
[classes.disabled]: disabled,
|
|
113
115
|
})}
|
|
114
116
|
buttonContext={ButtonContext.None}
|
|
117
|
+
disabled={disabled}
|
|
118
|
+
dataTestId="accordion-toggle-all"
|
|
115
119
|
></Button>
|
|
116
120
|
{header}
|
|
117
121
|
</div>
|
|
@@ -1,111 +1,73 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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('
|
|
23
|
-
const
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
expect(content).toHaveLength(0);
|
|
25
|
+
fireEvent.click(screen.getByTestId('accordion-item-row'));
|
|
26
|
+
expect(toggleExpanded).toHaveBeenCalled();
|
|
30
27
|
});
|
|
31
28
|
|
|
32
|
-
it('
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
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(
|
|
62
|
-
|
|
63
|
-
<AccordionItem header={
|
|
64
|
-
|
|
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
|
-
|
|
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(
|
|
78
|
-
|
|
79
|
-
<AccordionItem header={
|
|
80
|
-
|
|
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
|
-
|
|
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(
|
|
90
|
-
|
|
91
|
-
<AccordionItem header={
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
159
|
+
const { container, getByTestId } = render(
|
|
160
|
+
BulkEditFormFieldsConfigConverter(config),
|
|
158
161
|
);
|
|
159
162
|
|
|
160
|
-
expect(
|
|
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,65 @@ export const defaultComponentMap = {
|
|
|
17
19
|
export const BulkEditFormFieldsConfigConverter = (
|
|
18
20
|
config: BulkEditFieldConfigMap,
|
|
19
21
|
componentMap: Record<string, React.ElementType> = defaultComponentMap,
|
|
20
|
-
): JSX.Element
|
|
22
|
+
): JSX.Element => {
|
|
21
23
|
const keys = Object.keys(config);
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const fieldConfig = config[key];
|
|
25
|
+
const FormFields: React.FC = () => {
|
|
26
|
+
const { setFieldValue, setFieldTouched, values } = useFormikContext<Data>();
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const onFieldRemoved = (field: string): void => {
|
|
29
|
+
setFieldValue(field, undefined); // Clear the field value when removed
|
|
30
|
+
setFieldTouched(field, false); // Mark the field as not touched
|
|
31
|
+
};
|
|
31
32
|
|
|
32
|
-
|
|
33
|
+
// Effect to clear empty fields
|
|
34
|
+
// This will set fields with empty strings or empty arrays to undefined
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
values &&
|
|
37
|
+
Object.keys(values).forEach((key) => {
|
|
38
|
+
if (values[key] === '' || values[key].length === 0) {
|
|
39
|
+
setFieldValue(key, undefined);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}, [setFieldValue, values]);
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
const fields = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
keys
|
|
47
|
+
.map((key) => {
|
|
48
|
+
const fieldConfig = config[key];
|
|
39
49
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
// Determine the type of the field
|
|
51
|
+
const fieldType = Array.isArray(fieldConfig.type)
|
|
52
|
+
? 'Array' // Use 'Array' as the key for array types
|
|
53
|
+
: fieldConfig.type;
|
|
54
|
+
|
|
55
|
+
const Component =
|
|
56
|
+
componentMap[fieldType as keyof typeof componentMap];
|
|
57
|
+
|
|
58
|
+
if (!Component) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.warn(`No component found for field type: ${fieldType}`);
|
|
61
|
+
return null; // Filter out null entries later
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<Field
|
|
66
|
+
name={key}
|
|
67
|
+
key={key}
|
|
68
|
+
label={fieldConfig.label}
|
|
69
|
+
as={Component}
|
|
70
|
+
/>
|
|
71
|
+
);
|
|
72
|
+
})
|
|
73
|
+
.filter((element): element is JSX.Element => element !== null),
|
|
74
|
+
[],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<FieldSelection onFieldRemoved={onFieldRemoved}>{fields}</FieldSelection>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return <FormFields />; // Return the FormFields component
|
|
45
83
|
};
|
|
@@ -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
|
-
|
|
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
|
+
saveHeaderActionTitle="Apply"
|
|
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
|
|
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. */
|