From 1589549827f7a610e9a8db97643bfb3d6d1b4941 Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 4 Sep 2026 11:57:40 +0200 Subject: [PATCH 1/7] Remove ts-expect-error hack from MainNav link sorting The current-view-first sort mutated link items with a fake tmpIndex property to work around missing typing, requiring two @ts-expect-error suppressions. Compute the sort key inline instead. This commit was largely AI generated --- src/components/shared/MainNav.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/shared/MainNav.tsx b/src/components/shared/MainNav.tsx index 29aed6b9ea..51311d492e 100644 --- a/src/components/shared/MainNav.tsx +++ b/src/components/shared/MainNav.tsx @@ -179,10 +179,9 @@ const MainNav = ({ const arrToSort = linkMapItem.links; if (arrToSort != undefined && arrToSort.length > 1) { arrToSort.sort((a, b) => { - const aPriority = a.path === pathname ? 0 : 1; - const bPriority = b.path === pathname ? 0 : 1; - - return aPriority - bPriority; + const aIndex = a.path === pathname ? 0 : 1; + const bIndex = b.path === pathname ? 0 : 1; + return aIndex - bIndex; }); } } From 316b58fd0061f1835207134b8b47c463139e4e31 Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 4 Sep 2026 11:57:45 +0200 Subject: [PATCH 2/7] Type RenderField's editable ref instead of using any editableRef held whichever DOM/component instance was mounted for the current metadata field type (input, textarea, DatePicker, or react-select), so it was typed any with several eslint-disable-next-line comments to allow the unsafe member access. Since only one field variant ever mounts at a time, split it into four separately-typed refs and focus whichever one is set, removing the need for any unsafe casts. This commit was largely AI generated --- src/components/shared/wizard/RenderField.tsx | 46 +++++++++----------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/components/shared/wizard/RenderField.tsx b/src/components/shared/wizard/RenderField.tsx index d2c72ea52f..d282cbdd0b 100644 --- a/src/components/shared/wizard/RenderField.tsx +++ b/src/components/shared/wizard/RenderField.tsx @@ -32,9 +32,11 @@ const RenderField = ({ }) => { const { t } = useTranslation(); - // TODO: Figure out how to type a ref that could have multiple types - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const editableRef = useRef(null); + // Only one of the following is ever mounted at once, chosen by metadataField.type below. + const inputRef = useRef(null); + const textareaRef = useRef(null); + const datePickerRef = useRef(null); + const selectRef = useRef, boolean, GroupBase>>>(null); const [focused, setFocused] = useState(false); const onFocus = () => setFocused(true); const onBlur = () => setFocused(false); @@ -42,18 +44,10 @@ const RenderField = ({ return (
{ - if (editableRef.current) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (editableRef.current.focus) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - editableRef.current.focus(); - } - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (editableRef.current.setFocus) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access - editableRef.current.setFocus(); // For DatePicker - } - } + inputRef.current?.focus(); + textareaRef.current?.focus(); + selectRef.current?.focus(); + datePickerRef.current?.setFocus(); }} onFocus={onFocus} onBlur={onBlur} @@ -64,7 +58,7 @@ const RenderField = ({ field={field} form={form} isFirstField={isFirstField} - ref={editableRef} + ref={datePickerRef} /> )} {metadataField.type === "text" && @@ -78,7 +72,7 @@ const RenderField = ({ isFirstField={isFirstField} focused={focused} setFocused={setFocused} - ref={editableRef} + ref={selectRef} /> )} {metadataField.type === "ordered_text" && ( @@ -90,7 +84,7 @@ const RenderField = ({ isFirstField={isFirstField} focused={focused} setFocused={setFocused} - ref={editableRef} + ref={selectRef} /> )} {metadataField.type === "text" && @@ -100,14 +94,14 @@ const RenderField = ({ )} {metadataField.type === "text_long" && ( )} {metadataField.type === "date" && ( @@ -115,14 +109,14 @@ const RenderField = ({ field={field} form={form} isFirstField={isFirstField} - ref={editableRef} + ref={datePickerRef} /> )} {metadataField.type === "boolean" && ( )}
@@ -148,7 +142,7 @@ const EditableBooleanValue = ({ }: { field: FieldProps["field"] isFirstField?: boolean, - ref: React.RefObject + ref: React.RefObject }) => { return ( + ref: React.RefObject }) => { return ( // For some reason onclick events are bubbling up from the datepicker which we do not want. @@ -254,7 +248,7 @@ const EditableSingleValueTextArea = ({ }: { field: FieldProps["field"] isFirstField?: boolean, - ref: React.RefObject + ref: React.RefObject }) => { return ( // Maybe replace TextareaAutosize with css "field-sizing: content" once all @@ -304,7 +298,7 @@ const EditableSingleValueTime = ({ field: FieldProps["field"] form: FieldProps["form"] isFirstField?: boolean, - ref: React.RefObject + ref: React.RefObject }) => { return ( // For some reason onclick events are bubbling up from the datepicker which we do not want. From 9afbc64e300173296fe1d88088235d844f486d31 Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 4 Sep 2026 12:24:19 +0200 Subject: [PATCH 3/7] Type Field wrapper's props via Formik's FastFieldConfig The wrapper used React.ComponentProps, which resolves to any since FormikFastField itself is declared as React.FC. Type it via Formik's own FastFieldConfig, derived rather than hand-copied so it stays correct if Formik's config props change, while custom component/as props (which take app-specific extra props via sibling attributes) stay loosely typed like upstream Formik's own Field. This commit was largely AI generated --- src/components/shared/Field.tsx | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/src/components/shared/Field.tsx b/src/components/shared/Field.tsx index 30c50c1710..d82a4ecb56 100644 --- a/src/components/shared/Field.tsx +++ b/src/components/shared/Field.tsx @@ -1,13 +1,25 @@ -import { FastField as FormikFastField } from "formik"; +import { FastField as FormikFastField, FastFieldConfig } from "formik"; /** - * Wrapper for the Formik Fields + * Wrapper for the Formik Fields. + * + * `FormikFastField` itself is typed as `React.FC`, so + * `React.ComponentProps` would just resolve to `any`. + * We derive from Formik's own `FastFieldConfig` instead (rather than hand- + * copying its shape), so this stays correct if Formik's config props change. + * `component`/`as` are re-typed more loosely than Formik declares them: this + * app passes app-specific extra props (e.g. `metadataField`) into custom + * components via sibling attributes on ``, which isn't something + * Formik's own types can verify either - it types its `Field` the same way. */ -// TODO: Add strong typing -// The line below is currently just a fancy way of saying "any" -// Find a way to properly type this wrapper -type FieldProps = React.ComponentProps; -export const Field = (props: FieldProps) => { +type FieldProps = Omit, "component" | "as"> & { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + component?: string | React.ComponentType, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + as?: string | React.ComponentType, +} & Record; + +export const Field = (props: FieldProps) => { return ( Date: Fri, 4 Sep 2026 12:42:59 +0200 Subject: [PATCH 4/7] Fix dead conflict-detected branch in scheduling form submit checkConflicts is an async thunk that resolves to { conflicts, hasSchedulingConflicts }, but submitForm checked truthiness of the dispatched action object itself via .then(r => ...), which is always truthy. This meant the form would proceed to save scheduling changes even when a genuine conflict was detected. Use .unwrap() to read the actual payload and branch on hasSchedulingConflicts, and add a .catch() since .unwrap() turns a rejected thunk into a real promise rejection that was previously silently swallowed. This commit was largely AI generated --- .../EventDetailsSchedulingTab.tsx | 26 +++++++++++-------- src/slices/eventDetailsSlice.ts | 2 -- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx index 48f315b733..65bcd46e9d 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx @@ -192,20 +192,24 @@ const EventDetailsSchedulingTab = ({ values.scheduleEndHour, values.scheduleEndMinute, ); - dispatch(checkConflicts({ eventId, startDate, endDate, deviceId: values.captureAgent })).then( - r => { - if (r) { + const notifyNotUpdated = () => { + dispatch(addNotification({ + type: "error", + key: "EVENTS_NOT_UPDATED", + duration: -1, + context: NOTIFICATION_CONTEXT, + })); + }; + + dispatch(checkConflicts({ eventId, startDate, endDate, deviceId: values.captureAgent })).unwrap() + .then(({ hasSchedulingConflicts }) => { + if (!hasSchedulingConflicts) { dispatch(saveSchedulingInfo({ eventId, values, startDate, endDate })).then(); } else { - dispatch(addNotification({ - type: "error", - key: "EVENTS_NOT_UPDATED", - duration: -1, - context: NOTIFICATION_CONTEXT, - })); + notifyNotUpdated(); } - }, - ); + }) + .catch(notifyNotUpdated); }; // initial values of the formik form diff --git a/src/slices/eventDetailsSlice.ts b/src/slices/eventDetailsSlice.ts index 8ae42dffde..ce264f70b9 100644 --- a/src/slices/eventDetailsSlice.ts +++ b/src/slices/eventDetailsSlice.ts @@ -1190,8 +1190,6 @@ export const saveSchedulingInfo = createAppAsyncThunk("eventDetails/saveScheduli return source; }); -// TODO: This does not return a boolean anymore. Fix this in usage, make users -// get their info from the state export const checkConflicts = createAppAsyncThunk("eventDetails/checkConflicts", async (params: { eventId: Event["id"], startDate: Date, From 8a940d5b3580e45dc68712b555f046f96d7f5a8b Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 4 Sep 2026 13:23:57 +0200 Subject: [PATCH 5/7] Resolve stale TODO about empty workflow fallback shape Traced through git history: the "vastly different" data the TODO referred to was the old (commented-out, since removed) Angular reducer's empty-workflow object, which matched the full workflow- details shape. The current placeholder already matches our initial state exactly, and every consumer narrows on `"status" in workflow` before reading details-only fields, so the simpler fallback degrades safely. Replace the stale question with a note on why this is fine. This commit was largely AI generated --- src/slices/eventDetailsSlice.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/slices/eventDetailsSlice.ts b/src/slices/eventDetailsSlice.ts index ce264f70b9..483db189c5 100644 --- a/src/slices/eventDetailsSlice.ts +++ b/src/slices/eventDetailsSlice.ts @@ -2365,9 +2365,9 @@ const eventDetailsSlice = createSlice({ }) .addCase(fetchWorkflowDetails.rejected, (state, action) => { state.statusWorkflowDetails = "failed"; - // This is the empty workflow data from the original reducer - // TODO: Figure out why it is so vastly different from our initial state - // and maybe fix our initial state if this is actually correct + // Falls back to the same placeholder as our initial state (workflowId/description + // only, not the full workflow-details shape); consumers already narrow on + // `"status" in workflow` before reading details-only fields, so this degrades safely. const emptyWorkflowData = { workflowId: "", description: "", From b408c2061e33d8843745b72a1177187936d4b650 Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 4 Sep 2026 13:28:49 +0200 Subject: [PATCH 6/7] Remove stale ChangeMultiple TODO in SchedulingTime NewSourcePage.tsx already implements this: all three of its SchedulingTime usages branch on sourceMode === "SCHEDULE_MULTIPLE" inside the callbackHour/callbackMinute props they pass in, calling the *Multiple variants from dateUtils.ts. That's the extension point SchedulingTime exposes, so the shared component itself never needed to know about "Multiple" mode - the comment was just left behind. This commit was largely AI generated --- .../events/partials/wizards/scheduling/SchedulingTime.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/events/partials/wizards/scheduling/SchedulingTime.tsx b/src/components/events/partials/wizards/scheduling/SchedulingTime.tsx index 4e5812400d..0181171b66 100644 --- a/src/components/events/partials/wizards/scheduling/SchedulingTime.tsx +++ b/src/components/events/partials/wizards/scheduling/SchedulingTime.tsx @@ -47,7 +47,6 @@ const SchedulingTime = ({ handleChange={element => { if (element) { callbackHour(element.value); - // TODO: Allow for ChangeMultiple for NewSourcePage } }} placeholder={t(hourPlaceholder)} From f234f65525dfe50233d0cf4f09eb99fba6b8438b Mon Sep 17 00:00:00 2001 From: Arnei Date: Fri, 4 Sep 2026 13:41:17 +0200 Subject: [PATCH 7/7] Remove stale statisticsThunks modernization TODOs seriesDetailsSlice.ts's fetchSeriesStatistics/fetchSeriesStatisticsValueUpdate use the exact same pattern as these two (createAppAsyncThunk wrapping the shared fetchStatistics/fetchStatisticsValueUpdate helpers from statisticsSlice.ts), just swapping "episode" for "series" - and carry no such TODO. The modernization already happened; these comments were just never cleaned up. This commit was largely AI generated --- src/slices/eventDetailsSlice.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/slices/eventDetailsSlice.ts b/src/slices/eventDetailsSlice.ts index 483db189c5..dcdc833ef6 100644 --- a/src/slices/eventDetailsSlice.ts +++ b/src/slices/eventDetailsSlice.ts @@ -1528,7 +1528,6 @@ export const fetchWorkflowErrorDetails = createAppAsyncThunk("eventDetails/fetch return data.data; }); -// TODO: Fix this after the modernization of statisticsThunks happened export const fetchEventStatistics = createAppAsyncThunk("eventDetails/fetchEventStatistics", async (eventId: Event["id"], { getState }) => { // get prior statistics const state = getState(); @@ -1543,7 +1542,6 @@ export const fetchEventStatistics = createAppAsyncThunk("eventDetails/fetchEvent ); }); -// TODO: Fix this after the modernization of statisticsThunks happened export const fetchEventStatisticsValueUpdate = createAppAsyncThunk("eventDetails/fetchEventStatisticsValueUpdate", async (params: { id: Event["id"], providerId: string,