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 }; +};