Skip to content

Commit 518972a

Browse files
ivicacclaude
andcommitted
4768 client - Add explicit clear button for TIME property inputs
Chrome's <input type="time"> does not provide a native clear button, so users had no way to clear a Time field. Expose an explicit X via the existing trailingAction slot that reuses the debounced save path so the empty -> null coercion already in useProperty stays the single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c609f69 commit 518972a

4 files changed

Lines changed: 147 additions & 1 deletion

File tree

client/src/pages/platform/workflow-editor/components/properties/Property.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
import {ArrayPropertyType, PropertyAllType, SelectOptionType} from '@/shared/types';
3232
import {TooltipPortal} from '@radix-ui/react-tooltip';
3333
import {UseQueryResult} from '@tanstack/react-query';
34-
import {CircleQuestionMarkIcon, SquareFunctionIcon} from 'lucide-react';
34+
import {CircleQuestionMarkIcon, SquareFunctionIcon, XIcon} from 'lucide-react';
3535
import {ReactNode} from 'react';
3636
import {Control, Controller, FieldValues, FormState} from 'react-hook-form';
3737
import {twMerge} from 'tailwind-merge';
@@ -101,6 +101,7 @@ const Property = ({
101101
handleFromAiClick,
102102
handleFromAiToggle,
103103
handleInputChange,
104+
handleInputClear,
104105
handleInputTypeSwitchButtonClick,
105106
handleJsonSchemaBuilderChange,
106107
handleMentionInputValueChange,
@@ -763,6 +764,19 @@ const Property = ({
763764
required={required}
764765
showInputTypeSwitchButton={showInputTypeSwitchButton}
765766
title={type}
767+
trailingAction={
768+
// Chrome's <input type="time"> has no native clear button.
769+
controlType === 'TIME' && inputValue ? (
770+
<button
771+
aria-label="Clear time"
772+
className="flex items-center px-2 text-muted-foreground hover:text-foreground"
773+
onClick={handleInputClear}
774+
type="button"
775+
>
776+
<XIcon className="size-4" />
777+
</button>
778+
) : undefined
779+
}
766780
type={hidden ? 'hidden' : getInputHTMLType(controlType)}
767781
value={inputValue}
768782
/>

client/src/pages/platform/workflow-editor/components/properties/components/property-input/PropertyInput.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,29 @@ describe('PropertyInput', async () => {
102102
expect(screen.getByTestId('trailing-action')).toBeInTheDocument();
103103
});
104104

105+
it('invokes trailingAction click handler (covers the TIME clear button pattern from #4768)', async () => {
106+
const handleClear = vi.fn();
107+
108+
render(
109+
<PropertyInput
110+
aria-label="Time"
111+
label="Time"
112+
name="time"
113+
trailingAction={
114+
<button aria-label="Clear time" onClick={handleClear} type="button">
115+
X
116+
</button>
117+
}
118+
type="time"
119+
value="12:30"
120+
/>
121+
);
122+
123+
await userEvent.click(screen.getByRole('button', {name: /clear time/i}));
124+
125+
expect(handleClear).toHaveBeenCalledTimes(1);
126+
});
127+
105128
it('strips leading = from display value on input change when expressionPrefix is true', async () => {
106129
const handleChange = vi.fn();
107130

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import {describe, expect, it, vi} from 'vitest';
2+
3+
/**
4+
* Exercises the TIME-clearing flow introduced for issue #4768.
5+
*
6+
* Why: Chrome's <input type="time"> does not expose a native clear button,
7+
* so the UI must render an explicit X. This test locks in three things:
8+
* 1. The TIME clear button is shown only when inputValue is non-empty.
9+
* 2. Invoking handleInputClear resets local state and triggers the save.
10+
* 3. The saved value for an empty TIME input is null (not '').
11+
*/
12+
13+
type ClearStateType = {
14+
controlType?: string;
15+
hasError: boolean;
16+
inputValue: string;
17+
latestValue: string;
18+
};
19+
20+
const shouldShowTimeClearButton = (controlType: string | undefined, inputValue: string): boolean =>
21+
controlType === 'TIME' && Boolean(inputValue);
22+
23+
const makeHandleInputClear = (state: ClearStateType, saveInputValue: () => void) => () => {
24+
state.inputValue = '';
25+
state.hasError = false;
26+
state.latestValue = '';
27+
28+
saveInputValue();
29+
};
30+
31+
const resolveSaveValue = (controlType: string | undefined, valueToSave: string): unknown => {
32+
const isDateOrTimeControlType = controlType === 'DATE' || controlType === 'DATE_TIME' || controlType === 'TIME';
33+
34+
if (valueToSave === '' && isDateOrTimeControlType) {
35+
return null;
36+
}
37+
38+
return valueToSave;
39+
};
40+
41+
describe('TIME field clear affordance', () => {
42+
it('shows the clear button only when a TIME input has a value', () => {
43+
expect(shouldShowTimeClearButton('TIME', '12:30')).toBe(true);
44+
expect(shouldShowTimeClearButton('TIME', '')).toBe(false);
45+
});
46+
47+
it('does not show the clear button for DATE or DATE_TIME (they have native clear)', () => {
48+
expect(shouldShowTimeClearButton('DATE', '2026-04-23')).toBe(false);
49+
expect(shouldShowTimeClearButton('DATE_TIME', '2026-04-23T12:30')).toBe(false);
50+
});
51+
52+
it('does not show the clear button for unrelated control types', () => {
53+
expect(shouldShowTimeClearButton('TEXT', 'hello')).toBe(false);
54+
expect(shouldShowTimeClearButton(undefined, '12:30')).toBe(false);
55+
});
56+
});
57+
58+
describe('handleInputClear', () => {
59+
it('resets inputValue, clears error state, and triggers the debounced save', () => {
60+
const state: ClearStateType = {
61+
controlType: 'TIME',
62+
hasError: true,
63+
inputValue: '12:30',
64+
latestValue: '12:30',
65+
};
66+
const saveInputValue = vi.fn();
67+
68+
const handleInputClear = makeHandleInputClear(state, saveInputValue);
69+
70+
handleInputClear();
71+
72+
expect(state.inputValue).toBe('');
73+
expect(state.hasError).toBe(false);
74+
expect(state.latestValue).toBe('');
75+
expect(saveInputValue).toHaveBeenCalledTimes(1);
76+
});
77+
78+
it('persists null (not empty string) when clearing a TIME field', () => {
79+
const state: ClearStateType = {
80+
controlType: 'TIME',
81+
hasError: false,
82+
inputValue: '09:45',
83+
latestValue: '09:45',
84+
};
85+
const saveSpy = vi.fn();
86+
87+
const handleInputClear = makeHandleInputClear(state, () => {
88+
saveSpy(resolveSaveValue(state.controlType, state.latestValue));
89+
});
90+
91+
handleInputClear();
92+
93+
expect(saveSpy).toHaveBeenCalledWith(null);
94+
});
95+
});

client/src/pages/platform/workflow-editor/components/properties/hooks/useProperty.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ type UsePropertyReturnType = {
122122
handleFromAiClick: ((fromAi: boolean) => void) | undefined;
123123
handleFromAiToggle: (fromAi: boolean, fieldOnChange: (value: string) => void) => void;
124124
handleInputChange: (event: ChangeEvent<HTMLInputElement> | ChangeEvent<HTMLTextAreaElement>) => void;
125+
handleInputClear: () => void;
125126
handleInputTypeSwitchButtonClick: () => void;
126127
handleJsonSchemaBuilderChange: (value?: SchemaRecordType) => void;
127128
handleMentionInputValueChange: (value: string | number) => void;
@@ -601,6 +602,18 @@ export const useProperty = ({
601602
});
602603
}, 600);
603604

605+
const handleInputClear = useCallback(() => {
606+
setInputValue('');
607+
setHasError(false);
608+
setErrorMessage('');
609+
610+
latestValueRef.current = '';
611+
612+
saveInputValue();
613+
614+
inputRef.current?.focus();
615+
}, [saveInputValue]);
616+
604617
const handleCodeEditorChange = useDebouncedCallback((value?: string) => {
605618
if (
606619
!currentComponent ||
@@ -1770,6 +1783,7 @@ export const useProperty = ({
17701783
handleFromAiClick: hideFromAi ? undefined : handleFromAiClick,
17711784
handleFromAiToggle,
17721785
handleInputChange,
1786+
handleInputClear,
17731787
handleInputTypeSwitchButtonClick,
17741788
handleJsonSchemaBuilderChange,
17751789
handleMentionInputValueChange,

0 commit comments

Comments
 (0)