Skip to content

Commit 712ec92

Browse files
Feat(ui) : Enhance UI input method for multiple filter conditions when configuring ingestion pipelines (#21392)
* support to enter multiple filter conditions at a time in select input * added unit test * added copy paste functionality * fix copy styles * update paste functionality * update the parsing and added test * minor fix * fix for pasting duplicate values * address pr comments * minor fix --------- Co-authored-by: Shailesh Parmar <[email protected]>
1 parent 60b7f16 commit 712ec92

File tree

7 files changed

+345
-5
lines changed

7 files changed

+345
-5
lines changed
Lines changed: 3 additions & 0 deletions
Loading

openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/WorkflowArrayFieldTemplate.test.tsx

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
*/
1313

1414
import { FieldProps, IdSchema, Registry } from '@rjsf/utils';
15-
import { fireEvent, render, screen } from '@testing-library/react';
15+
import { fireEvent, render, screen, within } from '@testing-library/react';
1616
import { t } from 'i18next';
1717
import React from 'react';
1818
import { MOCK_WORKFLOW_ARRAY_FIELD_TEMPLATE } from '../../../../../mocks/Templates.mock';
@@ -146,4 +146,66 @@ describe('Test WorkflowArrayFieldTemplate Component', () => {
146146

147147
expect(placeholderText).toHaveTextContent('');
148148
});
149+
150+
it('Should call onChange with correct value when comma seperated values are entered', async () => {
151+
render(
152+
<WorkflowArrayFieldTemplate
153+
{...mockWorkflowArrayFieldTemplateProps}
154+
formData={[]}
155+
/>
156+
);
157+
158+
const select = screen.getByTestId('workflow-array-field-template');
159+
const input = within(select).getByRole('combobox');
160+
161+
// Test basic comma-separated values
162+
fireEvent.change(input, { target: { value: 'value1,value2,value3' } });
163+
fireEvent.keyDown(input, { key: 'Enter' });
164+
165+
expect(mockWorkflowArrayFieldTemplateProps.onChange).toHaveBeenCalledWith([
166+
'value1',
167+
'value2',
168+
'value3',
169+
]);
170+
171+
// Test values with spaces
172+
fireEvent.change(input, {
173+
target: { value: 'value 1, value 2 , value 3 ' },
174+
});
175+
fireEvent.keyDown(input, { key: 'Enter' });
176+
177+
expect(mockWorkflowArrayFieldTemplateProps.onChange).toHaveBeenCalledWith([
178+
'value 1',
179+
'value 2',
180+
'value 3',
181+
]);
182+
183+
// Test empty values
184+
fireEvent.change(input, { target: { value: 'value1,,value3' } });
185+
fireEvent.keyDown(input, { key: 'Enter' });
186+
187+
expect(mockWorkflowArrayFieldTemplateProps.onChange).toHaveBeenCalledWith([
188+
'value1',
189+
'',
190+
'value3',
191+
]);
192+
193+
// Test value with comma
194+
fireEvent.change(input, { target: { value: 'value1,"value,e3"' } });
195+
fireEvent.keyDown(input, { key: 'Enter' });
196+
197+
expect(mockWorkflowArrayFieldTemplateProps.onChange).toHaveBeenCalledWith([
198+
'value1',
199+
'value,e3',
200+
]);
201+
202+
// Test value which has double quotes in the middle
203+
fireEvent.change(input, { target: { value: `Test1,"random,\\"abc\\""` } });
204+
fireEvent.keyDown(input, { key: 'Enter' });
205+
206+
expect(mockWorkflowArrayFieldTemplateProps.onChange).toHaveBeenCalledWith([
207+
'Test1',
208+
'random,"abc"',
209+
]);
210+
});
149211
});

openmetadata-ui/src/main/resources/ui/src/components/common/Form/JSONSchema/JSONSchemaTemplate/WorkflowArrayFieldTemplate.tsx

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
1212
*/
1313

1414
import { FieldProps } from '@rjsf/utils';
15-
import { Col, Row, Select, Typography } from 'antd';
15+
import { Button, Col, Row, Select, Tooltip, Typography } from 'antd';
1616
import { t } from 'i18next';
17-
import { isArray, isObject, startCase } from 'lodash';
18-
import React from 'react';
17+
import { isArray, isEmpty, isObject, startCase } from 'lodash';
18+
import React, { useCallback } from 'react';
19+
import { ReactComponent as CopyLeft } from '../../../../../assets/svg/copy-left.svg';
20+
import { useClipboard } from '../../../../../hooks/useClipBoard';
21+
import { splitCSV } from '../../../../../utils/CSV/CSV.utils';
22+
import './workflow-array-field-template.less';
1923

2024
const WorkflowArrayFieldTemplate = (props: FieldProps) => {
2125
const isFilterPatternField = (id: string) => {
@@ -53,6 +57,57 @@ const WorkflowArrayFieldTemplate = (props: FieldProps) => {
5357
? t('message.filter-pattern-placeholder')
5458
: '';
5559
const options = generateOptions();
60+
const { onCopyToClipBoard, onPasteFromClipBoard, hasCopied } = useClipboard(
61+
JSON.stringify(value)
62+
);
63+
64+
const handleCopy = useCallback(
65+
async (e: React.MouseEvent) => {
66+
e.stopPropagation();
67+
await onCopyToClipBoard();
68+
},
69+
[value, onCopyToClipBoard]
70+
);
71+
72+
const handlePaste = useCallback(async () => {
73+
const text = await onPasteFromClipBoard();
74+
if (!text) {
75+
return;
76+
}
77+
78+
let processedValues: string[] = [];
79+
80+
// Try to parse as JSON array
81+
try {
82+
const parsed = JSON.parse(text);
83+
if (Array.isArray(parsed)) {
84+
processedValues = parsed.map((item) => String(item));
85+
}
86+
} catch {
87+
// Fallback to existing logic if not a JSON array
88+
processedValues = splitCSV(text);
89+
}
90+
91+
if (!isEmpty(processedValues)) {
92+
// Use Set to ensure unique values
93+
const uniqueValues = Array.from(new Set([...value, ...processedValues]));
94+
props.onChange(uniqueValues);
95+
}
96+
}, [onPasteFromClipBoard, props.onChange, value]);
97+
98+
const handleInputSplit = useCallback(
99+
(inputValue: string) => {
100+
if (isEmpty(inputValue)) {
101+
return;
102+
}
103+
const processedValues = splitCSV(inputValue);
104+
105+
// Use Set to ensure unique values
106+
const uniqueValues = Array.from(new Set([...value, ...processedValues]));
107+
props.onChange(uniqueValues);
108+
},
109+
[value, props.onChange]
110+
);
56111

57112
return (
58113
<Row>
@@ -63,7 +118,7 @@ const WorkflowArrayFieldTemplate = (props: FieldProps) => {
63118
<Typography>{startCase(props.name)}</Typography>
64119
)}
65120
</Col>
66-
<Col span={24}>
121+
<Col className="select-container" span={24}>
67122
<Select
68123
className="m-t-xss w-full"
69124
data-testid="workflow-array-field-template"
@@ -77,7 +132,38 @@ const WorkflowArrayFieldTemplate = (props: FieldProps) => {
77132
onBlur={() => props.onBlur(id, value)}
78133
onChange={(value) => props.onChange(value)}
79134
onFocus={handleFocus}
135+
onKeyDown={(e) => {
136+
if ((e.ctrlKey || e.metaKey) && e.key === 'v' && !options) {
137+
e.preventDefault();
138+
handlePaste();
139+
}
140+
if (!options && e.key === 'Enter') {
141+
// If there are no options(mode is tags), we need to split the input value by comma
142+
e.preventDefault();
143+
const inputValue = (e.target as HTMLInputElement).value;
144+
if (inputValue) {
145+
handleInputSplit(inputValue);
146+
}
147+
}
148+
}}
80149
/>
150+
151+
<div className="workflow-array-field-divider" />
152+
<Tooltip
153+
overlayClassName="custom-tooltip"
154+
placement="top"
155+
title={hasCopied ? 'Copied to clipboard' : 'Copy'}>
156+
<Button
157+
className="workflow-array-field-copy-button remove-button-default-styling"
158+
icon={<CopyLeft height={20} />}
159+
size="small"
160+
type="text"
161+
onClick={(e) => {
162+
e.stopPropagation();
163+
handleCopy(e);
164+
}}
165+
/>
166+
</Tooltip>
81167
</Col>
82168
</Row>
83169
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2025 Collate.
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
* Unless required by applicable law or agreed to in writing, software
8+
* distributed under the License is distributed on an "AS IS" BASIS,
9+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
* See the License for the specific language governing permissions and
11+
* limitations under the License.
12+
*/
13+
@import (reference) url('../../../../../styles/variables.less');
14+
15+
.workflow-array-field {
16+
&-container {
17+
position: relative;
18+
}
19+
20+
&-divider {
21+
position: absolute;
22+
right: 36px;
23+
top: 0;
24+
min-height: 32px;
25+
height: 100%;
26+
border-left: 1px solid @grey-22;
27+
margin-top: @size-xxs;
28+
}
29+
30+
&-copy-button {
31+
position: absolute;
32+
right: 0;
33+
&.ant-btn-icon-only.ant-btn-sm {
34+
padding: 10px 0px;
35+
text-align: center;
36+
&:hover,
37+
&:focus,
38+
&:active {
39+
svg path {
40+
stroke: @grey-500;
41+
}
42+
}
43+
}
44+
}
45+
}
46+
.select-container {
47+
position: relative;
48+
overflow: hidden;
49+
.ant-select-selection-overflow {
50+
padding-right: @size-md;
51+
}
52+
}
53+
.custom-tooltip {
54+
.ant-tooltip-inner {
55+
padding: 4px 8px;
56+
font-size: 12px;
57+
min-height: auto;
58+
background-color: @grey-900;
59+
color: @white;
60+
font-weight: 600;
61+
margin-top: -8px;
62+
}
63+
64+
&.ant-tooltip {
65+
padding: 0;
66+
}
67+
&.ant-tooltip-placement-top {
68+
padding-bottom: 0;
69+
}
70+
}

openmetadata-ui/src/main/resources/ui/src/hooks/useClipBoard.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ export const useClipboard = (
3939
}
4040
}, [valueState]);
4141

42+
const handlePaste = useCallback(async () => {
43+
try {
44+
const text = await navigator.clipboard.readText();
45+
46+
return text;
47+
} catch (error) {
48+
return null;
49+
}
50+
}, []);
51+
4252
// side effects
4353
useEffect(() => setValueState(value), [value]);
4454

@@ -62,6 +72,7 @@ export const useClipboard = (
6272

6373
return {
6474
onCopyToClipBoard: handleCopy,
75+
onPasteFromClipBoard: handlePaste,
6576
hasCopied,
6677
};
6778
};

openmetadata-ui/src/main/resources/ui/src/utils/CSV/CSV.utils.test.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
getColumnConfig,
2525
getCSVStringFromColumnsAndDataSource,
2626
getEntityColumnsAndDataSourceFromCSV,
27+
splitCSV,
2728
} from './CSV.utils';
2829

2930
describe('CSVUtils', () => {
@@ -174,4 +175,79 @@ describe('CSVUtils', () => {
174175
expect(convertedCSVEntities).toStrictEqual(`dateCp:undefined`);
175176
});
176177
});
178+
179+
describe('splitCSV', () => {
180+
it('should split simple CSV string correctly', () => {
181+
const input = 'value1,value2,value3';
182+
const result = splitCSV(input);
183+
184+
expect(result).toEqual(['value1', 'value2', 'value3']);
185+
});
186+
187+
it('should handle quoted values with commas', () => {
188+
const input = 'value1,"value,2",value3';
189+
const result = splitCSV(input);
190+
191+
expect(result).toEqual(['value1', 'value,2', 'value3']);
192+
});
193+
194+
it('should handle escaped quotes within quoted values', () => {
195+
const input = 'value1,"value "quoted" here",value3';
196+
const result = splitCSV(input);
197+
198+
expect(result).toEqual(['value1', 'value "quoted" here', 'value3']);
199+
});
200+
201+
it('should handle empty values', () => {
202+
const input = 'value1,,value3';
203+
const result = splitCSV(input);
204+
205+
expect(result).toEqual(['value1', '', 'value3']);
206+
});
207+
208+
it('should handle values with spaces', () => {
209+
const input = ' value1 , value2 , value3 ';
210+
const result = splitCSV(input);
211+
212+
expect(result).toEqual(['value1', 'value2', 'value3']);
213+
});
214+
215+
it('should handle empty string input', () => {
216+
const input = '';
217+
const result = splitCSV(input);
218+
219+
expect(result).toEqual([]);
220+
});
221+
222+
it('should handle complex quoted values with multiple commas', () => {
223+
const input = '"value,1,2,3","another,value","last,value"';
224+
const result = splitCSV(input);
225+
226+
expect(result).toEqual(['value,1,2,3', 'another,value', 'last,value']);
227+
});
228+
229+
it('should convert numbers to strings', () => {
230+
const input = '1,2,3,4,5';
231+
const result = splitCSV(input);
232+
233+
expect(result).toEqual(['1', '2', '3', '4', '5']);
234+
235+
// Verify each value is a string
236+
result.forEach((value) => {
237+
expect(typeof value).toBe('string');
238+
});
239+
});
240+
241+
it('should handle mixed number and string values', () => {
242+
const input = '1,hello,3,world,5';
243+
const result = splitCSV(input);
244+
245+
expect(result).toEqual(['1', 'hello', '3', 'world', '5']);
246+
247+
// Verify each value is a string
248+
result.forEach((value) => {
249+
expect(typeof value).toBe('string');
250+
});
251+
});
252+
});
177253
});

0 commit comments

Comments
 (0)