From 6ba986d181f4c88962562e1614cfab0ed7ace301 Mon Sep 17 00:00:00 2001 From: Jade Carino Date: Tue, 31 Mar 2026 16:28:20 +0100 Subject: [PATCH 1/2] fix: persist timeframe radio button selection in saved queries Signed-off-by: Jade Carino --- .../test-runs/timeframe/TimeFrameContent.tsx | 35 ++++++++++------ .../contexts/TestRunsQueryParamsContext.tsx | 12 ++++++ .../timeframe/TimeFrameContent.test.tsx | 41 +++++++++++++------ galasa-ui/src/utils/constants/common.ts | 2 + galasa-ui/src/utils/interfaces/testRuns.ts | 2 + 5 files changed, 67 insertions(+), 25 deletions(-) diff --git a/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx b/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx index 8eded839..ce3c2ef0 100644 --- a/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx +++ b/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx @@ -185,29 +185,38 @@ export default function TimeFrameContent({ values, setValues }: TimeFrameContent // Update the state with the corrected values only if the notification is not an error if (validationNotification?.kind !== 'error') { const finalState = calculateSynchronizedState(correctedFrom, correctedTo, timezone); - setValues((prevValues) => ({ ...prevValues, ...finalState })); + setValues((prevValues) => ({ + ...prevValues, + ...finalState, + fromOption: selectedFromOption, + toOption: selectedToOption, + })); } }, - [values, translations, setValues, getResolvedTimeZone, selectedFromOption] + [values, translations, setValues, getResolvedTimeZone, selectedFromOption, selectedToOption] ); // Update the isRelativeToNow state when the selectedToOption changes useEffect(() => { const isRelativeToNow = selectedToOption === ToSelectionOptions.now; - setValues((prevValues) => ({ ...prevValues, isRelativeToNow })); - }, [selectedToOption, setValues]); - - // Sync selected options whenever isRelativeToNow changes (from URL or user interaction) + setValues((prevValues) => ({ + ...prevValues, + isRelativeToNow, + fromOption: selectedFromOption, + toOption: selectedToOption, + })); + }, [selectedFromOption, selectedToOption, setValues]); + + // Sync radio button selections from URL parameters when query in view changes useEffect(() => { - if (values.isRelativeToNow !== undefined) { - const toOption = values.isRelativeToNow - ? ToSelectionOptions.now - : ToSelectionOptions.specificToTime; + if (values.fromOption) { + setSelectedFromOption(values.fromOption as FromSelectionOptions); + } - // Always sync the "To" option with isRelativeToNow - setSelectedToOption(toOption); + if (values.toOption) { + setSelectedToOption(values.toOption as ToSelectionOptions); } - }, [values.isRelativeToNow]); + }, [values.fromOption, values.toOption]); return (
diff --git a/galasa-ui/src/contexts/TestRunsQueryParamsContext.tsx b/galasa-ui/src/contexts/TestRunsQueryParamsContext.tsx index a7a3179a..94ff36e5 100644 --- a/galasa-ui/src/contexts/TestRunsQueryParamsContext.tsx +++ b/galasa-ui/src/contexts/TestRunsQueryParamsContext.tsx @@ -128,6 +128,8 @@ export function TestRunsQueryParamsProvider({ children }: TestRunsQueryParamsPro const fromParam = params.get(TEST_RUNS_QUERY_PARAMS.FROM); const toParam = params.get(TEST_RUNS_QUERY_PARAMS.TO); const durationParam = params.get(TEST_RUNS_QUERY_PARAMS.DURATION); + const fromOptionParam = params.get(TEST_RUNS_QUERY_PARAMS.FROM_OPTION); + const toOptionParam = params.get(TEST_RUNS_QUERY_PARAMS.TO_OPTION); let toDate: Date, fromDate: Date, @@ -153,6 +155,8 @@ export function TestRunsQueryParamsProvider({ children }: TestRunsQueryParamsPro const timeframe = { ...calculateSynchronizedState(fromDate, toDate, timezone), isRelativeToNow, + fromOption: fromOptionParam || undefined, + toOption: toOptionParam || undefined, }; // Search Criteria @@ -286,6 +290,14 @@ export function TestRunsQueryParamsProvider({ children }: TestRunsQueryParamsPro params.set(TEST_RUNS_QUERY_PARAMS.TO, timeframeValues.toDate.toISOString()); } + // Radio button selections + if (timeframeValues.fromOption) { + params.set(TEST_RUNS_QUERY_PARAMS.FROM_OPTION, timeframeValues.fromOption); + } + if (timeframeValues.toOption) { + params.set(TEST_RUNS_QUERY_PARAMS.TO_OPTION, timeframeValues.toOption); + } + // Search Criteria Object.entries(searchCriteria).forEach(([key, value]) => { if (value) { diff --git a/galasa-ui/src/tests/components/test-runs/timeframe/TimeFrameContent.test.tsx b/galasa-ui/src/tests/components/test-runs/timeframe/TimeFrameContent.test.tsx index 5b230378..9eace779 100644 --- a/galasa-ui/src/tests/components/test-runs/timeframe/TimeFrameContent.test.tsx +++ b/galasa-ui/src/tests/components/test-runs/timeframe/TimeFrameContent.test.tsx @@ -488,7 +488,7 @@ describe('applyTimeFrameRules', () => { }); }); - test('should synchronize the "To" option when the query changes to be relative to now', async () => { + test('should preserve radio button selections when switching between queries', async () => { const initialFrom = '2025-08-10T12:00:00.000Z'; const initialTo = '2025-08-13T12:00:00.000Z'; @@ -503,14 +503,30 @@ describe('applyTimeFrameRules', () => { return ( <> - {/* Add a button to simulate a query change */} + {/* Add a button to simulate loading a different query */} + ); @@ -535,20 +551,21 @@ describe('applyTimeFrameRules', () => { expect(nowRadio).not.toBeChecked(); }); - // Simulate a query change - const toggleButton = screen.getByRole('toggle-isRelativeToNow'); - fireEvent.click(toggleButton); + // Simulate loading a query that uses "Now" + const loadNowButton = screen.getByRole('load-query-with-now'); + fireEvent.click(loadNowButton); - // 'To' option should have switched from specific time to 'now' + // 'To' option should sync to 'now' based on the loaded query await waitFor(() => { expect(nowRadio).toBeChecked(); expect(specificToTimeRadio).not.toBeChecked(); }); - // Simulate another query change - fireEvent.click(toggleButton); + // Simulate loading a query that uses "Specific Time" + const loadSpecificButton = screen.getByRole('load-query-with-specific'); + fireEvent.click(loadSpecificButton); - // 'To' option should have switched back to specific time + // 'To' option should sync to 'specific time' based on the loaded query await waitFor(() => { expect(specificToTimeRadio).toBeChecked(); expect(nowRadio).not.toBeChecked(); diff --git a/galasa-ui/src/utils/constants/common.ts b/galasa-ui/src/utils/constants/common.ts index 102fc1f6..f626a37a 100644 --- a/galasa-ui/src/utils/constants/common.ts +++ b/galasa-ui/src/utils/constants/common.ts @@ -77,6 +77,8 @@ const TEST_RUNS_QUERY_PARAMS = { FROM: 'from', TO: 'to', DURATION: 'duration', + FROM_OPTION: 'fromOption', + TO_OPTION: 'toOption', RUN_NAME: 'runName', REQUESTOR: 'requestor', USER: 'user', diff --git a/galasa-ui/src/utils/interfaces/testRuns.ts b/galasa-ui/src/utils/interfaces/testRuns.ts index 92bfa623..f79e352e 100644 --- a/galasa-ui/src/utils/interfaces/testRuns.ts +++ b/galasa-ui/src/utils/interfaces/testRuns.ts @@ -42,6 +42,8 @@ export interface TimeFrameValues { durationHours: number; durationMinutes: number; isRelativeToNow?: boolean; + fromOption?: string; + toOption?: string; } export interface RunLog { From d4da5ec53b282173892a097befffdfe1119a7e84 Mon Sep 17 00:00:00 2001 From: Jade Carino Date: Tue, 31 Mar 2026 20:33:57 +0100 Subject: [PATCH 2/2] review/fix: Remove circular update causing browser to freeze Signed-off-by: Jade Carino --- .../test-runs/timeframe/TimeFrameContent.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx b/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx index ce3c2ef0..0f7e93e4 100644 --- a/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx +++ b/galasa-ui/src/components/test-runs/timeframe/TimeFrameContent.tsx @@ -7,7 +7,7 @@ import styles from '@/styles/test-runs/timeframe/TimeFrameContent.module.css'; import { TimeFrameValues } from '@/utils/interfaces'; -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import TimeFrameFilter from './TimeFrameFilter'; import { dateTimeLocal2UTC, dateTimeUTC2Local } from '@/utils/timeOperations'; import { InlineNotification } from '@carbon/react'; @@ -127,6 +127,7 @@ export default function TimeFrameContent({ values, setValues }: TimeFrameContent const [selectedToOption, setSelectedToOption] = useState( ToSelectionOptions.now ); + const isSyncingFromUrl = useRef(false); const handleValueChange = useCallback( (field: keyof TimeFrameValues, value: any) => { @@ -198,6 +199,11 @@ export default function TimeFrameContent({ values, setValues }: TimeFrameContent // Update the isRelativeToNow state when the selectedToOption changes useEffect(() => { + // Don't update if we're currently syncing from URL to avoid circular updates + if (isSyncingFromUrl.current) { + return; + } + const isRelativeToNow = selectedToOption === ToSelectionOptions.now; setValues((prevValues) => ({ ...prevValues, @@ -210,12 +216,16 @@ export default function TimeFrameContent({ values, setValues }: TimeFrameContent // Sync radio button selections from URL parameters when query in view changes useEffect(() => { if (values.fromOption) { + isSyncingFromUrl.current = true; setSelectedFromOption(values.fromOption as FromSelectionOptions); } if (values.toOption) { + isSyncingFromUrl.current = true; setSelectedToOption(values.toOption as ToSelectionOptions); } + // Reset the ref after state updates have been processed + isSyncingFromUrl.current = false; }, [values.fromOption, values.toOption]); return (