From 9b208713afd91983cfa8e5225156c1fea0356f9c Mon Sep 17 00:00:00 2001 From: anmol mishra Date: Thu, 16 Jul 2026 13:21:05 +0530 Subject: [PATCH 1/3] UI: Fix date range filter updating on each keystroke The date range filter inputs were calling onChange on every valid keystroke, which triggered URL search param updates and caused the editing state to be overwritten by the synced value from the parent. This made the date picker effectively unusable - typed values would appear to flash or get replaced with formatted values. Fix: Remove the auto-submit from the input change handler. The editing state stays local until the user explicitly applies it via any of these paths: - Clicking a date in the calendar popover - Pressing Enter in any date/time input - Clicking the new Apply button - Closing the popover (clicking outside / pressing Escape) closes: #69891 --- .../FilterBar/filters/DateInput.tsx | 3 ++ .../FilterBar/filters/DateRangeFilter.tsx | 8 ++++ .../FilterBar/filters/DateRangeInputs.tsx | 28 ++++++++++++- .../ui/src/hooks/useDateRangeFilter.ts | 40 ++++++++++++------- 4 files changed, 63 insertions(+), 16 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx index 0856c80dd9d8b..b109943b0a3ce 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateInput.tsx @@ -37,6 +37,7 @@ type DateInputProps = { readonly onClear: () => void; readonly onDateBlur?: () => void; readonly onFocus?: () => void; + readonly onKeyDown?: (event: React.KeyboardEvent) => void; readonly placeholder: string; }; @@ -51,6 +52,7 @@ export const DateInput = ({ onClear, onDateBlur, onFocus, + onKeyDown, placeholder, }: DateInputProps) => { const fieldName = inputType === "date" ? field : (`${field}Time` as const); @@ -69,6 +71,7 @@ export const DateInput = ({ onBlur={onDateBlur} onChange={handleInputChange(field, inputType)} onFocus={onFocus} + onKeyDown={onKeyDown} placeholder={placeholder} value={inputValue} w="full" diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx index a8992f668979f..645eeb419f6c4 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx @@ -45,6 +45,7 @@ export const DateRangeFilter = ({ filter, onChange, onRemove }: FilterPluginProp getFieldError, handleDateClick, handleInputChange, + applyDateRange, setEditingState, startDateValue, } = useDateRangeFilter({ @@ -64,6 +65,12 @@ export const DateRangeFilter = ({ filter, onChange, onRemove }: FilterPluginProp defaultOpen={!hasValue} key={filter.id} lazyMount + onOpenChange={(details) => { + if (!details.open) { + // Submit the current editing state when the popover closes + applyDateRange(); + } + }} positioning={{ placement: "bottom-start" }} unmountOnExit > @@ -141,6 +148,7 @@ export const DateRangeFilter = ({ filter, onChange, onRemove }: FilterPluginProp endDateValue={endDateValue} getFieldError={getFieldError} handleInputChange={handleInputChange} + onApply={applyDateRange} onChange={onChange} setEditingState={setEditingState} startDateValue={startDateValue} diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx index 61f1f79245807..cecbbeea8c35c 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { HStack, Text, VStack } from "@chakra-ui/react"; +import { Button, HStack, Text, VStack } from "@chakra-ui/react"; import dayjs from "dayjs"; import type { TFunction } from "i18next"; import { MdAccessTime } from "react-icons/md"; @@ -36,6 +36,7 @@ type DateRangeInputsProps = { field: "end" | "start", inputType: "date" | "time", ) => (event: React.ChangeEvent) => void; + readonly onApply?: () => void; readonly onChange: (value: DateRangeValue) => void; readonly setEditingState: React.Dispatch>; readonly startDateValue: dayjs.Dayjs | undefined; @@ -48,6 +49,7 @@ export const DateRangeInputs = ({ endDateValue, getFieldError, handleInputChange, + onApply, onChange, setEditingState, startDateValue, @@ -56,6 +58,12 @@ export const DateRangeInputs = ({ }: DateRangeInputsProps) => { const { selectedTimezone } = useTimezone(); + const handleKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && onApply) { + onApply(); + } + }; + const getBorderColor = (fieldName: ValidationError["field"]) => { if (getFieldError(fieldName)) { return "danger.solid"; @@ -137,6 +145,7 @@ export const DateRangeInputs = ({ onClear={() => clearField("start")} onDateBlur={handleDateBlur("start")} onFocus={handleFocus("start")} + onKeyDown={handleKeyDown} placeholder={DATE_INPUT_FORMAT} /> @@ -151,6 +160,7 @@ export const DateRangeInputs = ({ onClear={() => clearField("end")} onDateBlur={handleDateBlur("end")} onFocus={handleFocus("end")} + onKeyDown={handleKeyDown} placeholder={DATE_INPUT_FORMAT} /> @@ -165,6 +175,7 @@ export const DateRangeInputs = ({ inputValue={editingState.inputs.startTime} label={translate("common:filters.startTime")} onClear={clearTime("start")} + onKeyDown={handleKeyDown} placeholder={TIME_INPUT_FORMAT} /> @@ -177,6 +188,7 @@ export const DateRangeInputs = ({ inputValue={editingState.inputs.endTime} label={translate("common:filters.endTime")} onClear={clearTime("end")} + onKeyDown={handleKeyDown} placeholder={TIME_INPUT_FORMAT} /> @@ -186,6 +198,20 @@ export const DateRangeInputs = ({ {getFieldError("range")?.message} )} + + {onApply && ( + + )} ); }; diff --git a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts index a184204b4767b..b05222d91507e 100644 --- a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts +++ b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts @@ -19,7 +19,7 @@ import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import type { TFunction } from "i18next"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import type { DateRangeValue } from "src/components/FilterBar/types"; import { isValidDateValue } from "src/components/FilterBar/utils"; @@ -260,20 +260,6 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF const newInputs = { ...prev.inputs, [inputKey]: inputValue }; const validationErrors = validateInputs(newInputs); - const dateStr = field === "start" ? newInputs.start : newInputs.end; - const timeStr = field === "start" ? newInputs.startTime : newInputs.endTime; - - if (dayjs(dateStr, DATE_INPUT_FORMAT, true).isValid()) { - const combinedDateTime = combineDateAndTime(dateStr, timeStr, selectedTimezone); - - if (Boolean(combinedDateTime)) { - onChange({ - ...value, - [field === "start" ? "startDate" : "endDate"]: combinedDateTime, - }); - } - } - return { ...prev, inputs: newInputs, @@ -318,6 +304,29 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF const hasValidationErrors = editingState.validationErrors.length > 0; + const applyDateRange = useCallback(() => { + const { inputs } = editingState; + const errors = validateInputs(inputs); + + // Don't apply if there are validation errors + if (errors.length > 0) { + return; + } + + const startDateTime = inputs.start + ? combineDateAndTime(inputs.start, inputs.startTime, selectedTimezone) + : undefined; + const endDateTime = inputs.end + ? combineDateAndTime(inputs.end, inputs.endTime, selectedTimezone) + : undefined; + + onChange({ + ...value, + endDate: endDateTime, + startDate: startDateTime, + }); + }, [editingState, onChange, selectedTimezone, value]); + return { editingState, endDateValue, @@ -328,5 +337,6 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF hasValidationErrors, setEditingState, startDateValue, + applyDateRange, }; }; From 19792b96e25e06137c82855026efd3ac4e714349 Mon Sep 17 00:00:00 2001 From: anmolxlight Date: Thu, 16 Jul 2026 18:28:22 +0000 Subject: [PATCH 2/3] Address review: remove useCallback, onApply prop, and Apply button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove useCallback wrapper from applyDateRange (React compiler handles memoization) - Remove onApply prop, handleKeyDown, onKeyDown, and Apply button from DateRangeInputs - Popover onOpenChange already calls applyDateRange on close — button is redundant - Remove onApply={applyDateRange} from DateRangeFilter.tsx --- .../FilterBar/filters/DateRangeFilter.tsx | 1 - .../FilterBar/filters/DateRangeInputs.tsx | 28 +------------------ .../ui/src/hooks/useDateRangeFilter.ts | 6 ++-- 3 files changed, 4 insertions(+), 31 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx index 645eeb419f6c4..1cd0bfcf8f851 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx @@ -148,7 +148,6 @@ export const DateRangeFilter = ({ filter, onChange, onRemove }: FilterPluginProp endDateValue={endDateValue} getFieldError={getFieldError} handleInputChange={handleInputChange} - onApply={applyDateRange} onChange={onChange} setEditingState={setEditingState} startDateValue={startDateValue} diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx index cecbbeea8c35c..61f1f79245807 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeInputs.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Button, HStack, Text, VStack } from "@chakra-ui/react"; +import { HStack, Text, VStack } from "@chakra-ui/react"; import dayjs from "dayjs"; import type { TFunction } from "i18next"; import { MdAccessTime } from "react-icons/md"; @@ -36,7 +36,6 @@ type DateRangeInputsProps = { field: "end" | "start", inputType: "date" | "time", ) => (event: React.ChangeEvent) => void; - readonly onApply?: () => void; readonly onChange: (value: DateRangeValue) => void; readonly setEditingState: React.Dispatch>; readonly startDateValue: dayjs.Dayjs | undefined; @@ -49,7 +48,6 @@ export const DateRangeInputs = ({ endDateValue, getFieldError, handleInputChange, - onApply, onChange, setEditingState, startDateValue, @@ -58,12 +56,6 @@ export const DateRangeInputs = ({ }: DateRangeInputsProps) => { const { selectedTimezone } = useTimezone(); - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === "Enter" && onApply) { - onApply(); - } - }; - const getBorderColor = (fieldName: ValidationError["field"]) => { if (getFieldError(fieldName)) { return "danger.solid"; @@ -145,7 +137,6 @@ export const DateRangeInputs = ({ onClear={() => clearField("start")} onDateBlur={handleDateBlur("start")} onFocus={handleFocus("start")} - onKeyDown={handleKeyDown} placeholder={DATE_INPUT_FORMAT} /> @@ -160,7 +151,6 @@ export const DateRangeInputs = ({ onClear={() => clearField("end")} onDateBlur={handleDateBlur("end")} onFocus={handleFocus("end")} - onKeyDown={handleKeyDown} placeholder={DATE_INPUT_FORMAT} /> @@ -175,7 +165,6 @@ export const DateRangeInputs = ({ inputValue={editingState.inputs.startTime} label={translate("common:filters.startTime")} onClear={clearTime("start")} - onKeyDown={handleKeyDown} placeholder={TIME_INPUT_FORMAT} /> @@ -188,7 +177,6 @@ export const DateRangeInputs = ({ inputValue={editingState.inputs.endTime} label={translate("common:filters.endTime")} onClear={clearTime("end")} - onKeyDown={handleKeyDown} placeholder={TIME_INPUT_FORMAT} /> @@ -198,20 +186,6 @@ export const DateRangeInputs = ({ {getFieldError("range")?.message} )} - - {onApply && ( - - )} ); }; diff --git a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts index b05222d91507e..9fbd39b3d86bf 100644 --- a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts +++ b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts @@ -19,7 +19,7 @@ import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import type { TFunction } from "i18next"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import type { DateRangeValue } from "src/components/FilterBar/types"; import { isValidDateValue } from "src/components/FilterBar/utils"; @@ -304,7 +304,7 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF const hasValidationErrors = editingState.validationErrors.length > 0; - const applyDateRange = useCallback(() => { + const applyDateRange = () => { const { inputs } = editingState; const errors = validateInputs(inputs); @@ -325,7 +325,7 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF endDate: endDateTime, startDate: startDateTime, }); - }, [editingState, onChange, selectedTimezone, value]); + }; return { editingState, From bd945fe2d95dd550538b6b023a6c3392adbc3c25 Mon Sep 17 00:00:00 2001 From: anmolxlight Date: Mon, 20 Jul 2026 16:33:33 +0000 Subject: [PATCH 3/3] fixup: sort applyDateRange alphabetically in destructured objects --- .../ui/src/components/FilterBar/filters/DateRangeFilter.tsx | 2 +- airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx index 1cd0bfcf8f851..38aaddfdeb110 100644 --- a/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx +++ b/airflow-core/src/airflow/ui/src/components/FilterBar/filters/DateRangeFilter.tsx @@ -39,13 +39,13 @@ export const DateRangeFilter = ({ filter, onChange, onRemove }: FilterPluginProp const hasValue = isValidFilterValue(filter.config.type, filter.value); const { + applyDateRange, editingState, endDateValue, formatDisplayValue, getFieldError, handleDateClick, handleInputChange, - applyDateRange, setEditingState, startDateValue, } = useDateRangeFilter({ diff --git a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts index 9fbd39b3d86bf..068eb18361e70 100644 --- a/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts +++ b/airflow-core/src/airflow/ui/src/hooks/useDateRangeFilter.ts @@ -328,6 +328,7 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF }; return { + applyDateRange, editingState, endDateValue, formatDisplayValue, @@ -337,6 +338,5 @@ export const useDateRangeFilter = ({ onChange, translate, value }: UseDateRangeF hasValidationErrors, setEditingState, startDateValue, - applyDateRange, }; };