Skip to content

Commit

Permalink
Add notification context for deletion messages
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
owi92 committed Feb 28, 2025
1 parent 6dfc3ce commit 9b3556d
Show file tree
Hide file tree
Showing 8 changed files with 105 additions and 19 deletions.
13 changes: 8 additions & 5 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -38,11 +39,13 @@ export const App: React.FC<Props> = ({ initialRoute, consentGiven }) => (
<Router initialRoute={initialRoute}>
<GraphQLErrorBoundary>
<MenuProvider>
<LoadingIndicator />
<InitialConsent {...{ consentGiven }} />
<Suspense fallback={<InitialLoading />}>
<ActiveRoute />
</Suspense>
<NotificationProvider>
<LoadingIndicator />
<InitialConsent {...{ consentGiven }} />
<Suspense fallback={<InitialLoading />}>
<ActiveRoute />
</Suspense>
</NotificationProvider>
</MenuProvider>
</GraphQLErrorBoundary>
</Router>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ manage:
cannot-be-undone: This action <strong>cannot</strong> 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: >
Expand Down
15 changes: 6 additions & 9 deletions frontend/src/routes/manage/Series/SeriesDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import {
DeleteButton,
HostRealms,
} from "../Shared/Details";
import {
SeriesDetailsMetadataMutation,
} from "./__generated__/SeriesDetailsMetadataMutation.graphql";
import { Link } from "../../../router";


Expand Down Expand Up @@ -43,7 +40,7 @@ export const ManageSeriesDetailsRoute = makeManageSeriesRoute(
}}
sections={series => [
<UpdatedCreatedInfo key="date-info" item={series} />,
<SeriesButtonSection key="button-section" seriesId={series.id} />,
<SeriesButtonSection key="button-section" {...{ series }} />,
<DirectLink key="direct-link" url={
new URL(DirectSeriesRoute.url({ seriesId: series.id }), document.baseURI)
} />,
Expand All @@ -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 <div css={{ display: "flex", gap: 12, marginBottom: 16 }}>
<DeleteButton
itemId={seriesId}
itemId={series.id}
itemTitle={series.title}
itemType="series"
returnPath="/~manage/series"
commit={config => {
Expand All @@ -80,10 +78,9 @@ const SeriesButtonSection: React.FC<{ seriesId: string }> = ({ seriesId }) => {
};

const SeriesMetadataSection: React.FC<{ series: Series }> = ({ series }) => {
const [commit, inFlight]
= useMutation<SeriesDetailsMetadataMutation>(updateSeriesMetadata);
const [commit, inFlight] = useMutation(updateSeriesMetadata);

return <MetadataSection<SeriesDetailsMetadataMutation>
return <MetadataSection
item={{ ...series, description: series.syncedData?.description }}
inFlight={inFlight}
commit={config => {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/routes/manage/Shared/Details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -218,21 +219,25 @@ export const ButtonSection: React.FC<PropsWithChildren> = ({ children }) => (

type DeleteButtonProps<TMutation extends MutationParameters> = PropsWithChildren & {
itemId: string;
itemTitle: string;
itemType: "video" | "series";
commit: (config: UseMutationConfig<TMutation>) => Disposable;
returnPath: string;
};

export const DeleteButton = <TMutation extends MutationParameters>({
itemId,
itemTitle,
itemType,
commit,
returnPath,
children,
}: DeleteButtonProps<TMutation>) => {
const { t } = useTranslation();
const { setNotification } = useNotification();
const modalRef = useRef<ConfirmationModalHandle>(null);
const router = useRouter();

const item = t(`manage.shared.item.${itemType}`);

const onSubmit = () => {
Expand All @@ -241,6 +246,11 @@ export const DeleteButton = <TMutation extends MutationParameters>({
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 => {
Expand Down
27 changes: 22 additions & 5 deletions frontend/src/routes/manage/Shared/Table.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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 = {
Expand Down Expand Up @@ -63,13 +64,29 @@ export const ManageItems = <T extends Item>({
RenderRow,
}: ManageItemProps<T>) => {
const { t } = useTranslation();
const { Notification } = useNotification();

let inner;
if (connection.items.length === 0 && connection.totalCount === 0) {
inner = <Card kind="info">{t("manage.item-table.no-entries-found")}</Card>;
inner = <div css={{ display: "flex", flexDirection: "column", gap: 16 }}>
<Notification />
<Card kind="info" css={{ width: "fit-content" }}>
{t("manage.item-table.no-entries-found")}
</Card>
</div>;
} else {
inner = <>
<PageNavigation {...{ vars, connection }} />
<div css={{
display: "flex",
justifyContent: "space-between",
flexWrap: "wrap",
gap: 16,
}}>
<Notification />
<span css={{ marginLeft: "auto" }}>
<PageNavigation {...{ vars, connection }} />
</span>
</div>
<div css={{ flex: "1 0 0", margin: "16px 0" }}>
<ItemTable {...{ vars, connection, additionalColumns, RenderRow }} />
</div>
Expand Down Expand Up @@ -136,7 +153,7 @@ const ItemTable = <T extends Item>({
return () => {};
});

return <div css={{ position: "relative" }}>
return <div css={{ position: "relative", overflow: "auto" }}>
<table css={{
width: "100%",
borderSpacing: 0,
Expand Down Expand Up @@ -176,7 +193,7 @@ const ItemTable = <T extends Item>({
{/* Each table has thumbnails, but their width might vary */}
<col span={1} css={{ width: THUMBNAIL_WIDTH + 2 * 6 }} />
{/* Each table has a column for title and description */}
<col span={1} />
<col span={1} css={{ [screenWidthAtMost(1000)]: { width: 135 } }} />
{/*
Additional columns can be declared in the specific column array.
*/}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/routes/manage/Video/VideoDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const VideoButtonSection: React.FC<{ event: AuthorizedEvent }> = ({ event }) =>
)}
<DeleteButton
itemId={event.id}
itemTitle={event.title}
itemType="video"
returnPath="/~manage/videos"
commit={config => {
Expand Down
56 changes: 56 additions & 0 deletions frontend/src/ui/NotificationContext.tsx
Original file line number Diff line number Diff line change
@@ -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<NotificationContext | null>(null);

export const NotificationProvider: React.FC<PropsWithChildren> = ({ children }) => {
const [notification, setNotification] = useState<NotificationMessage>();
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 <NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>;
};

export const useNotification = () => {
const context = useContext(NotificationContext) ?? bug("Not initialized!");
const { notification, setNotification } = context;

const Notification: React.FC = () => notification && (
<Card css={{ width: "fit-content", marginTop: 12 }} kind={notification.kind}>
{notification.message()}
</Card>
);

return { Notification, setNotification };
};

0 comments on commit 9b3556d

Please sign in to comment.