diff --git a/src/App.tsx b/src/App.tsx index 70bca7e225..c36d230fd9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -39,8 +39,8 @@ function App() { dispatch(fetchUserInfo()); }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Only run on mount + }, [dispatch]); return ( diff --git a/src/components/About.tsx b/src/components/About.tsx index 396a356758..c6deb04ed8 100644 --- a/src/components/About.tsx +++ b/src/components/About.tsx @@ -32,6 +32,7 @@ const About = () => { setAboutContent(t("ABOUT.NOCONTENT").toString()); }); }); + // Exclude t from the array, as it is not guaranteed to be stable // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.pathname]); // Listen to changes in pathname diff --git a/src/components/Header.tsx b/src/components/Header.tsx index a2a2773f91..64d72ae903 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -57,10 +57,6 @@ const Header = () => { const orgProperties = useAppSelector(state => getOrgProperties(state)); const displayTerms = (orgProperties["org.opencastproject.admin.display_terms"] || "false").toLowerCase() === "true"; - const loadHealthStatus = async () => { - await dispatch(fetchHealthStatus()); - }; - const hideMenuHelp = () => { setMenuHelp(false); }; @@ -120,6 +116,9 @@ const Header = () => { } }; + const loadHealthStatus = async () => { + await dispatch(fetchHealthStatus()); + }; // Fetching health status information at mount loadHealthStatus().then(r => console.info(r)); @@ -133,8 +132,8 @@ const Header = () => { clearInterval(interval); window.removeEventListener("mousedown", handleClickOutside); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Only run on mount + }, [dispatch]); useEffect(() => { if (!user) { return; } diff --git a/src/components/events/Events.tsx b/src/components/events/Events.tsx index 0ade511d14..07eb0863b6 100644 --- a/src/components/events/Events.tsx +++ b/src/components/events/Events.tsx @@ -54,8 +54,7 @@ const Events = () => { useEffect(() => { // disable actions button dispatch(setShowActions(false)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.hash]); + }, [dispatch, location.hash]); const onNewEventModal = async () => { await Promise.all([ diff --git a/src/components/events/Series.tsx b/src/components/events/Series.tsx index 819f8c7c2a..8f99282a06 100644 --- a/src/components/events/Series.tsx +++ b/src/components/events/Series.tsx @@ -38,8 +38,7 @@ const Series = () => { useEffect(() => { // disable actions button dispatch(showActionsSeries(false)); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [location.hash]); + }, [dispatch, location.hash]); const onNewSeriesModal = async () => { await Promise.all([ diff --git a/src/components/events/partials/ModalTabsAndPages/DetailsTobiraTab.tsx b/src/components/events/partials/ModalTabsAndPages/DetailsTobiraTab.tsx index 8060923bae..afc6ac7624 100644 --- a/src/components/events/partials/ModalTabsAndPages/DetailsTobiraTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/DetailsTobiraTab.tsx @@ -42,8 +42,7 @@ const DetailsTobiraTab = ({ kind, id }: DetailsTobiraTabProps) => { // is removed when switching to another tab. dispatch(fetchEventDetailsTobira(id)); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [id]); + }, [dispatch, id, kind]); const [initialValues, setInitialValues] = useState({ breadcrumbs: [], diff --git a/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsEditPage.tsx b/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsEditPage.tsx index f81b06c9a5..ec117d72d6 100644 --- a/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsEditPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsEditPage.tsx @@ -71,6 +71,8 @@ const EditScheduledEventsEditPage = ({ fetchNewScheduling: fetchEventInfos, setFormikValue: formik.setFieldValue, })); + // Dispatching the backend request should only happen when events change, + // and WILL change editedEvents // eslint-disable-next-line react-hooks/exhaustive-deps }, [formik.values.events]); @@ -93,21 +95,23 @@ const EditScheduledEventsEditPage = ({ */ const reduceGroupedEvent = (groupedEvents: EditedEvents[]) => { const result = groupedEvents.reduce((prev, curr) => { - for (const [key, value] of Object.entries(curr)) { - // TODO: This relies on the fact that the EditedEvent type only contains 'string' and 'string[]'. Improve on that. - if (typeof value === "string") { - // @ts-expect-error TS(7006): - prev[key as keyof EditedEvents] = prev[key as keyof EditedEvents] === curr[key as keyof EditedEvents] ? curr[key as keyof EditedEvents] : ""; - } else { - // @ts-expect-error TS(7006): - prev[key as keyof EditedEvents] = prev[key as keyof EditedEvents] === curr[key as keyof EditedEvents] ? curr[key as keyof EditedEvents] : []; - } + for (const key of Object.keys(curr) as Array) { + updateKey(key, prev, curr, defaultValuesEditedEvents); } return prev; }, lodash.cloneDeep(groupedEvents[0])); return result; }; + function updateKey( + key: K, + prev: EditedEvents, + curr: EditedEvents, + defaults: EditedEvents, + ) { + prev[key] = prev[key] === curr[key] ? curr[key] : defaults[key]; + } + const findSeriesName = (seriesOptions: { name: string, value: string }[], editedEvents: EditedEvents[]) => { const series = seriesOptions.find(e => e.value === reduceGroupedEvent(editedEvents).changedSeries); return series ? series.name : ""; @@ -426,4 +430,26 @@ const EditScheduledEventsEditPage = ({ ); }; +const defaultValuesEditedEvents: EditedEvents = { + changedDeviceInputs: [], + changedEndTimeHour: "", + changedEndTimeMinutes: "", + changedLocation: "", + changedSeries: "", + changedStartTimeHour: "", + changedStartTimeMinutes: "", + changedTitle: "", + changedWeekday: "MO", + deviceInputs: "", + endTimeHour: "", + endTimeMinutes: "", + eventId: "", + location: "", + series: "", + startTimeHour: "", + startTimeMinutes: "", + title: "", + weekday: "MO", +}; + export default EditScheduledEventsEditPage; diff --git a/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsGeneralPage.tsx b/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsGeneralPage.tsx index 60e20879d7..6719930df5 100644 --- a/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsGeneralPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EditScheduledEventsGeneralPage.tsx @@ -1,7 +1,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; -import { getSelectedRows } from "../../../../selectors/tableSelectors"; +import { getSelectedEvents } from "../../../../selectors/tableSelectors"; import { useSelectionChanges } from "../../../../hooks/wizardHooks"; import { getUserInformation } from "../../../../selectors/userInfoSelectors"; import { @@ -35,7 +35,7 @@ const EditScheduledEventsGeneralPage = ({ }) => { const { t } = useTranslation(); - const selectedRows = useAppSelector(state => getSelectedRows(state)); + const selectedRows = useAppSelector(state => getSelectedEvents(state)); const user = useAppSelector(state => getUserInformation(state)); const { @@ -43,7 +43,6 @@ const EditScheduledEventsGeneralPage = ({ allChecked, onChangeSelected, onChangeAllSelected, - // @ts-expect-error TS(7006): } = useSelectionChanges(formik, selectedRows); useEffect(() => { diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsAssetsTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsAssetsTab.tsx index 5e54f2d8f8..e2f94cba11 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsAssetsTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsAssetsTab.tsx @@ -85,6 +85,8 @@ const EventDetailsAssetsTab = ({ useEffect(() => { dispatch(removeNotificationWizardForm()); dispatch(fetchAssets(eventId)).then(); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsCommentsTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsCommentsTab.tsx index 48f3dc1409..399147c731 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsCommentsTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsCommentsTab.tsx @@ -47,6 +47,8 @@ const EventDetailsCommentsTab = ({ useEffect(() => { dispatch(fetchComments(eventId)); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsPublicationTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsPublicationTab.tsx index a34d2a8654..ec6ad8f8d0 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsPublicationTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsPublicationTab.tsx @@ -19,7 +19,9 @@ const EventDetailsPublicationTab = ({ const publications = useAppSelector(state => getPublications(state)); useEffect(() => { - dispatch(fetchEventPublications(eventId)).then(r => console.info(r)); + dispatch(fetchEventPublications(eventId)); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx index 86652f0e29..0d5b9e3ec2 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsSchedulingTab.tsx @@ -100,7 +100,8 @@ const EventDetailsSchedulingTab = ({ startDate: sourceStartDate, endDate: endStartDate, deviceId: source.device.id, - })).then(); + })); + // Force an initial conflicts check // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowDetails.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowDetails.tsx index 601c1f84a8..caca51ff6e 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowDetails.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowDetails.tsx @@ -60,6 +60,8 @@ const EventDetailsWorkflowDetails = ({ } else { dispatch(fetchWorkflowDetails({ eventId, workflowId })); } + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -350,6 +352,8 @@ const OperationsPreview = ({ // Unmount interval return () => clearInterval(fetchWorkflowOperationsInterval); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowErrors.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowErrors.tsx index a9b6a600c9..0b96b0ff70 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowErrors.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowErrors.tsx @@ -51,6 +51,8 @@ const EventDetailsWorkflowErrors = ({ if (workflowId) { dispatch(fetchWorkflowErrors({ eventId, workflowId })).then(); } + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowOperations.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowOperations.tsx index ecce2e6422..0e63000b1d 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowOperations.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowOperations.tsx @@ -46,6 +46,8 @@ const EventDetailsWorkflowOperations = ({ // Unmount interval return () => clearInterval(fetchWorkflowOperationsInterval); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx index 20dad74e06..3b354af6e8 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowSchedulingTab.tsx @@ -57,6 +57,8 @@ const EventDetailsWorkflowSchedulingTab = ({ useEffect(() => { dispatch(removeNotificationWizardForm()); dispatch(fetchWorkflows(eventId)).then(); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowTab.tsx b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowTab.tsx index c2205117c0..38aa9369e9 100644 --- a/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/EventDetailsWorkflowTab.tsx @@ -55,6 +55,8 @@ const EventDetailsWorkflowTab = ({ useEffect(() => { dispatch(removeNotificationWizardForm()); dispatch(fetchWorkflows(eventId)).then(); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx b/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx index b2a7d9961b..bac79efe26 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewAccessPage.tsx @@ -25,10 +25,10 @@ import ModalContentTable from "../../../shared/modals/ModalContentTable"; */ interface RequiredFormProps { metadata: { - "dublincore/episode_isPartOf": string, + "dublincore/episode_isPartOf"?: string, }, policies: TransformedAcl[], - aclTemplate: string, + aclTemplate: string, // For TemplateSelector // theme: string, } @@ -81,6 +81,7 @@ const NewAccessPage = ({ if (initEventAclWithSeriesAcl && formik.values.metadata["dublincore/episode_isPartOf"]) { dispatch(fetchSeriesDetailsAcls(formik.values.metadata["dublincore/episode_isPartOf"])); } + // We only care about the series, not all metadata // eslint-disable-next-line react-hooks/exhaustive-deps }, [formik.values.metadata["dublincore/episode_isPartOf"], initEventAclWithSeriesAcl, dispatch]); @@ -89,6 +90,7 @@ const NewAccessPage = ({ if (initEventAclWithSeriesAcl && formik.values.metadata["dublincore/episode_isPartOf"] && seriesAcl) { formik.setFieldValue("policies", seriesAcl); } + // We only care to set "policies" if the seriesAcl updated // eslint-disable-next-line react-hooks/exhaustive-deps }, [initEventAclWithSeriesAcl, seriesAcl]); diff --git a/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx b/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx index 4cf30ad41b..ee710ce29c 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewProcessingPage.tsx @@ -17,6 +17,7 @@ import ModalContentTable from "../../../shared/modals/ModalContentTable"; interface RequiredFormProps { sourceMode: string, processingWorkflow: string, + configuration?: { [key: string]: unknown } // For RenderWorkflowConfig } const NewProcessingPage = ({ @@ -47,6 +48,7 @@ const NewProcessingPage = ({ if (workflowDef.length === 1) { setDefaultValues(workflowDef[0].id); } + // We only care to set default values if workflowDef changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [workflowDef]); @@ -118,7 +120,6 @@ const NewProcessingPage = ({ ) : null} diff --git a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx index 72fa4fafb6..5745e0c423 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewSourcePage.tsx @@ -94,7 +94,8 @@ const NewSourcePage = ({ dispatch(fetchRecordings("inputs")); // validate form because dependent default values need to be checked - formik.validateForm().then(r => console.info(r)); + formik.validateForm(); + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/NewTobiraPage.tsx b/src/components/events/partials/ModalTabsAndPages/NewTobiraPage.tsx index 89265d5fbe..7273e3ea97 100644 --- a/src/components/events/partials/ModalTabsAndPages/NewTobiraPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/NewTobiraPage.tsx @@ -96,6 +96,7 @@ const NewTobiraPage = ({ valid = valid && check("warning", "TOBIRA_PATH_SEGMENT_INVALID", NOTIFICATION_CONTEXT_TOBIRA, () => ( newPage.segment.length <= 1 || [ + // We are explicitly checking that nothing is wrong with the path // eslint-disable-next-line no-control-regex /[\u0000-\u001F\u007F-\u009F]/u, /[\u00A0\u1680\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]/u, @@ -158,6 +159,7 @@ const NewTobiraPage = ({ select(undefined); formik.setFieldValue("breadcrumbs", [...formik.values.breadcrumbs, currentPage]); } + // Should only trigger on page change // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPage]); diff --git a/src/components/events/partials/ModalTabsAndPages/SeriesDetailsAccessTab.tsx b/src/components/events/partials/ModalTabsAndPages/SeriesDetailsAccessTab.tsx index b39f8c68e0..d3443bcaee 100644 --- a/src/components/events/partials/ModalTabsAndPages/SeriesDetailsAccessTab.tsx +++ b/src/components/events/partials/ModalTabsAndPages/SeriesDetailsAccessTab.tsx @@ -36,8 +36,7 @@ const SeriesDetailsAccessTab = ({ useEffect(() => { dispatch(removeNotificationWizardForm()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); return ( ({ }) => { const { t } = useTranslation(); - const selectedRows = useAppSelector(state => getSelectedRows(state)); + const selectedRows = useAppSelector(state => getSelectedEvents(state)); const { selectedEvents, allChecked, onChangeSelected, onChangeAllSelected, - // @ts-expect-error TS(7006): } = useSelectionChanges(formik, selectedRows); useEffect(() => { @@ -49,6 +48,7 @@ const StartTaskGeneralPage = ({ if (formik.values.events.length === 0) { formik.setFieldValue("events", selectedEvents); } + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/ModalTabsAndPages/StartTaskWorkflowPage.tsx b/src/components/events/partials/ModalTabsAndPages/StartTaskWorkflowPage.tsx index 1701bcf479..014736cb52 100644 --- a/src/components/events/partials/ModalTabsAndPages/StartTaskWorkflowPage.tsx +++ b/src/components/events/partials/ModalTabsAndPages/StartTaskWorkflowPage.tsx @@ -16,6 +16,7 @@ import ModalContentTable from "../../../shared/modals/ModalContentTable"; */ interface RequiredFormProps { workflow: string, + configuration?: { [key: string]: unknown } // For RenderWorkflowConfig } const StartTaskWorkflowPage = ({ @@ -37,14 +38,14 @@ const StartTaskWorkflowPage = ({ useEffect(() => { // Load workflow definitions for selecting dispatch(fetchWorkflowDef("tasks")); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); // Preselect the first item useEffect(() => { if (workflowDef.length === 1) { setDefaultValues(workflowDef[0].id); } + // We only care to set default values if workflowDef changes // eslint-disable-next-line react-hooks/exhaustive-deps }, [workflowDef]); @@ -101,7 +102,6 @@ const StartTaskWorkflowPage = ({ diff --git a/src/components/events/partials/modals/DeleteEventsModal.tsx b/src/components/events/partials/modals/DeleteEventsModal.tsx index 490c0c6773..77303ba0d4 100644 --- a/src/components/events/partials/modals/DeleteEventsModal.tsx +++ b/src/components/events/partials/modals/DeleteEventsModal.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; -import { getSelectedRows } from "../../../../selectors/tableSelectors"; +import { getSelectedEvents } from "../../../../selectors/tableSelectors"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { deleteMultipleEvent } from "../../../../slices/eventSlice"; import { isEvent } from "../../../../slices/tableSlice"; @@ -19,13 +19,12 @@ const DeleteEventsModal = ({ const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selectedRows = useAppSelector(state => getSelectedRows(state)); + const selectedRows = useAppSelector(state => getSelectedEvents(state)); const [allChecked, setAllChecked] = useState(true); const [selectedEvents, setSelectedEvents] = useState(selectedRows); const deleteSelectedEvents = () => { - // @ts-expect-error TS(7006): Type guarding array is hard dispatch(deleteMultipleEvent(selectedEvents)); close(); }; diff --git a/src/components/events/partials/modals/DeleteSeriesModal.tsx b/src/components/events/partials/modals/DeleteSeriesModal.tsx index 821d7f3ad5..b05b558edb 100644 --- a/src/components/events/partials/modals/DeleteSeriesModal.tsx +++ b/src/components/events/partials/modals/DeleteSeriesModal.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { getSelectedRows } from "../../../../selectors/tableSelectors"; +import { getSelectedSeries } from "../../../../selectors/tableSelectors"; import cn from "classnames"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { @@ -27,7 +27,7 @@ const DeleteSeriesModal = ({ const { t } = useTranslation(); const dispatch = useAppDispatch(); - const selectedRows = useAppSelector(state => getSelectedRows(state)); + const selectedRows = useAppSelector(state => getSelectedSeries(state)); const modifiedSelectedRows = selectedRows.map(row => { return { ...row, hasEvents: false }; }); @@ -62,11 +62,11 @@ const DeleteSeriesModal = ({ setSelectedSeries(series); } fetchData(); + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const deleteSelectedSeries = () => { - // @ts-expect-error TS(7006): Type guarding array is hard dispatch(deleteMultipleSeries(selectedSeries)); close(); }; diff --git a/src/components/events/partials/modals/EditMetadataEventsModal.tsx b/src/components/events/partials/modals/EditMetadataEventsModal.tsx index 9bb9773a19..8eba07e3fe 100644 --- a/src/components/events/partials/modals/EditMetadataEventsModal.tsx +++ b/src/components/events/partials/modals/EditMetadataEventsModal.tsx @@ -80,6 +80,7 @@ const EditMetadataEventsModal = ({ setLoading(false); } fetchData(); + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/modals/EditScheduledEventsModal.tsx b/src/components/events/partials/modals/EditScheduledEventsModal.tsx index 8d02b1cdee..e6079da0f8 100644 --- a/src/components/events/partials/modals/EditScheduledEventsModal.tsx +++ b/src/components/events/partials/modals/EditScheduledEventsModal.tsx @@ -53,8 +53,8 @@ const EditScheduledEventsModal = ({ useEffect(() => { // Load recordings that can be used for input dispatch(fetchRecordings("inputs")); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + // Only run on mount + }, [dispatch]); type StepName = "general" | "edit" | "summary"; type Step = WizardStep & { diff --git a/src/components/events/partials/modals/EventDetails.tsx b/src/components/events/partials/modals/EventDetails.tsx index 6e190df462..b221e450e2 100644 --- a/src/components/events/partials/modals/EventDetails.tsx +++ b/src/components/events/partials/modals/EventDetails.tsx @@ -120,6 +120,8 @@ const EventDetails = ({ dispatch(fetchEventDetailsTobira(eventId)); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/modals/SeriesDetails.tsx b/src/components/events/partials/modals/SeriesDetails.tsx index 323c430f93..bff322d2d9 100644 --- a/src/components/events/partials/modals/SeriesDetails.tsx +++ b/src/components/events/partials/modals/SeriesDetails.tsx @@ -69,6 +69,8 @@ const SeriesDetails = ({ dispatch(fetchSeriesStatistics(seriesId)); dispatch(fetchSeriesDetailsTobira(seriesId)); dispatch(setTobiraTabHierarchy("main")); + // Only run on mount. + // Don't update when the id changes (which should not happen anyway) to avoid data inconsistencies // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/events/partials/wizards/NewEventSummary.tsx b/src/components/events/partials/wizards/NewEventSummary.tsx index 43baec1036..19be0d6e41 100644 --- a/src/components/events/partials/wizards/NewEventSummary.tsx +++ b/src/components/events/partials/wizards/NewEventSummary.tsx @@ -103,7 +103,6 @@ const NewEventSummary = ({ {/* Summary metadata*/} @@ -112,7 +111,6 @@ const NewEventSummary = ({ {!metaDataExtendedHidden && ( diff --git a/src/components/events/partials/wizards/NewEventWizard.tsx b/src/components/events/partials/wizards/NewEventWizard.tsx index 1512c7021b..00a5c61a36 100644 --- a/src/components/events/partials/wizards/NewEventWizard.tsx +++ b/src/components/events/partials/wizards/NewEventWizard.tsx @@ -48,9 +48,7 @@ const NewEventWizard = ({ useEffect(() => { dispatch(removeNotificationWizardForm()); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); // Whether the ACL of a new event is initialized with the ACL of its series. let initEventAclWithSeriesAcl = true; @@ -225,11 +223,8 @@ const NewEventWizard = ({ )} {steps[page].name === "access" && ( ({ diff --git a/src/components/events/partials/wizards/NewPlaylistWizard.tsx b/src/components/events/partials/wizards/NewPlaylistWizard.tsx index d641cad088..2afb3f24a7 100644 --- a/src/components/events/partials/wizards/NewPlaylistWizard.tsx +++ b/src/components/events/partials/wizards/NewPlaylistWizard.tsx @@ -152,7 +152,6 @@ const NewPlaylistWizard = ({ ({ {/* Summary metadata*/} @@ -56,7 +55,6 @@ const NewSeriesSummary = ({ {!metaDataExtendedHidden ? ( diff --git a/src/components/events/partials/wizards/NewSeriesWizard.tsx b/src/components/events/partials/wizards/NewSeriesWizard.tsx index cde03290f4..f200a635de 100644 --- a/src/components/events/partials/wizards/NewSeriesWizard.tsx +++ b/src/components/events/partials/wizards/NewSeriesWizard.tsx @@ -48,16 +48,13 @@ const NewSeriesWizard = ({ useEffect(() => { dispatch(removeNotificationWizardForm()); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); useEffect(() => { // This should set off a web request that will intentionally fail, in order // to check if tobira is available at all dispatch(fetchSeriesDetailsTobiraNew("")); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); const themesEnabled = (orgProperties["admin.themes.enabled"] || "false").toLowerCase() === "true"; @@ -195,7 +192,6 @@ const NewSeriesWizard = ({ ({ diff --git a/src/components/events/partials/wizards/summaryTables/MetadataSummaryTable.tsx b/src/components/events/partials/wizards/summaryTables/MetadataSummaryTable.tsx index 0f0af7de8d..374d64c1e6 100644 --- a/src/components/events/partials/wizards/summaryTables/MetadataSummaryTable.tsx +++ b/src/components/events/partials/wizards/summaryTables/MetadataSummaryTable.tsx @@ -14,7 +14,7 @@ const MetadataSummaryTable = ({ header, }: { metadataCatalogs: MetadataCatalog[], - formikValues: { [key: string]: string | string[] | boolean | Date }, + formikValues: { [key: string]: unknown }, header: ParseKeys, }) => { const { t } = useTranslation(); @@ -26,7 +26,7 @@ const MetadataSummaryTable = ({ let metadata: { name: string, label: string, - value: string | string[] | boolean, + value: unknown, }[] = []; for (let i = 0; metadataFields.length > i; i++) { let fieldValue = @@ -82,7 +82,7 @@ const MetadataSummaryTable = ({ {Array.isArray(entry.value) ? entry.value.join(", ") - : entry.value} + : String(entry.value)} ))} diff --git a/src/components/shared/DropDown.tsx b/src/components/shared/DropDown.tsx index 9cc87b895c..d41ba0f2dc 100644 --- a/src/components/shared/DropDown.tsx +++ b/src/components/shared/DropDown.tsx @@ -132,19 +132,19 @@ const DropDown = ({ */ const MenuList = (props: MenuListProps) => { const { children, maxHeight } = props; + const items = React.Children.toArray(children); return Array.isArray(children) ? (
@@ -156,7 +156,7 @@ const DropDown = ({ names, style, }: RowComponentProps<{ - names: string[]; + names: React.ReactNode[]; }>) { const name = names[index]; return
{name}
; diff --git a/src/components/shared/MainNav.tsx b/src/components/shared/MainNav.tsx index 40623d126f..b3f7f8f222 100644 --- a/src/components/shared/MainNav.tsx +++ b/src/components/shared/MainNav.tsx @@ -178,12 +178,12 @@ const MainNav = ({ if (linkMapItem?.links && linkMapItem.links.length > 1) { const arrToSort = linkMapItem.links; if (arrToSort != undefined && arrToSort.length > 1) { - arrToSort.forEach(item => { - // @ts-expect-error: TODO: Someone else can fix this - if (item.path === pathname) { item.tmpIndex = 0; } else { item.tmpIndex = 1; } + arrToSort.sort((a, b) => { + const aPriority = a.path === pathname ? 0 : 1; + const bPriority = b.path === pathname ? 0 : 1; + + return aPriority - bPriority; }); - // @ts-expect-error: TODO: Someone else can fix this - arrToSort.sort((a, b) => a.tmpIndex - b.tmpIndex); } } } diff --git a/src/components/shared/RegistrationModal.tsx b/src/components/shared/RegistrationModal.tsx index 3ccda2b80d..c5b5fb3c26 100644 --- a/src/components/shared/RegistrationModal.tsx +++ b/src/components/shared/RegistrationModal.tsx @@ -76,8 +76,9 @@ const RegistrationModalContent = () => { }>(); useEffect(() => { - fetchRegistrationInfos().then(r => console.log(r)); + fetchRegistrationInfos(); fetchStatisticSummary(); + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -683,8 +684,7 @@ const RegistrationModalContent = () => { {states[state].buttons.back && ( setState(states[state].nextState[5])} + onClick={() => setState(states[state].nextState[5] as keyof typeof states)} > {t("ADOPTER_REGISTRATION.MODAL.BACK")} @@ -700,8 +700,7 @@ const RegistrationModalContent = () => { {states[state].buttons.skip && ( setState(states[state].nextState[2])} + onClick={() => setState(states[state].nextState[2] as keyof typeof states)} > {t("ADOPTER_REGISTRATION.MODAL.SKIP")} diff --git a/src/components/shared/Stats.tsx b/src/components/shared/Stats.tsx index 9451efa861..a36633616b 100644 --- a/src/components/shared/Stats.tsx +++ b/src/components/shared/Stats.tsx @@ -50,20 +50,19 @@ const Stats = () => { dispatch(loadEventsIntoTable()); }; - const loadStats = async () => { - // Fetching stats from server - await dispatch(fetchStats()); - }; - useEffect(() => { + const loadStats = async () => { + // Fetching stats from server + await dispatch(fetchStats()); + }; + // Load stats on mount loadStats(); const fetchEventsInterval = setInterval(() => loadStats(), 5000); return () => clearInterval(fetchEventsInterval); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); return ( <> diff --git a/src/components/shared/Table.tsx b/src/components/shared/Table.tsx index c807c8afac..22828fb362 100644 --- a/src/components/shared/Table.tsx +++ b/src/components/shared/Table.tsx @@ -96,6 +96,7 @@ const Table = ({ allowLoadIntoTable = false; clearInterval(fetchResourceInterval); }; + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.hash]); @@ -183,14 +184,14 @@ const MultiSelect = ({ selectAllCheckboxRef }: { selectAllCheckboxRef: React.Ref const selected = e.target.checked; dispatch(changeAllSelected(selected)); }; - useEffect(() => { - if (isNewEventAdded && multiSelect) { - if (selectAllCheckboxRef.current?.checked) { - selectAllCheckboxRef.current.checked = false; + + useEffect(() => { + if (isNewEventAdded && multiSelect) { + if (selectAllCheckboxRef.current?.checked) { + selectAllCheckboxRef.current.checked = false; } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isNewEventAdded, multiSelect]); + } + }, [isNewEventAdded, multiSelect, selectAllCheckboxRef]); return ( <> diff --git a/src/components/shared/TableFilters.tsx b/src/components/shared/TableFilters.tsx index 7554d80918..2608d97ccc 100644 --- a/src/components/shared/TableFilters.tsx +++ b/src/components/shared/TableFilters.tsx @@ -69,6 +69,7 @@ const TableFilters = ({ useEffect(() => { dispatch(fetchFilters(resource)); + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, [location.hash]); @@ -145,26 +146,27 @@ const TableFilters = ({ } }; - // Apply the filter changes (in debounced) accomulated in handleChange, - // simply by going to first page and then load resources. - // This helps increase performance by reducing the number of calls to load resources. - const applyFilterChangesDebounced = async () => { - console.log("Applying filter changes with value: " + itemValue); - // No matter what, we go to page one. - dispatch(goToPage(0)); - // Reload of resource - await dispatch(loadResource()); - dispatch(loadResourceIntoTable()); - }; - useEffect(() => { + // Apply the filter changes (in debounced) accomulated in handleChange, + // simply by going to first page and then load resources. + // This helps increase performance by reducing the number of calls to load resources. + const applyFilterChangesDebounced = async () => { + console.log("Applying filter changes with value: " + itemValue); + // No matter what, we go to page one. + dispatch(goToPage(0)); + // Reload of resource + await dispatch(loadResource()); + dispatch(loadResourceIntoTable()); + }; + if (itemValue) { // Call to apply filter changes with 600MS debounce! const applyFilterChangesDebouncedTimeoutId = setTimeout(applyFilterChangesDebounced, 600); return () => clearTimeout(applyFilterChangesDebouncedTimeoutId); } - // eslint-disable-next-line react-hooks/exhaustive-deps + // Only run if the filter value changed + // eslint-disable-next-line react-hooks/exhaustive-deps }, [itemValue]); const handleDatepicker = (dates?: [Date | undefined | null, Date | undefined | null]) => { diff --git a/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx b/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx index 451d517e59..baf7e3c4c4 100644 --- a/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx +++ b/src/components/shared/modals/ResourceDetailsAccessPolicyTab.tsx @@ -146,6 +146,7 @@ const ResourceDetailsAccessPolicyTab = ({ } fetchData().then(() => {}); + // Only run on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -431,8 +432,7 @@ export const AccessPolicyTable = ({ useEffect(() => { dispatch(fetchAclDefaults()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); const dropdownOptions = useMemo(() => { return roles.length > 0 diff --git a/src/components/shared/wizard/FileUpload.tsx b/src/components/shared/wizard/FileUpload.tsx index cad6d0de1b..a8703e386e 100644 --- a/src/components/shared/wizard/FileUpload.tsx +++ b/src/components/shared/wizard/FileUpload.tsx @@ -65,6 +65,7 @@ const FileUpload = ({ // additional rerender which then triggers formik validation. useEffect(() => { formik.validateForm(); + // Only run validation if these values change // eslint-disable-next-line react-hooks/exhaustive-deps }, [formik.values.fileId, formik.values.fileName, loaded]); diff --git a/src/components/shared/wizard/SelectContainer.tsx b/src/components/shared/wizard/SelectContainer.tsx index e81a18d0c3..cce06b3e16 100644 --- a/src/components/shared/wizard/SelectContainer.tsx +++ b/src/components/shared/wizard/SelectContainer.tsx @@ -67,6 +67,7 @@ const SelectContainer = ({ setItems(initialItems); setDefaultItems(initialItems); + // Init on mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/statistics/Statistics.tsx b/src/components/statistics/Statistics.tsx index 4d5aeb7e27..e9b9337acf 100644 --- a/src/components/statistics/Statistics.tsx +++ b/src/components/statistics/Statistics.tsx @@ -34,12 +34,10 @@ const Statistics = () => { // fetch user information for organization id, then fetch statistics useEffect(() => { dispatch(fetchUserInfo()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [dispatch]); useEffect(() => { - dispatch(fetchStatisticsPageStatistics(organizationId)).then(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [organizationId]); + dispatch(fetchStatisticsPageStatistics(organizationId)); + }, [dispatch, organizationId]); /* generates file name for download-link for a statistic */ const statisticsCsvFileName = (statsTitle: string) => { diff --git a/src/configs/modalConfig.ts b/src/configs/modalConfig.ts index bbe35452c6..b8560caeb9 100644 --- a/src/configs/modalConfig.ts +++ b/src/configs/modalConfig.ts @@ -106,7 +106,7 @@ export const initialFormValuesNewSeries: { breadcrumbs: TobiraPage[], selectedPage?: TobiraPage, - aclTemplate?: string, + aclTemplate: string, metadata: { [key: string]: unknown } } = { policies: [ @@ -120,13 +120,14 @@ export const initialFormValuesNewSeries: { theme: "", breadcrumbs: [], selectedPage: undefined, + aclTemplate: "", metadata: {}, }; export const initialFormValuesNewPlaylist: { policies: TransformedAcl[], - aclTemplate?: string, + aclTemplate: string, metadata: { [key: string]: unknown }, entries: PlaylistEntry[], } = { @@ -138,6 +139,7 @@ export const initialFormValuesNewPlaylist: { actions: [], }, ], + aclTemplate: "", metadata: {}, entries: [], }; diff --git a/src/hooks/wizardHooks.ts b/src/hooks/wizardHooks.ts index 43555a302b..229bfc80ee 100644 --- a/src/hooks/wizardHooks.ts +++ b/src/hooks/wizardHooks.ts @@ -1,7 +1,6 @@ import { FormikProps } from "formik"; import { useEffect, useState } from "react"; import { Event } from "../slices/eventSlice"; -import { isEvent } from "../slices/tableSlice"; export const usePageFunctions = (initialPage: number) => { const [page, setPage] = useState(initialPage); @@ -65,7 +64,7 @@ export const useSelectionChanges = ( const onChangeSelected = (e: React.ChangeEvent, id: string) => { const selected = e.target.checked; const changedEvents = selectedEvents.map(event => { - if (isEvent(event) && event.id === id) { + if (event.id === id) { return { ...event, selected: selected, diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts index 4a8b19122e..cb2b2ea49e 100644 --- a/src/i18n/i18n.ts +++ b/src/i18n/i18n.ts @@ -57,6 +57,7 @@ const myFormatter: FormatterModule = { }); } + // If we are given any by the package, we can only really return any // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value; }, diff --git a/src/selectors/tableSelectors.ts b/src/selectors/tableSelectors.ts index 79d779fd80..5073625aee 100644 --- a/src/selectors/tableSelectors.ts +++ b/src/selectors/tableSelectors.ts @@ -1,6 +1,6 @@ import { createSelector } from "reselect"; import { RootState } from "../store"; -import { rowsSelectors, TableState } from "../slices/tableSlice"; +import { isEvent, isSeries, rowsSelectors, TableState } from "../slices/tableSlice"; /** * This file contains selectors regarding the table view @@ -31,3 +31,11 @@ export const getSelectedRows = createSelector( rowsSelectors.selectAll, rows => rows.filter(row => row.selected), ); +export const getSelectedEvents = createSelector( + rowsSelectors.selectAll, + rows => rows.filter(row => row.selected).filter(isEvent), +); +export const getSelectedSeries = createSelector( + rowsSelectors.selectAll, + rows => rows.filter(row => row.selected).filter(isSeries), +); diff --git a/src/slices/seriesSlice.ts b/src/slices/seriesSlice.ts index 9d5725c624..d4b524d504 100644 --- a/src/slices/seriesSlice.ts +++ b/src/slices/seriesSlice.ts @@ -222,10 +222,13 @@ export const postNewSeries = (params: { const access = prepareAccessPolicyRulesForPost(values.policies); // Tobira - const tobira: any = {}; + const tobira: { + parentPagePath?: string + newPages?: { name?: string, pathSegment: string }[] + } = {}; if (values.selectedPage && values.breadcrumbs) { - const existingPages: any[] = []; - const newPages: any[] = []; + const existingPages: TobiraPage[] = []; + const newPages: { name?: string, pathSegment: string }[] = []; values.breadcrumbs.concat(values.selectedPage).forEach(function (page: TobiraPage) { if (page.new) { newPages.push({ @@ -237,10 +240,8 @@ export const postNewSeries = (params: { } }); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access - tobira["parentPagePath"] = existingPages.pop().path; - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - tobira["newPages"] = newPages; + tobira.parentPagePath = existingPages.pop()!.path; + tobira.newPages = newPages; } @@ -249,12 +250,11 @@ export const postNewSeries = (params: { options: unknown, access: typeof access, theme?: number, - tobira?: any + tobira?: typeof tobira } = { metadata: metadata, options: {}, access: access, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment tobira: tobira, }; @@ -325,14 +325,8 @@ export const deleteSeries = (id: Series["id"]): AppThunk => dispatch => { // delete series with provided ids export const deleteMultipleSeries = ( series: { - contributors: string[], - createdBy: string, - creation_date: string, - hasEvents: false, id: string, - organizers: string[], selected: boolean, - title: string, }[], ): AppThunk => dispatch => { const data = []; diff --git a/src/slices/tableSlice.ts b/src/slices/tableSlice.ts index eef6905192..f4a348ba00 100644 --- a/src/slices/tableSlice.ts +++ b/src/slices/tableSlice.ts @@ -71,14 +71,22 @@ export function isRowSelectable(row: Row) { return false; } -export function isEvent(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Event { +export function isEvent(row: Row): row is Row & Event { return (row as Event).event_status !== undefined; } -export function isSeries(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Series { +// export function isEvent(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Event { +// return (row as Event).event_status !== undefined; +// } + +export function isSeries(row: Row): row is Row & Series { return (row as Series).organizers !== undefined; } +// export function isSeries(row: Row | Event | Series | Recording | Server | Job | Service | User | Group | AclResult | ThemeDetailsType): row is Series { +// return (row as Series).organizers !== undefined; +// } + // TODO: Improve row typing. While this somewhat correctly reflects the current state of our code, it is rather annoying to work with. export type Row = { id: string, // For use with entityAdapter. Directly taken from event/series etc. if available