From 9b3556d358b3cb254ec50106e144662d146e981d Mon Sep 17 00:00:00 2001 From: Ole Wieners Date: Wed, 26 Feb 2025 00:33:34 +0100 Subject: [PATCH] Add notification context for deletion messages This could also be used for other purposes, but is currently only used for displaying a message on the "My series" table when a series gets deleted. The notification will clear when the user navigates away from the table. This commit also fixes the table layout on narrow screens. Previously, the table would squish the "Title" column and overlap it with the one right next to it. Now each column uses a fixed width on smaller screens and adds a scrollbar, it it gets too crammed. --- frontend/src/App.tsx | 13 +++-- frontend/src/i18n/locales/de.yaml | 1 + frontend/src/i18n/locales/en.yaml | 1 + .../routes/manage/Series/SeriesDetails.tsx | 15 ++--- frontend/src/routes/manage/Shared/Details.tsx | 10 ++++ frontend/src/routes/manage/Shared/Table.tsx | 27 +++++++-- .../src/routes/manage/Video/VideoDetails.tsx | 1 + frontend/src/ui/NotificationContext.tsx | 56 +++++++++++++++++++ 8 files changed, 105 insertions(+), 19 deletions(-) create mode 100644 frontend/src/ui/NotificationContext.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index cea82f4ac..61c5d3b75 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -20,6 +20,7 @@ import { import { COLORS } from "./color"; import { InitialConsent } from "./ui/InitialConsent"; import { InitialLoading } from "./layout/Root"; +import { NotificationProvider } from "./ui/NotificationContext"; type Props = { @@ -38,11 +39,13 @@ export const App: React.FC = ({ initialRoute, consentGiven }) => ( - - - }> - - + + + + }> + + + diff --git a/frontend/src/i18n/locales/de.yaml b/frontend/src/i18n/locales/de.yaml index 17ce6ad3e..a45d16166 100644 --- a/frontend/src/i18n/locales/de.yaml +++ b/frontend/src/i18n/locales/de.yaml @@ -463,6 +463,7 @@ manage: failed: Löschen fehlgeschlagen. failed-maybe: Löschen möglicherweise fehlgeschlagen. pending: Löschvorgang ausstehend. + in-progress: '„{{itemTitle}}“ wird gelöscht.' tooltip: failed: > Der Löschvorgang für diesen Eintrag wurde {{time}} angesetzt, ist aber bisher nicht erfolgt diff --git a/frontend/src/i18n/locales/en.yaml b/frontend/src/i18n/locales/en.yaml index af0c90007..c8cc40c62 100644 --- a/frontend/src/i18n/locales/en.yaml +++ b/frontend/src/i18n/locales/en.yaml @@ -444,6 +444,7 @@ manage: cannot-be-undone: This action cannot be undone! failed: Deleting the {{item}} failed. failed-maybe: Deleting the {{item}} possibly failed. + in-progress: '“{{itemTitle}}” is being deleted.' pending: Deletion is pending. tooltip: failed: > diff --git a/frontend/src/routes/manage/Series/SeriesDetails.tsx b/frontend/src/routes/manage/Series/SeriesDetails.tsx index d797c01c2..a8f412f44 100644 --- a/frontend/src/routes/manage/Series/SeriesDetails.tsx +++ b/frontend/src/routes/manage/Series/SeriesDetails.tsx @@ -13,9 +13,6 @@ import { DeleteButton, HostRealms, } from "../Shared/Details"; -import { - SeriesDetailsMetadataMutation, -} from "./__generated__/SeriesDetailsMetadataMutation.graphql"; import { Link } from "../../../router"; @@ -43,7 +40,7 @@ export const ManageSeriesDetailsRoute = makeManageSeriesRoute( }} sections={series => [ , - , + , , @@ -59,13 +56,14 @@ export const ManageSeriesDetailsRoute = makeManageSeriesRoute( />, ); -const SeriesButtonSection: React.FC<{ seriesId: string }> = ({ seriesId }) => { +const SeriesButtonSection: React.FC<{ series: Series }> = ({ series }) => { const { t } = useTranslation(); const [commit] = useMutation(deleteSeriesMutation); return
{ @@ -80,10 +78,9 @@ const SeriesButtonSection: React.FC<{ seriesId: string }> = ({ seriesId }) => { }; const SeriesMetadataSection: React.FC<{ series: Series }> = ({ series }) => { - const [commit, inFlight] - = useMutation(updateSeriesMetadata); + const [commit, inFlight] = useMutation(updateSeriesMetadata); - return + return { diff --git a/frontend/src/routes/manage/Shared/Details.tsx b/frontend/src/routes/manage/Shared/Details.tsx index 3c1e99d3c..647aec916 100644 --- a/frontend/src/routes/manage/Shared/Details.tsx +++ b/frontend/src/routes/manage/Shared/Details.tsx @@ -24,6 +24,7 @@ import { PAGE_WIDTH } from "./Nav"; import { displayCommitError } from "../Realm/util"; import { ConfirmationModal, ConfirmationModalHandle } from "../../../ui/Modal"; import { Link, useRouter } from "../../../router"; +import { useNotification } from "../../../ui/NotificationContext"; type UrlProps = { @@ -218,6 +219,7 @@ export const ButtonSection: React.FC = ({ children }) => ( type DeleteButtonProps = PropsWithChildren & { itemId: string; + itemTitle: string; itemType: "video" | "series"; commit: (config: UseMutationConfig) => Disposable; returnPath: string; @@ -225,14 +227,17 @@ type DeleteButtonProps = PropsWithChildren export const DeleteButton = ({ itemId, + itemTitle, itemType, commit, returnPath, children, }: DeleteButtonProps) => { const { t } = useTranslation(); + const { setNotification } = useNotification(); const modalRef = useRef(null); const router = useRouter(); + const item = t(`manage.shared.item.${itemType}`); const onSubmit = () => { @@ -241,6 +246,11 @@ export const DeleteButton = ({ updater: store => store.invalidateStore(), onCompleted: () => { currentRef(modalRef).done(); + setNotification({ + kind: "info", + message: () => t("manage.shared.delete.in-progress", { itemTitle }), + scope: returnPath, + }); router.goto(returnPath); }, onError: error => { diff --git a/frontend/src/routes/manage/Shared/Table.tsx b/frontend/src/routes/manage/Shared/Table.tsx index 6eb568060..fdc907383 100644 --- a/frontend/src/routes/manage/Shared/Table.tsx +++ b/frontend/src/routes/manage/Shared/Table.tsx @@ -1,4 +1,4 @@ -import { Card, match, useColorScheme } from "@opencast/appkit"; +import { Card, match, screenWidthAtMost, useColorScheme } from "@opencast/appkit"; import { useState, useRef, useEffect, ReactNode, ComponentType } from "react"; import { ParseKeys } from "i18next"; import { useTranslation } from "react-i18next"; @@ -22,6 +22,7 @@ import { Breadcrumbs } from "../../../ui/Breadcrumbs"; import { Link } from "../../../router"; import { VideosSortColumn } from "../Video/__generated__/VideoManageQuery.graphql"; import { SeriesSortColumn } from "../Series/__generated__/SeriesManageQuery.graphql"; +import { useNotification } from "../../../ui/NotificationContext"; type ItemVars = { @@ -63,13 +64,29 @@ export const ManageItems = ({ RenderRow, }: ManageItemProps) => { const { t } = useTranslation(); + const { Notification } = useNotification(); let inner; if (connection.items.length === 0 && connection.totalCount === 0) { - inner = {t("manage.item-table.no-entries-found")}; + inner =
+ + + {t("manage.item-table.no-entries-found")} + +
; } else { inner = <> - +
+ + + + +
@@ -136,7 +153,7 @@ const ItemTable = ({ return () => {}; }); - return
+ return
({ {/* Each table has thumbnails, but their width might vary */} {/* Each table has a column for title and description */} - + {/* Additional columns can be declared in the specific column array. */} diff --git a/frontend/src/routes/manage/Video/VideoDetails.tsx b/frontend/src/routes/manage/Video/VideoDetails.tsx index 2dc407f5c..1218f0e93 100644 --- a/frontend/src/routes/manage/Video/VideoDetails.tsx +++ b/frontend/src/routes/manage/Video/VideoDetails.tsx @@ -88,6 +88,7 @@ const VideoButtonSection: React.FC<{ event: AuthorizedEvent }> = ({ event }) => )} { diff --git a/frontend/src/ui/NotificationContext.tsx b/frontend/src/ui/NotificationContext.tsx new file mode 100644 index 000000000..bddda8aa6 --- /dev/null +++ b/frontend/src/ui/NotificationContext.tsx @@ -0,0 +1,56 @@ +import { bug, Card } from "@opencast/appkit"; +import { createContext, useState, useContext, PropsWithChildren, useEffect, useMemo } from "react"; +import { useRouter } from "../router"; + +export type NotificationMessage = { + kind: "info" | "error"; + // Making this a function helps the message to use the currently + // selected language when changed. + message: () => string; + // The path where the notification should be displayed. If specified, the + // notification will be cleared when the user navigates away from this path. + scope?: string; +} + +type NotificationContext = { + notification?: NotificationMessage; + setNotification: (msg?: NotificationMessage) => void; +} + +const NotificationContext = createContext(null); + +export const NotificationProvider: React.FC = ({ children }) => { + const [notification, setNotification] = useState(); + const router = useRouter(); + const value = useMemo(() => ({ + notification, + setNotification, + }), [notification]); + + useEffect(() => { + const clear = router.listenAtNav(({ newUrl }) => { + if (notification?.scope && newUrl.pathname !== notification.scope) { + setNotification(undefined); + } + }); + + return () => clear(); + }, [router, notification]); + + return + {children} + ; +}; + +export const useNotification = () => { + const context = useContext(NotificationContext) ?? bug("Not initialized!"); + const { notification, setNotification } = context; + + const Notification: React.FC = () => notification && ( + + {notification.message()} + + ); + + return { Notification, setNotification }; +};