Skip to content

Commit d23c27e

Browse files
feat: Use search attributes in start form (#1055)
* Search attribute component Signed-off-by: Assem Hafez <[email protected]> * Fix type check Signed-off-by: Assem Hafez <[email protected]> * revert errors to have key and value Signed-off-by: Assem Hafez <[email protected]> * Address the nits * change button label * Create hook for fetching search attributes Signed-off-by: Assem Hafez <[email protected]> * Refactor error message helpers * feat: Create hook for fetching search attributes (#1046) * Create hook for fetching search attributes Signed-off-by: Assem Hafez <[email protected]> * use stringifyUrl Signed-off-by: Assem Hafez <[email protected]> --------- Signed-off-by: Assem Hafez <[email protected]> * fix: propagate user headers between internal api requests (#1049) * propagate user headers from server renders * fix tests * dynamically import headers * log propagated header keys * add test cases * import headers inline * Add import/named to lint rules (#1050) *Add import/named rule to .eslintrc to enable detecting import issues * Use deep imports for Lodash * Fix other misc lint issues (unused imports/variables) Signed-off-by: Adhitya Mamallan <[email protected]> * remove type Signed-off-by: Assem Hafez <[email protected]> * lint fixes Signed-off-by: Assem Hafez <[email protected]> * align multi json add button design with search attributes * add parsing for double Signed-off-by: Assem Hafez <[email protected]> * Add search attributes to start form Signed-off-by: Assem Hafez <[email protected]> * move label to form Signed-off-by: Assem Hafez <[email protected]> * change test case for search attributes validation Signed-off-by: Assem Hafez <[email protected]> * change test case for search attributes validation Signed-off-by: Assem Hafez <[email protected]> --------- Signed-off-by: Assem Hafez <[email protected]> Signed-off-by: Adhitya Mamallan <[email protected]> Co-authored-by: Adhitya Mamallan <[email protected]>
1 parent 2db5cb5 commit d23c27e

13 files changed

+367
-130
lines changed

src/route-handlers/start-workflow/schemas/start-workflow-request-body-schema.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ const startWorkflowRequestBodySchema = z.object({
3333
})
3434
.optional(),
3535
memo: z.record(z.any()).optional(),
36-
searchAttributes: z.record(z.any()).optional(),
36+
searchAttributes: z
37+
.record(z.union([z.string(), z.number(), z.boolean()]))
38+
.optional(),
3739
header: z.record(z.string()).optional(),
3840
});
3941

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import getSearchAttributesErrorMessage from '../get-search-attributes-error-message';
2+
3+
describe('getSearchAttributesErrorMessage', () => {
4+
const FIELD_NAME = 'searchAttributes';
5+
6+
it('should return undefined when field does not exist', () => {
7+
const fieldErrors = {};
8+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
9+
10+
expect(result).toBeUndefined();
11+
});
12+
13+
it('should handle array of search attribute errors with key and value', () => {
14+
const fieldErrors = {
15+
[FIELD_NAME]: [
16+
{
17+
key: { message: 'Key is required' },
18+
value: { message: 'Value is required' },
19+
},
20+
{
21+
key: { message: 'Invalid key format' },
22+
value: { message: 'Invalid value format' },
23+
},
24+
],
25+
};
26+
27+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
28+
29+
expect(result).toEqual([
30+
{ key: 'Key is required', value: 'Value is required' },
31+
{ key: 'Invalid key format', value: 'Invalid value format' },
32+
]);
33+
});
34+
35+
it('should handle array with only key errors', () => {
36+
const fieldErrors = {
37+
[FIELD_NAME]: [
38+
{
39+
key: { message: 'Key is required' },
40+
},
41+
{
42+
key: { message: 'Invalid key format' },
43+
},
44+
],
45+
};
46+
47+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
48+
49+
expect(result).toEqual([
50+
{ key: 'Key is required' },
51+
{ key: 'Invalid key format' },
52+
]);
53+
});
54+
55+
it('should handle array with only value errors', () => {
56+
const fieldErrors = {
57+
[FIELD_NAME]: [
58+
{
59+
value: { message: 'Value is required' },
60+
},
61+
{
62+
value: { message: 'Invalid value format' },
63+
},
64+
],
65+
};
66+
67+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
68+
69+
expect(result).toEqual([
70+
{ value: 'Value is required' },
71+
{ value: 'Invalid value format' },
72+
]);
73+
});
74+
75+
it('should handle array with null/undefined elements', () => {
76+
const fieldErrors = {
77+
[FIELD_NAME]: [
78+
{
79+
key: { message: 'Key error' },
80+
value: { message: 'Value error' },
81+
},
82+
null,
83+
undefined,
84+
{
85+
key: { message: 'Another key error' },
86+
},
87+
],
88+
};
89+
90+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
91+
92+
expect(result).toEqual([
93+
{ key: 'Key error', value: 'Value error' },
94+
{},
95+
{},
96+
{ key: 'Another key error' },
97+
]);
98+
});
99+
100+
it('should handle single error object with message property', () => {
101+
const fieldErrors = {
102+
[FIELD_NAME]: { message: 'Single error message' },
103+
};
104+
105+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
106+
107+
expect(result).toBe('Single error message');
108+
});
109+
110+
it('should handle empty array', () => {
111+
const fieldErrors = {
112+
[FIELD_NAME]: [],
113+
};
114+
115+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
116+
117+
expect(result).toEqual([]);
118+
});
119+
120+
it('should handle empty object without message property', () => {
121+
const fieldErrors = {
122+
[FIELD_NAME]: {},
123+
};
124+
125+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
126+
127+
expect(result).toBeUndefined();
128+
});
129+
130+
it('should handle non-string message values gracefully', () => {
131+
const fieldErrors = {
132+
[FIELD_NAME]: [
133+
{
134+
key: { message: 123 }, // Non-string message
135+
value: { message: 'Valid message' },
136+
},
137+
],
138+
};
139+
140+
const result = getSearchAttributesErrorMessage(fieldErrors, FIELD_NAME);
141+
142+
expect(result).toEqual([{ value: 'Valid message' }]);
143+
});
144+
145+
it('should handle nested field paths', () => {
146+
const fieldErrors = {
147+
nested: {
148+
deep: {
149+
field: [
150+
{
151+
key: { message: 'Nested key error' },
152+
value: { message: 'Nested value error' },
153+
},
154+
],
155+
},
156+
},
157+
};
158+
159+
const result = getSearchAttributesErrorMessage(
160+
fieldErrors,
161+
'nested.deep.field'
162+
);
163+
164+
expect(result).toEqual([
165+
{ key: 'Nested key error', value: 'Nested value error' },
166+
]);
167+
});
168+
});

src/views/workflow-actions/workflow-action-start-form/helpers/__tests__/transform-start-workflow-form-to-submission.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,10 @@ describe('transformStartWorkflowFormToSubmission', () => {
232232
it('should parse searchAttributes JSON', () => {
233233
const formData: StartWorkflowFormData = {
234234
...baseFormData,
235-
searchAttributes: '{"attr1": "value1", "attr2": 456}',
235+
searchAttributes: [
236+
{ key: 'attr1', value: 'value1' },
237+
{ key: 'attr2', value: 456 },
238+
],
236239
};
237240

238241
const result = transformStartWorkflowFormToSubmission(formData);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import get from 'lodash/get';
2+
3+
/**
4+
* Extracts error messages from search attributes field errors.
5+
*/
6+
export default function getSearchAttributesErrorMessage(
7+
fieldErrors: Record<string, any>,
8+
fieldName: string
9+
): string | Partial<Record<'key' | 'value', string>>[] | undefined {
10+
const fieldError = get(fieldErrors, fieldName);
11+
if (!fieldError) {
12+
return undefined;
13+
}
14+
15+
if (Array.isArray(fieldError)) {
16+
return fieldError.map((err) => {
17+
const errorObj: Record<string, string> = {};
18+
19+
if (err && typeof err === 'object') {
20+
if ('key' in err && err.key && typeof err.key === 'object') {
21+
if ('message' in err.key && typeof err.key.message === 'string') {
22+
errorObj.key = err.key.message;
23+
}
24+
}
25+
26+
if ('value' in err && err.value && typeof err.value === 'object') {
27+
if ('message' in err.value && typeof err.value.message === 'string') {
28+
errorObj.value = err.value.message;
29+
}
30+
}
31+
}
32+
33+
return errorObj;
34+
});
35+
}
36+
37+
// Handle single error object with message property
38+
if (
39+
typeof fieldError === 'object' &&
40+
'message' in fieldError &&
41+
typeof fieldError.message === 'string'
42+
) {
43+
return fieldError.message;
44+
}
45+
46+
return undefined;
47+
}

src/views/workflow-actions/workflow-action-start-form/helpers/transform-start-workflow-form-to-submission.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,21 @@ export default function transformStartWorkflowFormToSubmission(
5353
}),
5454
};
5555

56+
const searchAttributesObject =
57+
formData?.searchAttributes && formData.searchAttributes.length > 0
58+
? Object.fromEntries(
59+
formData.searchAttributes.map((item) => [item.key, item.value])
60+
)
61+
: undefined;
62+
5663
return {
5764
...basicFormData,
5865
...conditionalFormData,
5966
input: formData?.input
6067
?.filter((v) => v !== '')
6168
.map((v) => JSON.parse(v) as Json),
6269
memo: formData?.memo ? JSON.parse(formData?.memo) : undefined,
63-
searchAttributes: formData?.searchAttributes
64-
? JSON.parse(formData?.searchAttributes)
65-
: undefined,
70+
searchAttributes: searchAttributesObject,
6671
header: formData?.header ? JSON.parse(formData?.header) : undefined,
6772
};
6873
}

src/views/workflow-actions/workflow-action-start-form/schemas/start-workflow-form-schema.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,18 @@ const baseSchema = z.object({
5555
}
5656
}, 'Memo must be valid JSON Object'),
5757
searchAttributes: z
58-
.string()
59-
.optional()
60-
.refine((val) => {
61-
if (!val || val.trim() === '') return true;
62-
try {
63-
JSON.parse(val);
64-
return true;
65-
} catch {
66-
return false;
67-
}
68-
}, 'Search Attributes must be valid JSON Object'),
58+
.array(
59+
z.object({
60+
key: z.string().min(1, 'Attribute key is required'),
61+
value: z.union([
62+
z.string().min(1, 'Attribute value is required'),
63+
z.number(),
64+
z.boolean(),
65+
]),
66+
})
67+
)
68+
.optional(),
69+
6970
header: z
7071
.string()
7172
.optional()

src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export default function WorkflowActionStartForm({
2323
clearErrors,
2424
formData,
2525
trigger,
26+
cluster,
2627
}: Props) {
2728
const now = useMemo(() => new Date(), []);
2829

@@ -246,6 +247,7 @@ export default function WorkflowActionStartForm({
246247
clearErrors={clearErrors}
247248
formData={formData}
248249
fieldErrors={fieldErrors}
250+
cluster={cluster}
249251
/>
250252
</div>
251253
);

src/views/workflow-actions/workflow-action-start-optional-section/__tests__/workflow-action-start-optional-section.test.tsx

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,21 @@ jest.mock(
1515
})
1616
);
1717

18+
jest.mock(
19+
'../../workflow-actions-search-attributes/workflow-actions-search-attributes',
20+
() =>
21+
jest.fn(({ error }) => {
22+
return (
23+
<input
24+
type="text"
25+
name="Search Attributes"
26+
aria-label="Search Attributes"
27+
aria-invalid={Boolean(error)}
28+
/>
29+
);
30+
})
31+
);
32+
1833
describe('WorkflowActionStartForm', () => {
1934
it('displays error when form has errors', async () => {
2035
const formErrors = {
@@ -48,6 +63,7 @@ describe('WorkflowActionStartForm', () => {
4863
'true'
4964
);
5065

66+
// Test if error is passed to the search attributes input/mock
5167
expect(
5268
screen.getByRole('textbox', { name: 'Search Attributes' })
5369
).toHaveAttribute('aria-invalid', 'true');
@@ -61,11 +77,18 @@ describe('WorkflowActionStartForm', () => {
6177
});
6278

6379
await user.click(toggleButton);
80+
const hideToggleButton = await screen.findByRole('button', {
81+
name: /Hide Optional Configurations/i,
82+
});
6483

65-
expect(toggleButton).toHaveTextContent('Hide Optional Configurations');
66-
await user.click(toggleButton);
84+
expect(hideToggleButton).toBeInTheDocument();
6785

68-
expect(toggleButton).toHaveTextContent('Show Optional Configurations');
86+
await user.click(hideToggleButton);
87+
expect(
88+
await screen.findByRole('button', {
89+
name: /Show Optional Configurations/i,
90+
})
91+
).toBeInTheDocument();
6992
});
7093

7194
it('handles fields changes', async () => {
@@ -111,14 +134,7 @@ describe('WorkflowActionStartForm', () => {
111134
});
112135
expect(memoInput).toHaveValue(JSON.stringify({ memo: 'test' }));
113136

114-
// Should change search attributes
115-
const searchAttributesInput = screen.getByLabelText('Search Attributes');
116-
fireEvent.change(searchAttributesInput, {
117-
target: { value: JSON.stringify({ attr: 'value' }) },
118-
});
119-
expect(searchAttributesInput).toHaveValue(
120-
JSON.stringify({ attr: 'value' })
121-
);
137+
// Search attributes input checks are done in its own component test
122138
});
123139
});
124140

@@ -138,6 +154,7 @@ function TestWrapper({ formData, fieldErrors }: TestProps) {
138154
clearErrors={methods.clearErrors}
139155
formData={formData}
140156
fieldErrors={fieldErrors}
157+
cluster="test-cluster"
141158
/>
142159
);
143160
}

0 commit comments

Comments
 (0)