From e6a9979e0002a7e23509f9133ab9873cc0e07343 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 10:20:46 +0100 Subject: [PATCH 01/14] Add custom `redux-persist` storage engine using URL search parameters --- src/utils/urlStorage.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/utils/urlStorage.ts diff --git a/src/utils/urlStorage.ts b/src/utils/urlStorage.ts new file mode 100644 index 0000000000..6f1457ff0e --- /dev/null +++ b/src/utils/urlStorage.ts @@ -0,0 +1,34 @@ +import { WebStorage } from "redux-persist"; + +// Custom storage engine for `redux-persist` +// to store (parts of) the application state in URL parameters. +// This allows users to share "deep links" to specific parts of the application. +const storage = { + getItem: async (key: string) => withParams(params => params.get(key)), + setItem: async (key: string, value: string) => updateParams(params => { + params.set(key, value); + }), + removeItem: async (key: string) => updateParams(params => { + params.delete(key); + }) +} satisfies WebStorage; +export default storage; + +// Helper functions to work with URL parameters +const withParams = (fn: (params: URLSearchParams) => T): T => ( + fn( + new URLSearchParams(window.location.search) + ) +); +const updateParams = (fn: (params: URLSearchParams) => void) => { + withParams(params => { + fn(params); + window.history.pushState(null, "", `${ + window.location.pathname + }?${ + params.toString() + }#${ + window.location.hash + }`); + }); +}; From 7a70490b966b8cda4467a60efcf6eac6cf4bf8d1 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 10:47:58 +0100 Subject: [PATCH 02/14] Some stylistic nitpicks --- src/components/events/Events.tsx | 2 +- src/components/events/Series.tsx | 8 ++++---- src/slices/seriesDetailsSlice.ts | 6 +----- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/components/events/Events.tsx b/src/components/events/Events.tsx index 0e1337a4f6..33904e4c1b 100644 --- a/src/components/events/Events.tsx +++ b/src/components/events/Events.tsx @@ -315,7 +315,7 @@ const Events = () => { {/*Include table component*/} {/* */} -
+
diff --git a/src/components/events/Series.tsx b/src/components/events/Series.tsx index 4dcce7f070..36bee650de 100644 --- a/src/components/events/Series.tsx +++ b/src/components/events/Series.tsx @@ -142,11 +142,11 @@ const Series = () => { }; useHotkeys( - availableHotkeys.general.NEW_SERIES.sequence, - () => showNewSeriesModal(), + availableHotkeys.general.NEW_SERIES.sequence, + () => showNewSeriesModal(), { description: t(availableHotkeys.general.NEW_SERIES.description) ?? undefined }, - [showNewSeriesModal] - ); + [showNewSeriesModal] + ); return ( <> diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index a386363573..44b0c87841 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -47,7 +47,7 @@ type SeriesDetailsState = { errorStatisticsValue: SerializedError | null, statusTobiraData: 'uninitialized' | 'loading' | 'succeeded' | 'failed', errorTobiraData: SerializedError | null, - metadata: MetadataCatalog, + metadata: MetadataCatalog, extendedMetadata: MetadataCatalog[], feeds: Feed[], acl: TransformedAcl[], @@ -524,9 +524,6 @@ const seriesDetailsSlice = createSlice({ >) { state.statistics = action.payload; }, - setDoNothing(state) { - - } }, // These are used for thunks extraReducers: builder => { @@ -654,7 +651,6 @@ export const { setSeriesDetailsExtendedMetadata, setSeriesStatisticsError, setSeriesStatistics, - setDoNothing, } = seriesDetailsSlice.actions; // Export the slice reducer as the default export From e9156ae6fcc2340d987359eea145f46a40bf718c Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 10:25:47 +0100 Subject: [PATCH 03/14] Persist event details modal state in the URL parameters --- src/store.ts | 4 +++- src/utils/urlStorage.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/store.ts b/src/store.ts index 78cd3df563..4cbec869c8 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,5 +1,6 @@ import { FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE, persistReducer } from "redux-persist"; import storage from "redux-persist/lib/storage"; +import urlStorage from "./utils/urlStorage"; import { UnknownAction, combineReducers } from "redux"; import tableFilters from "./slices/tableFilterSlice"; import tableFilterProfiles from "./slices/tableFilterProfilesSlice"; @@ -46,6 +47,7 @@ const usersPersistConfig = { key: "users", storage, whitelist: ["columns"] } const groupsPersistConfig = { key: "groups", storage, whitelist: ["columns"] } const aclsPersistConfig = { key: "acls", storage, whitelist: ["columns"] } const themesPersistConfig = { key: "themes", storage, whitelist: ["columns"] } +const eventDetailsPersistConfig = { key: "eventDetails", storage: urlStorage, whitelist: ["modal"]} // form reducer and all other reducers used in this app const reducers = combineReducers({ @@ -65,7 +67,7 @@ const reducers = combineReducers({ health, notifications, workflows, - eventDetails, + eventDetails: persistReducer(eventDetailsPersistConfig, eventDetails), themeDetails, seriesDetails, recordingDetails, diff --git a/src/utils/urlStorage.ts b/src/utils/urlStorage.ts index 6f1457ff0e..ffa1d5122c 100644 --- a/src/utils/urlStorage.ts +++ b/src/utils/urlStorage.ts @@ -10,7 +10,7 @@ const storage = { }), removeItem: async (key: string) => updateParams(params => { params.delete(key); - }) + }), } satisfies WebStorage; export default storage; From 45622f75e619ec39c59ff2439f1a97bf72f73c9c Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 10:54:11 +0100 Subject: [PATCH 04/14] Move series details modal page into Redux --- .../events/partials/modals/SeriesDetails.tsx | 8 +++++--- src/selectors/seriesDetailsSelectors.ts | 4 ++++ src/slices/seriesDetailsSlice.ts | 16 ++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/components/events/partials/modals/SeriesDetails.tsx b/src/components/events/partials/modals/SeriesDetails.tsx index 228d1212ef..37e4443aa4 100644 --- a/src/components/events/partials/modals/SeriesDetails.tsx +++ b/src/components/events/partials/modals/SeriesDetails.tsx @@ -1,7 +1,8 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import cn from "classnames"; import { + getModalPage, getSeriesDetailsExtendedMetadata, getSeriesDetailsFeeds, getSeriesDetailsMetadata, @@ -19,6 +20,7 @@ import DetailsMetadataTab from "../ModalTabsAndPages/DetailsMetadataTab"; import DetailsExtendedMetadataTab from "../ModalTabsAndPages/DetailsExtendedMetadataTab"; import { useAppDispatch, useAppSelector } from "../../../../store"; import { + setModalPage, fetchSeriesStatistics, updateExtendedSeriesMetadata, updateSeriesMetadata, @@ -52,7 +54,7 @@ const SeriesDetails = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const [page, setPage] = useState(0); + const page = useAppSelector(state => getModalPage(state)); const user = useAppSelector(state => getUserInformation(state)); const orgProperties = useAppSelector(state => getOrgProperties(state)); @@ -97,7 +99,7 @@ const SeriesDetails = ({ ]; const openTab = (tabNr: number) => { - setPage(tabNr); + dispatch(setModalPage(tabNr)); }; return ( diff --git a/src/selectors/seriesDetailsSelectors.ts b/src/selectors/seriesDetailsSelectors.ts index 5b269ce0d5..152bc83dc6 100644 --- a/src/selectors/seriesDetailsSelectors.ts +++ b/src/selectors/seriesDetailsSelectors.ts @@ -3,6 +3,10 @@ import { RootState } from "../store"; /** * This file contains selectors regarding details of a certain series */ + +/* Selectors for the modal */ +export const getModalPage = (state: RootState) => state.seriesDetails.modal.page; + export const getSeriesDetailsMetadata = (state: RootState) => state.seriesDetails.metadata; export const getSeriesDetailsExtendedMetadata = (state: RootState) => state.seriesDetails.extendedMetadata; export const getSeriesDetailsAcl = (state: RootState) => state.seriesDetails.acl; diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index 44b0c87841..ac213a0289 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -30,6 +30,12 @@ export type Feed = { version: string, } +// Contains the navigation logic for the modal +type SeriesDetailsModal = { + page: number, +} + + type SeriesDetailsState = { statusMetadata: 'uninitialized' | 'loading' | 'succeeded' | 'failed', errorMetadata: SerializedError | null, @@ -66,6 +72,7 @@ type SeriesDetailsState = { }[], }[], }, + modal: SeriesDetailsModal, } // Initial state of series details in redux store @@ -103,6 +110,9 @@ const initialState: SeriesDetailsState = { baseURL: "", hostPages: [], }, + modal: { + page: 0, + }, }; // fetch metadata of certain series from server @@ -499,6 +509,11 @@ const seriesDetailsSlice = createSlice({ name: 'seriesDetails', initialState, reducers: { + setModalPage(state, action: PayloadAction< + SeriesDetailsState["modal"]["page"] + >) { + state.modal.page = action.payload; + }, setSeriesDetailsTheme(state, action: PayloadAction< SeriesDetailsState["theme"] >) { @@ -646,6 +661,7 @@ const seriesDetailsSlice = createSlice({ }); export const { + setModalPage, setSeriesDetailsTheme, setSeriesDetailsMetadata, setSeriesDetailsExtendedMetadata, From 64715d7696556c914c06367bbddf31e42073752c Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 11:40:47 +0100 Subject: [PATCH 05/14] Always open series details modal on the first page again Due to the previous commit, the current modal page is no longer local state that is cleaned up when the series details modal is unmounted. --- src/components/events/partials/EventActionCell.tsx | 13 ++++++++----- .../events/partials/SeriesActionsCell.tsx | 3 +++ src/slices/seriesDetailsSlice.ts | 7 ++++++- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/events/partials/EventActionCell.tsx b/src/components/events/partials/EventActionCell.tsx index 0654b63cf2..c3f3228fdc 100644 --- a/src/components/events/partials/EventActionCell.tsx +++ b/src/components/events/partials/EventActionCell.tsx @@ -8,6 +8,7 @@ import SeriesDetailsModal from "./modals/SeriesDetailsModal"; import { EventDetailsPage } from "./modals/EventDetails"; import { useAppDispatch, useAppSelector } from "../../../store"; import { + openModal as openSeriesModal, fetchSeriesDetailsAcls, fetchSeriesDetailsFeeds, fetchSeriesDetailsMetadata, @@ -16,7 +17,7 @@ import { } from "../../../slices/seriesDetailsSlice"; import { Event, deleteEvent } from "../../../slices/eventSlice"; import { Tooltip } from "../../shared/Tooltip"; -import { openModal } from "../../../slices/eventDetailsSlice"; +import { openModal as openEventModal } from "../../../slices/eventDetailsSlice"; /** * This component renders the action cells of events in the table view @@ -67,24 +68,26 @@ const EventActionCell = ({ await dispatch(fetchSeriesDetailsTheme(row.series.id)); await dispatch(fetchSeriesDetailsThemeNames()); + dispatch(openSeriesModal()); + showSeriesDetailsModal(); } }; const onClickEventDetails = () => { - dispatch(openModal(EventDetailsPage.Metadata, row)); + dispatch(openEventModal(EventDetailsPage.Metadata, row)); }; const onClickComments = () => { - dispatch(openModal(EventDetailsPage.Comments, row)); + dispatch(openEventModal(EventDetailsPage.Comments, row)); }; const onClickWorkflow = () => { - dispatch(openModal(EventDetailsPage.Workflow, row)); + dispatch(openEventModal(EventDetailsPage.Workflow, row)); }; const onClickAssets = () => { - dispatch(openModal(EventDetailsPage.Assets, row)); + dispatch(openEventModal(EventDetailsPage.Assets, row)); }; return ( diff --git a/src/components/events/partials/SeriesActionsCell.tsx b/src/components/events/partials/SeriesActionsCell.tsx index 6315c17ba8..d94f2ad6b6 100644 --- a/src/components/events/partials/SeriesActionsCell.tsx +++ b/src/components/events/partials/SeriesActionsCell.tsx @@ -9,6 +9,7 @@ import { fetchSeriesDetailsMetadata, fetchSeriesDetailsTheme, fetchSeriesDetailsTobira, + openModal, } from "../../../slices/seriesDetailsSlice"; import { getUserInformation } from "../../../selectors/userInfoSelectors"; import { hasAccess } from "../../../utils/utils"; @@ -69,6 +70,8 @@ const SeriesActionsCell = ({ await dispatch(fetchSeriesDetailsThemeNames()); await dispatch(fetchSeriesDetailsTobira(row.id)); + dispatch(openModal()); + setSeriesDetailsModal(true); }; diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index ac213a0289..7ee4618c5c 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -20,6 +20,7 @@ import { Statistics, fetchStatistics, fetchStatisticsValueUpdate } from './stati import { Ace } from './aclSlice'; import { TransformedAcl } from './aclDetailsSlice'; import { MetadataCatalog } from './eventSlice'; +import { AppDispatch } from '../store'; /** * This file contains redux reducer for actions affecting the state of a series @@ -30,12 +31,12 @@ export type Feed = { version: string, } + // Contains the navigation logic for the modal type SeriesDetailsModal = { page: number, } - type SeriesDetailsState = { statusMetadata: 'uninitialized' | 'loading' | 'succeeded' | 'failed', errorMetadata: SerializedError | null, @@ -115,6 +116,10 @@ const initialState: SeriesDetailsState = { }, }; +export const openModal = () => (dispatch: AppDispatch) => { + dispatch(setModalPage(0)); +}; + // fetch metadata of certain series from server export const fetchSeriesDetailsMetadata = createAppAsyncThunk('seriesDetails/fetchSeriesDetailsMetadata', async (id: string, { rejectWithValue }) => { const res = await axios.get(`/admin-ng/series/${id}/metadata.json`); From a9ea39a78c7b21446448a758ddae9c0450765f56 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 11:44:11 +0100 Subject: [PATCH 06/14] Pull fetching series details into the new `openModal` --- .../events/partials/EventActionCell.tsx | 18 ++--------------- .../events/partials/SeriesActionsCell.tsx | 20 ++----------------- src/slices/seriesDetailsSlice.ts | 8 +++++++- 3 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/components/events/partials/EventActionCell.tsx b/src/components/events/partials/EventActionCell.tsx index c3f3228fdc..e81735d7ae 100644 --- a/src/components/events/partials/EventActionCell.tsx +++ b/src/components/events/partials/EventActionCell.tsx @@ -7,14 +7,7 @@ import { hasAccess } from "../../../utils/utils"; import SeriesDetailsModal from "./modals/SeriesDetailsModal"; import { EventDetailsPage } from "./modals/EventDetails"; import { useAppDispatch, useAppSelector } from "../../../store"; -import { - openModal as openSeriesModal, - fetchSeriesDetailsAcls, - fetchSeriesDetailsFeeds, - fetchSeriesDetailsMetadata, - fetchSeriesDetailsTheme, - fetchSeriesDetailsThemeNames, -} from "../../../slices/seriesDetailsSlice"; +import { openModal as openSeriesModal } from "../../../slices/seriesDetailsSlice"; import { Event, deleteEvent } from "../../../slices/eventSlice"; import { Tooltip } from "../../shared/Tooltip"; import { openModal as openEventModal } from "../../../slices/eventDetailsSlice"; @@ -62,14 +55,7 @@ const EventActionCell = ({ const onClickSeriesDetails = async () => { if (!!row.series) { - await dispatch(fetchSeriesDetailsMetadata(row.series.id)); - await dispatch(fetchSeriesDetailsAcls(row.series.id)); - await dispatch(fetchSeriesDetailsFeeds(row.series.id)); - await dispatch(fetchSeriesDetailsTheme(row.series.id)); - await dispatch(fetchSeriesDetailsThemeNames()); - - dispatch(openSeriesModal()); - + await dispatch(openSeriesModal(row.series.id)); showSeriesDetailsModal(); } }; diff --git a/src/components/events/partials/SeriesActionsCell.tsx b/src/components/events/partials/SeriesActionsCell.tsx index d94f2ad6b6..d0cd336f29 100644 --- a/src/components/events/partials/SeriesActionsCell.tsx +++ b/src/components/events/partials/SeriesActionsCell.tsx @@ -2,15 +2,7 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import ConfirmModal from "../../shared/ConfirmModal"; import SeriesDetailsModal from "./modals/SeriesDetailsModal"; -import { - fetchSeriesDetailsThemeNames, - fetchSeriesDetailsAcls, - fetchSeriesDetailsFeeds, - fetchSeriesDetailsMetadata, - fetchSeriesDetailsTheme, - fetchSeriesDetailsTobira, - openModal, -} from "../../../slices/seriesDetailsSlice"; +import { openModal } from "../../../slices/seriesDetailsSlice"; import { getUserInformation } from "../../../selectors/userInfoSelectors"; import { hasAccess } from "../../../utils/utils"; import { @@ -63,15 +55,7 @@ const SeriesActionsCell = ({ }; const showSeriesDetailsModal = async () => { - await dispatch(fetchSeriesDetailsMetadata(row.id)); - await dispatch(fetchSeriesDetailsAcls(row.id)); - await dispatch(fetchSeriesDetailsFeeds(row.id)); - await dispatch(fetchSeriesDetailsTheme(row.id)); - await dispatch(fetchSeriesDetailsThemeNames()); - await dispatch(fetchSeriesDetailsTobira(row.id)); - - dispatch(openModal()); - + await dispatch(openModal(row.id)); setSeriesDetailsModal(true); }; diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index 7ee4618c5c..7b2c22fd78 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -116,7 +116,13 @@ const initialState: SeriesDetailsState = { }, }; -export const openModal = () => (dispatch: AppDispatch) => { +export const openModal = (id: string) => async (dispatch: AppDispatch) => { + await dispatch(fetchSeriesDetailsMetadata(id)); + await dispatch(fetchSeriesDetailsAcls(id)); + await dispatch(fetchSeriesDetailsFeeds(id)); + await dispatch(fetchSeriesDetailsTheme(id)); + await dispatch(fetchSeriesDetailsThemeNames()); + await dispatch(fetchSeriesDetailsTobira(id)); dispatch(setModalPage(0)); }; From dc3f9e0af4ccb847b201bb8677d4de03db0c2974 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 15:27:01 +0100 Subject: [PATCH 07/14] Get series for details modal into/from Redux --- src/components/events/partials/EventActionCell.tsx | 4 +--- .../events/partials/SeriesActionsCell.tsx | 4 +--- .../events/partials/modals/SeriesDetailsModal.tsx | 12 ++++++------ src/selectors/seriesDetailsSelectors.ts | 1 + src/slices/seriesDetailsSlice.ts | 13 ++++++++++++- src/slices/seriesSlice.ts | 2 ++ 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/components/events/partials/EventActionCell.tsx b/src/components/events/partials/EventActionCell.tsx index e81735d7ae..8b9ec68c44 100644 --- a/src/components/events/partials/EventActionCell.tsx +++ b/src/components/events/partials/EventActionCell.tsx @@ -55,7 +55,7 @@ const EventActionCell = ({ const onClickSeriesDetails = async () => { if (!!row.series) { - await dispatch(openSeriesModal(row.series.id)); + await dispatch(openSeriesModal(row.series)); showSeriesDetailsModal(); } }; @@ -81,8 +81,6 @@ const EventActionCell = ({ {!!row.series && displaySeriesDetailsModal && ( )} diff --git a/src/components/events/partials/SeriesActionsCell.tsx b/src/components/events/partials/SeriesActionsCell.tsx index d0cd336f29..8554add1e9 100644 --- a/src/components/events/partials/SeriesActionsCell.tsx +++ b/src/components/events/partials/SeriesActionsCell.tsx @@ -55,7 +55,7 @@ const SeriesActionsCell = ({ }; const showSeriesDetailsModal = async () => { - await dispatch(openModal(row.id)); + await dispatch(openModal(row)); setSeriesDetailsModal(true); }; @@ -74,8 +74,6 @@ const SeriesActionsCell = ({ {displaySeriesDetailsModal && ( )} diff --git a/src/components/events/partials/modals/SeriesDetailsModal.tsx b/src/components/events/partials/modals/SeriesDetailsModal.tsx index d89621f03a..0a71d45da4 100644 --- a/src/components/events/partials/modals/SeriesDetailsModal.tsx +++ b/src/components/events/partials/modals/SeriesDetailsModal.tsx @@ -3,24 +3,24 @@ import { useTranslation } from "react-i18next"; import SeriesDetails from "./SeriesDetails"; import { useHotkeys } from "react-hotkeys-hook"; import { availableHotkeys } from "../../../../configs/hotkeysConfig"; +import { useAppSelector } from "../../../../store"; +import { getModalSeries } from "../../../../selectors/seriesDetailsSelectors"; /** * This component renders the modal for displaying series details */ const SeriesDetailsModal = ({ handleClose, - seriesTitle, - seriesId }: { handleClose: () => void - seriesTitle: string - seriesId: string }) => { const { t } = useTranslation(); // tracks, whether the policies are different to the initial value const [policyChanged, setPolicyChanged] = useState(false); + const series = useAppSelector(state => getModalSeries(state))!; + const confirmUnsaved = () => { return window.confirm(t("CONFIRMATIONS.WARNINGS.UNSAVED_CHANGES")); }; @@ -47,12 +47,12 @@ const SeriesDetailsModal = ({
setPolicyChanged(value)} /> diff --git a/src/selectors/seriesDetailsSelectors.ts b/src/selectors/seriesDetailsSelectors.ts index 152bc83dc6..d3ee6c15af 100644 --- a/src/selectors/seriesDetailsSelectors.ts +++ b/src/selectors/seriesDetailsSelectors.ts @@ -6,6 +6,7 @@ import { RootState } from "../store"; /* Selectors for the modal */ export const getModalPage = (state: RootState) => state.seriesDetails.modal.page; +export const getModalSeries = (state: RootState) => state.seriesDetails.modal.series; export const getSeriesDetailsMetadata = (state: RootState) => state.seriesDetails.metadata; export const getSeriesDetailsExtendedMetadata = (state: RootState) => state.seriesDetails.extendedMetadata; diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index 7b2c22fd78..908efc2c96 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -21,6 +21,7 @@ import { Ace } from './aclSlice'; import { TransformedAcl } from './aclDetailsSlice'; import { MetadataCatalog } from './eventSlice'; import { AppDispatch } from '../store'; +import { PartialSeries } from './seriesSlice'; /** * This file contains redux reducer for actions affecting the state of a series @@ -35,6 +36,7 @@ export type Feed = { // Contains the navigation logic for the modal type SeriesDetailsModal = { page: number, + series: PartialSeries | null, } type SeriesDetailsState = { @@ -113,16 +115,19 @@ const initialState: SeriesDetailsState = { }, modal: { page: 0, + series: null, }, }; -export const openModal = (id: string) => async (dispatch: AppDispatch) => { +export const openModal = (series: PartialSeries) => async (dispatch: AppDispatch) => { + const { id } = series; await dispatch(fetchSeriesDetailsMetadata(id)); await dispatch(fetchSeriesDetailsAcls(id)); await dispatch(fetchSeriesDetailsFeeds(id)); await dispatch(fetchSeriesDetailsTheme(id)); await dispatch(fetchSeriesDetailsThemeNames()); await dispatch(fetchSeriesDetailsTobira(id)); + dispatch(setModalSeries(series)); dispatch(setModalPage(0)); }; @@ -525,6 +530,11 @@ const seriesDetailsSlice = createSlice({ >) { state.modal.page = action.payload; }, + setModalSeries(state, action: PayloadAction< + SeriesDetailsState["modal"]["series"] + >) { + state.modal.series = action.payload; + }, setSeriesDetailsTheme(state, action: PayloadAction< SeriesDetailsState["theme"] >) { @@ -673,6 +683,7 @@ const seriesDetailsSlice = createSlice({ export const { setModalPage, + setModalSeries, setSeriesDetailsTheme, setSeriesDetailsMetadata, setSeriesDetailsExtendedMetadata, diff --git a/src/slices/seriesSlice.ts b/src/slices/seriesSlice.ts index 3974e0d54c..959612205f 100644 --- a/src/slices/seriesSlice.ts +++ b/src/slices/seriesSlice.ts @@ -35,6 +35,8 @@ export type Series = { title: string, } +export type PartialSeries = Pick; + type Theme = { description: string, id: string, From 4087b6abc149facc107cec4fe3dd8e94874d547d Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 15:53:19 +0100 Subject: [PATCH 08/14] Put series details modal display state into Redux This necessitates moving the actual rendering of said modal from the individual action cells into the main tables, so that we don't render multiple instances. --- src/components/events/Events.tsx | 15 ++++++++++++--- src/components/events/Series.tsx | 11 +++++++++++ .../events/partials/EventActionCell.tsx | 17 ----------------- .../events/partials/SeriesActionsCell.tsx | 19 +------------------ src/selectors/seriesDetailsSelectors.ts | 1 + src/slices/seriesDetailsSlice.ts | 9 +++++++++ 6 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/components/events/Events.tsx b/src/components/events/Events.tsx index 33904e4c1b..02094f80e8 100644 --- a/src/components/events/Events.tsx +++ b/src/components/events/Events.tsx @@ -42,7 +42,10 @@ import { } from "../../slices/eventSlice"; import { fetchSeries } from "../../slices/seriesSlice"; import EventDetailsModal from "./partials/modals/EventDetailsModal"; -import { showModal } from "../../selectors/eventDetailsSelectors"; +import { showModal as showEventModal } from "../../selectors/eventDetailsSelectors"; +import { showModal as showSeriesModal } from "../../selectors/seriesDetailsSelectors"; +import SeriesDetailsModal from "./partials/modals/SeriesDetailsModal"; +import { setShowModal as setShowSeriesModal } from "../../slices/seriesDetailsSlice"; // References for detecting a click outside of the container of the dropdown menu const containerAction = React.createRef(); @@ -55,7 +58,8 @@ const Events = () => { const dispatch = useAppDispatch(); const currentFilterType = useAppSelector(state => getCurrentFilterResource(state)); - const displayEventDetailsModal = useAppSelector(state => showModal(state)); + const displayEventDetailsModal = useAppSelector(state => showEventModal(state)); + const displaySeriesDetailsModal = useAppSelector(state => showSeriesModal(state)); const [displayActionMenu, setActionMenu] = useState(false); const [displayNavigation, setNavigation] = useState(false); @@ -308,10 +312,15 @@ const Events = () => {

{t("TABLE_SUMMARY", { numberOfRows: events })}

- {/*Include table modal*/} + {/*Include table modals*/} {displayEventDetailsModal && } + {displaySeriesDetailsModal && + { + dispatch(setShowSeriesModal(false)); + }} /> + } {/*Include table component*/} {/*
*/} diff --git a/src/components/events/Series.tsx b/src/components/events/Series.tsx index 36bee650de..f918f9c238 100644 --- a/src/components/events/Series.tsx +++ b/src/components/events/Series.tsx @@ -34,6 +34,9 @@ import { showActionsSeries, } from "../../slices/seriesSlice"; import { fetchSeriesDetailsTobiraNew } from "../../slices/seriesSlice"; +import { showModal } from "../../selectors/seriesDetailsSelectors"; +import SeriesDetailsModal from "./partials/modals/SeriesDetailsModal"; +import { setShowModal } from "../../slices/seriesDetailsSlice"; // References for detecting a click outside of the container of the dropdown menu const containerAction = React.createRef(); @@ -51,6 +54,7 @@ const Series = () => { const user = useAppSelector(state => getUserInformation(state)); const currentFilterType = useAppSelector(state => getCurrentFilterResource(state)); + const displaySeriesDetailsModal = useAppSelector(state => showModal(state)); let location = useLocation(); @@ -234,6 +238,13 @@ const Series = () => { {/* Include table view */}

{t("TABLE_SUMMARY", { numberOfRows: series })}

+ + {displaySeriesDetailsModal && + { + dispatch(setShowModal(false)); + }} /> + } +
diff --git a/src/components/events/partials/EventActionCell.tsx b/src/components/events/partials/EventActionCell.tsx index 8b9ec68c44..620d01770a 100644 --- a/src/components/events/partials/EventActionCell.tsx +++ b/src/components/events/partials/EventActionCell.tsx @@ -4,7 +4,6 @@ import ConfirmModal from "../../shared/ConfirmModal"; import EmbeddingCodeModal from "./modals/EmbeddingCodeModal"; import { getUserInformation } from "../../../selectors/userInfoSelectors"; import { hasAccess } from "../../../utils/utils"; -import SeriesDetailsModal from "./modals/SeriesDetailsModal"; import { EventDetailsPage } from "./modals/EventDetails"; import { useAppDispatch, useAppSelector } from "../../../store"; import { openModal as openSeriesModal } from "../../../slices/seriesDetailsSlice"; @@ -24,7 +23,6 @@ const EventActionCell = ({ const dispatch = useAppDispatch(); const [displayDeleteConfirmation, setDeleteConfirmation] = useState(false); - const [displaySeriesDetailsModal, setSeriesDetailsModal] = useState(false); const [displayEmbeddingCodeModal, setEmbeddingCodeModal] = useState(false); const user = useAppSelector(state => getUserInformation(state)); @@ -45,18 +43,9 @@ const EventActionCell = ({ setEmbeddingCodeModal(true); }; - const showSeriesDetailsModal = () => { - setSeriesDetailsModal(true); - }; - - const hideSeriesDetailsModal = () => { - setSeriesDetailsModal(false); - }; - const onClickSeriesDetails = async () => { if (!!row.series) { await dispatch(openSeriesModal(row.series)); - showSeriesDetailsModal(); } }; @@ -78,12 +67,6 @@ const EventActionCell = ({ return ( <> - {!!row.series && displaySeriesDetailsModal && ( - - )} - {/* Open event details */} {hasAccess("ROLE_UI_EVENTS_DETAILS_VIEW", user) && ( diff --git a/src/components/events/partials/SeriesActionsCell.tsx b/src/components/events/partials/SeriesActionsCell.tsx index 8554add1e9..0b858b70ce 100644 --- a/src/components/events/partials/SeriesActionsCell.tsx +++ b/src/components/events/partials/SeriesActionsCell.tsx @@ -1,7 +1,6 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import ConfirmModal from "../../shared/ConfirmModal"; -import SeriesDetailsModal from "./modals/SeriesDetailsModal"; import { openModal } from "../../../slices/seriesDetailsSlice"; import { getUserInformation } from "../../../selectors/userInfoSelectors"; import { hasAccess } from "../../../utils/utils"; @@ -30,7 +29,6 @@ const SeriesActionsCell = ({ const dispatch = useAppDispatch(); const [displayDeleteConfirmation, setDeleteConfirmation] = useState(false); - const [displaySeriesDetailsModal, setSeriesDetailsModal] = useState(false); const user = useAppSelector(state => getUserInformation(state)); const hasEvents = useAppSelector(state => getSeriesHasEvents(state)); @@ -50,33 +48,18 @@ const SeriesActionsCell = ({ dispatch(deleteSeries(id)); }; - const hideSeriesDetailsModal = () => { - setSeriesDetailsModal(false); - }; - - const showSeriesDetailsModal = async () => { - await dispatch(openModal(row)); - setSeriesDetailsModal(true); - }; - return ( <> {/* series details */} {hasAccess("ROLE_UI_SERIES_DETAILS_VIEW", user) && (
diff --git a/src/components/events/partials/modals/EventDetails.tsx b/src/components/events/partials/modals/EventDetails.tsx index 19d5ed136b..70a52c0f7d 100644 --- a/src/components/events/partials/modals/EventDetails.tsx +++ b/src/components/events/partials/modals/EventDetails.tsx @@ -62,12 +62,10 @@ export type AssetTabHierarchy = "entry" | "add-asset" | "asset-attachments" | "a */ const EventDetails = ({ eventId, - close, policyChanged, setPolicyChanged, }: { eventId: string, - close?: () => void, policyChanged: boolean, setPolicyChanged: (value: boolean) => void, }) => { diff --git a/src/components/events/partials/modals/SeriesDetailsModal.tsx b/src/components/events/partials/modals/SeriesDetailsModal.tsx index 0a71d45da4..73edfd04e6 100644 --- a/src/components/events/partials/modals/SeriesDetailsModal.tsx +++ b/src/components/events/partials/modals/SeriesDetailsModal.tsx @@ -3,18 +3,16 @@ import { useTranslation } from "react-i18next"; import SeriesDetails from "./SeriesDetails"; import { useHotkeys } from "react-hotkeys-hook"; import { availableHotkeys } from "../../../../configs/hotkeysConfig"; -import { useAppSelector } from "../../../../store"; +import { useAppDispatch, useAppSelector } from "../../../../store"; import { getModalSeries } from "../../../../selectors/seriesDetailsSelectors"; +import { setShowModal } from "../../../../slices/seriesDetailsSlice"; /** * This component renders the modal for displaying series details */ -const SeriesDetailsModal = ({ - handleClose, -}: { - handleClose: () => void -}) => { +const SeriesDetailsModal = () => { const { t } = useTranslation(); + const dispatch = useAppDispatch(); // tracks, whether the policies are different to the initial value const [policyChanged, setPolicyChanged] = useState(false); @@ -28,7 +26,7 @@ const SeriesDetailsModal = ({ const close = () => { if (!policyChanged || confirmUnsaved()) { setPolicyChanged(false); - handleClose(); + dispatch(setShowModal(false)); } }; From b12d2d588ded503fb9e2ad69d4d081e1e5c376a1 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Thu, 14 Nov 2024 16:23:16 +0100 Subject: [PATCH 10/14] Persist series details modal state in the URL --- src/store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/store.ts b/src/store.ts index 4cbec869c8..76df57e07c 100644 --- a/src/store.ts +++ b/src/store.ts @@ -48,6 +48,7 @@ const groupsPersistConfig = { key: "groups", storage, whitelist: ["columns"] } const aclsPersistConfig = { key: "acls", storage, whitelist: ["columns"] } const themesPersistConfig = { key: "themes", storage, whitelist: ["columns"] } const eventDetailsPersistConfig = { key: "eventDetails", storage: urlStorage, whitelist: ["modal"]} +const seriesDetailsPersistConfig = { key: "seriesDetails", storage: urlStorage, whitelist: ["modal"]} // form reducer and all other reducers used in this app const reducers = combineReducers({ @@ -69,7 +70,7 @@ const reducers = combineReducers({ workflows, eventDetails: persistReducer(eventDetailsPersistConfig, eventDetails), themeDetails, - seriesDetails, + seriesDetails: persistReducer(seriesDetailsPersistConfig, seriesDetails), recordingDetails, userDetails, groupDetails, From 1caa8916f3782ad5d275534059e004330da4bc64 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Tue, 19 Nov 2024 12:29:47 +0100 Subject: [PATCH 11/14] Fix appending route hash after persisting state to the URL This lead to the app not remembering what page (events vs. series) the user was on. --- src/utils/urlStorage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/urlStorage.ts b/src/utils/urlStorage.ts index ffa1d5122c..59ae66a3d7 100644 --- a/src/utils/urlStorage.ts +++ b/src/utils/urlStorage.ts @@ -27,7 +27,7 @@ const updateParams = (fn: (params: URLSearchParams) => void) => { window.location.pathname }?${ params.toString() - }#${ + }${ window.location.hash }`); }); From 74925a5b5e0a86285fb61348959532860a0a24fe Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Tue, 19 Nov 2024 14:57:30 +0100 Subject: [PATCH 12/14] Remove unused `fetchingStatisticsInProgress` Redux state field --- src/selectors/seriesDetailsSelectors.ts | 2 +- src/slices/seriesDetailsSlice.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/selectors/seriesDetailsSelectors.ts b/src/selectors/seriesDetailsSelectors.ts index e58e1377bf..95dc85e213 100644 --- a/src/selectors/seriesDetailsSelectors.ts +++ b/src/selectors/seriesDetailsSelectors.ts @@ -29,4 +29,4 @@ export const getStatistics = (state: RootState) => state.seriesDetails.statistic export const hasStatisticsError = (state: RootState) => state.seriesDetails.hasStatisticsError; export const isFetchingStatistics = (state: RootState) => - state.seriesDetails.fetchingStatisticsInProgress; + state.seriesDetails.statusStatistics === 'loading'; diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index 9743b8ba53..8a987f3bcb 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -63,7 +63,6 @@ type SeriesDetailsState = { acl: TransformedAcl[], theme: string, themeNames: { id: string, value: string }[], - fetchingStatisticsInProgress: boolean, statistics: Statistics[], hasStatisticsError: boolean, tobiraData: { @@ -107,7 +106,6 @@ const initialState: SeriesDetailsState = { acl: [], theme: "", themeNames: [], - fetchingStatisticsInProgress: false, statistics: [], hasStatisticsError: false, tobiraData: { From 2e3816957be153aec736af104f41d00b72d3f0ae Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Mon, 25 Nov 2024 16:31:56 +0100 Subject: [PATCH 13/14] Refactor series details data fetching This makes the series details modal work when coming from a "direct link." It also makes it work more like the events modal. --- .../events/partials/modals/SeriesDetails.tsx | 36 +++++++++++++++---- src/selectors/seriesDetailsSelectors.ts | 9 +++++ src/slices/seriesDetailsSlice.ts | 6 ---- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/components/events/partials/modals/SeriesDetails.tsx b/src/components/events/partials/modals/SeriesDetails.tsx index 37e4443aa4..b6a76db3f0 100644 --- a/src/components/events/partials/modals/SeriesDetails.tsx +++ b/src/components/events/partials/modals/SeriesDetails.tsx @@ -8,6 +8,11 @@ import { getSeriesDetailsMetadata, getSeriesDetailsTheme, getSeriesDetailsThemeNames, + isFetchingFeeds, + isFetchingMetadata, + isFetchingStatistics, + isFetchingThemes, + isFetchingTobiraData, hasStatistics as seriesHasStatistics, } from "../../../../selectors/seriesDetailsSelectors"; import { getOrgProperties, getUserInformation } from "../../../../selectors/userInfoSelectors"; @@ -24,6 +29,11 @@ import { fetchSeriesStatistics, updateExtendedSeriesMetadata, updateSeriesMetadata, + fetchSeriesDetailsMetadata, + fetchSeriesDetailsTheme, + fetchSeriesDetailsThemeNames, + fetchSeriesDetailsFeeds, + fetchSeriesDetailsTobira, } from "../../../../slices/seriesDetailsSlice"; import SeriesDetailsTobiraTab from "../ModalTabsAndPages/SeriesDetailsTobiraTab"; @@ -42,15 +52,25 @@ const SeriesDetails = ({ const { t } = useTranslation(); const dispatch = useAppDispatch(); + const metadataFields = useAppSelector(state => getSeriesDetailsMetadata(state)); const extendedMetadata = useAppSelector(state => getSeriesDetailsExtendedMetadata(state)); + const isLoadingMetadata = useAppSelector(state => isFetchingMetadata(state)); const feeds = useAppSelector(state => getSeriesDetailsFeeds(state)); - const metadataFields = useAppSelector(state => getSeriesDetailsMetadata(state)); + const isLoadingFeeds = useAppSelector(state => isFetchingFeeds(state)); const theme = useAppSelector(state => getSeriesDetailsTheme(state)); const themeNames = useAppSelector(state => getSeriesDetailsThemeNames(state)); + const isLoadingThemes = useAppSelector(state => isFetchingThemes(state)); const hasStatistics = useAppSelector(state => seriesHasStatistics(state)); + const isLoadingStatistics = useAppSelector(state => isFetchingStatistics(state)); + const isLoadingTobiraData = useAppSelector(state => isFetchingTobiraData(state)); useEffect(() => { + dispatch(fetchSeriesDetailsMetadata(seriesId)); dispatch(fetchSeriesStatistics(seriesId)); + dispatch(fetchSeriesDetailsTheme(seriesId)); + dispatch(fetchSeriesDetailsFeeds(seriesId)); + dispatch(fetchSeriesDetailsTobira(seriesId)); + dispatch(fetchSeriesDetailsThemeNames()); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -145,7 +165,7 @@ const SeriesDetails = ({ {/* render modal content depending on current page */}
- {page === 0 && ( + {page === 0 && !isLoadingMetadata && ( )} - {page === 1 && ( + {page === 1 && !isLoadingMetadata && ( )} - {page === 3 && ( + {page === 3 && !isLoadingThemes && ( )} - {page === 4 && ( + {page === 4 && !isLoadingTobiraData && ( )} - {page === 5 && ( + {page === 5 && !isLoadingStatistics && ( )} - {page === 6 && } + {page === 6 && !isLoadingFeeds && ( + + )}
); diff --git a/src/selectors/seriesDetailsSelectors.ts b/src/selectors/seriesDetailsSelectors.ts index 95dc85e213..9ee509ff31 100644 --- a/src/selectors/seriesDetailsSelectors.ts +++ b/src/selectors/seriesDetailsSelectors.ts @@ -11,16 +11,25 @@ export const getModalSeries = (state: RootState) => state.seriesDetails.modal.se export const getSeriesDetailsMetadata = (state: RootState) => state.seriesDetails.metadata; export const getSeriesDetailsExtendedMetadata = (state: RootState) => state.seriesDetails.extendedMetadata; +export const isFetchingMetadata = (state: RootState) => + state.seriesDetails.statusMetadata === 'loading'; export const getSeriesDetailsAcl = (state: RootState) => state.seriesDetails.acl; + export const getSeriesDetailsFeeds = (state: RootState) => state.seriesDetails.feeds; +export const isFetchingFeeds = (state: RootState) => state.seriesDetails.statusFeeds === 'loading'; + export const getSeriesDetailsTheme = (state: RootState) => state.seriesDetails.theme; export const getSeriesDetailsThemeNames = (state: RootState) => state.seriesDetails.themeNames; +export const isFetchingThemes = (state: RootState) => + state.seriesDetails.statusTheme === 'loading'; export const getSeriesDetailsTobiraData = (state: RootState) => state.seriesDetails.tobiraData export const getSeriesDetailsTobiraDataError = (state: RootState) => state.seriesDetails.errorTobiraData +export const isFetchingTobiraData = (state: RootState) => + state.seriesDetails.statusTobiraData === 'loading'; /* selectors for statistics */ export const hasStatistics = (state: RootState) => diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index 8a987f3bcb..330fb3eab1 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -121,12 +121,6 @@ const initialState: SeriesDetailsState = { export const openModal = (series: PartialSeries) => async (dispatch: AppDispatch) => { const { id } = series; - await dispatch(fetchSeriesDetailsMetadata(id)); - await dispatch(fetchSeriesDetailsAcls(id)); - await dispatch(fetchSeriesDetailsFeeds(id)); - await dispatch(fetchSeriesDetailsTheme(id)); - await dispatch(fetchSeriesDetailsThemeNames()); - await dispatch(fetchSeriesDetailsTobira(id)); dispatch(setModalSeries(series)); dispatch(setModalPage(0)); dispatch(setShowModal(true)); From 8ac81a4bb787d65983cb323fddf23fb15048c078 Mon Sep 17 00:00:00 2001 From: Julian Kniephoff Date: Mon, 25 Nov 2024 16:58:21 +0100 Subject: [PATCH 14/14] Fix lint --- src/slices/seriesDetailsSlice.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/slices/seriesDetailsSlice.ts b/src/slices/seriesDetailsSlice.ts index b5988cebef..fad144967d 100644 --- a/src/slices/seriesDetailsSlice.ts +++ b/src/slices/seriesDetailsSlice.ts @@ -120,7 +120,6 @@ const initialState: SeriesDetailsState = { }; export const openModal = (series: PartialSeries) => async (dispatch: AppDispatch) => { - const { id } = series; dispatch(setModalSeries(series)); dispatch(setModalPage(0)); dispatch(setTobiraTabHierarchy("main"));