Skip to content

Commit abc94d5

Browse files
authored
fix(stage-editor): use list editor component in wizard forms COMPASS-6792 (#4349)
1 parent a0bba81 commit abc94d5

File tree

4 files changed

+170
-139
lines changed

4 files changed

+170
-139
lines changed

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/group/group-with-statistics.tsx

Lines changed: 53 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import {
22
css,
33
Body,
4-
Icon,
54
Select,
65
Option,
76
spacing,
8-
IconButton,
7+
ListEditor,
98
ComboboxWithCustomOption,
109
} from '@mongodb-js/compass-components';
1110
import React, { useMemo, useState } from 'react';
@@ -151,20 +150,17 @@ const GroupAccumulatorForm = ({
151150
data: GroupAccumulators[];
152151
onChange: (value: GroupAccumulators[]) => void;
153152
}) => {
154-
const onChangeGroup = (
153+
const onChangeGroup = <T extends keyof GroupAccumulators>(
155154
index: number,
156-
key: keyof GroupAccumulators,
157-
value: string | null
155+
key: T,
156+
value: GroupAccumulators[T]
158157
) => {
159-
if (!value) {
160-
return;
161-
}
162158
const newData = [...data];
163159
newData[index][key] = value;
164160
onChange(newData);
165161
};
166162

167-
const onAddGroup = (at: number) => {
163+
const onAddItem = (at: number) => {
168164
const newData = [...data];
169165
newData.splice(at + 1, 0, {
170166
field: '',
@@ -173,7 +169,7 @@ const GroupAccumulatorForm = ({
173169
onChange(newData);
174170
};
175171

176-
const onRemoveGroup = (at: number) => {
172+
const onRemoveItem = (at: number) => {
177173
const newData = [...data];
178174
newData.splice(at, 1);
179175
onChange(newData);
@@ -186,58 +182,54 @@ const GroupAccumulatorForm = ({
186182

187183
return (
188184
<div className={containerStyles}>
189-
{data.map(({ accumulator, field }, index) => {
190-
return (
191-
<div className={groupRowStyles} key={index}>
192-
<Body className={groupLabelStyles}>
193-
{index === 0 ? 'Calculate' : 'and'}
194-
</Body>
195-
{/* @ts-expect-error leafygreen unresonably expects a labelledby here */}
196-
<Select
197-
className={selectStyles}
198-
allowDeselect={false}
199-
aria-label={'Select accumulator'}
200-
value={accumulator}
201-
onChange={(value: string) =>
202-
onChangeGroup(index, 'accumulator', value)
203-
}
204-
>
205-
{accumulators.map((x, i) => {
206-
return (
207-
<Option value={x.value} key={i}>
208-
{x.label}
209-
</Option>
210-
);
211-
})}
212-
</Select>
213-
<Body>of</Body>
214-
<ComboboxWithCustomOption
215-
className={accumulatorFieldcomboboxStyles}
216-
aria-label={ACCUMULATOR_FIELD_LABEL}
217-
placeholder={ACCUMULATOR_FIELD_LABEL}
218-
size="default"
219-
clearable={false}
220-
value={field}
221-
onChange={(value: string | null) =>
222-
onChangeGroup(index, 'field', value)
223-
}
224-
options={fields}
225-
optionLabel="Field:"
226-
/>
227-
<IconButton aria-label="Add" onClick={() => onAddGroup(index)}>
228-
<Icon glyph="Plus" />
229-
</IconButton>
230-
{data.length > 1 && (
231-
<IconButton
232-
aria-label="Remove"
233-
onClick={() => onRemoveGroup(index)}
185+
<ListEditor
186+
items={data}
187+
onAddItem={(index) => onAddItem(index)}
188+
onRemoveItem={(index) => onRemoveItem(index)}
189+
renderItem={(item, index) => {
190+
return (
191+
<div className={groupRowStyles} key={index}>
192+
<Body className={groupLabelStyles}>
193+
{index === 0 ? 'Calculate' : 'and'}
194+
</Body>
195+
{/* @ts-expect-error leafygreen unresonably expects a labelledby here */}
196+
<Select
197+
className={selectStyles}
198+
allowDeselect={false}
199+
aria-label={'Select accumulator'}
200+
value={item.accumulator}
201+
onChange={(value: string) =>
202+
onChangeGroup(index, 'accumulator', value)
203+
}
234204
>
235-
<Icon glyph="Minus" />
236-
</IconButton>
237-
)}
238-
</div>
239-
);
240-
})}
205+
{accumulators.map((x, i) => {
206+
return (
207+
<Option value={x.value} key={i}>
208+
{x.label}
209+
</Option>
210+
);
211+
})}
212+
</Select>
213+
<Body>of</Body>
214+
<ComboboxWithCustomOption
215+
className={accumulatorFieldcomboboxStyles}
216+
aria-label={ACCUMULATOR_FIELD_LABEL}
217+
placeholder={ACCUMULATOR_FIELD_LABEL}
218+
size="default"
219+
clearable={false}
220+
value={item.field}
221+
onChange={(value: string | null) => {
222+
if (value) {
223+
onChangeGroup(index, 'field', value);
224+
}
225+
}}
226+
options={fields}
227+
optionLabel="Field:"
228+
/>
229+
</div>
230+
);
231+
}}
232+
/>
241233
</div>
242234
);
243235
};

packages/compass-aggregations/src/components/aggregation-side-panel/stage-wizard-use-cases/sort/sort.tsx

Lines changed: 104 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {
22
Select,
33
Option,
4-
IconButton,
5-
Icon,
64
Body,
75
spacing,
86
css,
97
ComboboxWithCustomOption,
8+
ListEditor,
109
} from '@mongodb-js/compass-components';
11-
import React, { useEffect, useMemo, useState } from 'react';
10+
import React, { useMemo, useState } from 'react';
1211
import { SORT_DIRECTION_OPTIONS, mapSortDataToStageValue } from '../utils';
1312

1413
import type { WizardComponentProps } from '..';
@@ -23,6 +22,8 @@ const containerStyles = css({
2322
display: 'flex',
2423
flexDirection: 'column',
2524
gap: spacing[2],
25+
width: 'max-content',
26+
maxWidth: '100%',
2627
});
2728

2829
const formGroupStyles = css({
@@ -47,6 +48,72 @@ const mapSortFormDataToStageValue = (
4748
return mapSortDataToStageValue(formData);
4849
};
4950

51+
const SortFormGroup = ({
52+
index,
53+
comboboxClassName,
54+
fields,
55+
sortField,
56+
sortDirection,
57+
onChange,
58+
}: {
59+
index: number;
60+
comboboxClassName: string;
61+
fields: string[];
62+
sortField: string;
63+
sortDirection: SortDirection;
64+
onChange: <T extends keyof SortFieldState>(
65+
property: T,
66+
value: SortFieldState[T]
67+
) => void;
68+
}) => {
69+
return (
70+
<div className={formGroupStyles} key={`sort-form-${index}`}>
71+
<Body className={labelStyles}>
72+
{index === 0 ? 'Sort documents by' : 'and'}
73+
</Body>
74+
<div data-testid={`sort-form-${index}-field`}>
75+
<ComboboxWithCustomOption
76+
className={comboboxClassName}
77+
aria-label="Select a field"
78+
size="default"
79+
clearable={false}
80+
value={sortField}
81+
onChange={(value: string | null) => {
82+
if (value) {
83+
onChange('field', value);
84+
}
85+
}}
86+
options={fields}
87+
optionLabel="Field:"
88+
// Used for testing to access the popover for a stage
89+
popoverClassName={`sort-form-${index}-field-combobox`}
90+
/>
91+
</div>
92+
<Body>in</Body>
93+
<div data-testid={`sort-form-${index}-direction`}>
94+
{/* @ts-expect-error leafygreen unresonably expects a labelledby here */}
95+
<Select
96+
className={sortDirectionStyles}
97+
allowDeselect={false}
98+
aria-label="Select direction"
99+
value={sortDirection}
100+
onChange={(value: string) =>
101+
onChange('direction', value as SortDirection)
102+
}
103+
>
104+
{SORT_DIRECTION_OPTIONS.map((sort, index) => {
105+
return (
106+
<Option key={index} value={sort.value}>
107+
{sort.label}
108+
</Option>
109+
);
110+
})}
111+
</Select>
112+
</div>
113+
</div>
114+
);
115+
};
116+
50117
export const SortForm = ({ fields, onChange }: WizardComponentProps) => {
51118
const fieldNames = useMemo(() => fields.map(({ name }) => name), [fields]);
52119
const [formData, setFormData] = useState<SortFieldState[]>([
@@ -56,110 +123,71 @@ export const SortForm = ({ fields, onChange }: WizardComponentProps) => {
56123
},
57124
]);
58125

59-
useEffect(() => {
60-
const stageValue = mapSortFormDataToStageValue(formData);
126+
const onSetFormData = (data: SortFieldState[]) => {
127+
const stageValue = mapSortFormDataToStageValue(data);
61128
onChange(
62129
JSON.stringify(stageValue),
63130
Object.keys(stageValue).length === 0
64131
? new Error('No field selected')
65132
: null
66133
);
67-
}, [formData, onChange]);
68134

69-
const onSelectField = (index: number, value: string | null) => {
70-
if (!value) return;
71-
const newFormData = [...formData];
72-
newFormData[index].field = value;
73-
setFormData(newFormData);
135+
setFormData(data);
74136
};
75137

76-
const onSelectDirection = (index: number, value: SortDirection) => {
138+
const onChangeProperty = <T extends keyof SortFieldState>(
139+
index: number,
140+
property: T,
141+
value: SortFieldState[T]
142+
) => {
77143
const newFormData = [...formData];
78-
newFormData[index].direction = value;
79-
setFormData(newFormData);
144+
newFormData[index][property] = value;
145+
onSetFormData(newFormData);
80146
};
81147

82-
const addItem = (at: number) => {
148+
const onAddItem = (at: number) => {
83149
const newData = [...formData];
84150
newData.splice(at + 1, 0, {
85151
field: '',
86152
direction: 'Asc',
87153
});
88-
setFormData(newData);
154+
onSetFormData(newData);
89155
};
90156

91-
const removeItem = (at: number) => {
157+
const onRemoveItem = (at: number) => {
92158
const newData = [...formData];
93159
newData.splice(at, 1);
94-
setFormData(newData);
160+
onSetFormData(newData);
95161
};
96162

97-
const comboboxStyles = useMemo(() => {
98-
return {
163+
const comboboxClassName = useMemo(() => {
164+
return css({
99165
width: `calc(${String(
100166
Math.max(...fieldNames.map((label) => label.length), 10)
101167
)}ch)`,
102-
};
168+
});
103169
}, [fieldNames]);
104170

105171
return (
106172
<div className={containerStyles}>
107-
{formData.map((sort, index: number) => (
108-
<div
109-
className={formGroupStyles}
110-
key={`sort-form-${index}`}
111-
data-testid={`sort-form-${index}`}
112-
>
113-
<Body className={labelStyles}>
114-
{index === 0 ? 'Sort documents by' : 'and'}
115-
</Body>
116-
<div data-testid={`sort-form-${index}-field`}>
117-
<ComboboxWithCustomOption
118-
style={comboboxStyles}
119-
aria-label="Select a field"
120-
size="default"
121-
clearable={false}
122-
value={sort.field}
123-
onChange={(value: string | null) => onSelectField(index, value)}
124-
options={fieldNames}
125-
optionLabel="Field:"
126-
// Used for testing to access the popover for a stage
127-
popoverClassName={`sort-form-${index}-field-combobox`}
173+
<ListEditor
174+
items={formData}
175+
onAddItem={(index) => onAddItem(index)}
176+
onRemoveItem={(index) => onRemoveItem(index)}
177+
itemTestId={(index) => `sort-form-${index}`}
178+
renderItem={(item, index) => {
179+
return (
180+
<SortFormGroup
181+
comboboxClassName={comboboxClassName}
182+
index={index}
183+
sortField={item.field}
184+
sortDirection={item.direction}
185+
fields={fieldNames}
186+
onChange={(prop, value) => onChangeProperty(index, prop, value)}
128187
/>
129-
</div>
130-
<Body>in</Body>
131-
<div data-testid={`sort-form-${index}-direction`}>
132-
{/* @ts-expect-error leafygreen unresonably expects a labelledby here */}
133-
<Select
134-
className={sortDirectionStyles}
135-
allowDeselect={false}
136-
aria-label="Select direction"
137-
usePortal={false}
138-
value={sort.direction}
139-
onChange={(value: string) =>
140-
onSelectDirection(index, value as SortDirection)
141-
}
142-
>
143-
{SORT_DIRECTION_OPTIONS.map((sort, index) => {
144-
return (
145-
<Option key={index} value={sort.value}>
146-
{sort.label}
147-
</Option>
148-
);
149-
})}
150-
</Select>
151-
</div>
152-
<Body>order</Body>
153-
<IconButton aria-label="Add" onClick={() => addItem(index)}>
154-
<Icon glyph="Plus" />
155-
</IconButton>
156-
{formData.length > 1 && (
157-
<IconButton aria-label="Remove" onClick={() => removeItem(index)}>
158-
<Icon glyph="Minus" />
159-
</IconButton>
160-
)}
161-
</div>
162-
))}
188+
);
189+
}}
190+
/>
163191
</div>
164192
);
165193
};

0 commit comments

Comments
 (0)