Skip to content

Commit

Permalink
Add mutation and UI for series metadata update
Browse files Browse the repository at this point in the history
This refactors the metadata form to be more generic. Again,
it's a tradeoff between limiting duplication and making the
code more complicated.

I also changed the naming convention of the series/video
details and access pages to use more specific names, which
will hopefully be less confusing.
  • Loading branch information
owi92 committed Feb 5, 2025
1 parent 7d96095 commit e53772d
Show file tree
Hide file tree
Showing 15 changed files with 434 additions and 228 deletions.
12 changes: 8 additions & 4 deletions frontend/src/i18n/locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -307,22 +307,26 @@ upload:
angeschaut werden, sobald die Verarbeitung abgeschlossen ist. Sie können diese
Seite jetzt schließen.
metadata:
title: Videotitel
description: Beschreibung
required: Pflichtfeld
save: Speichern und fertigstellen
note-writable-series: >
Sie können Videos nur in Serien hochladen, auf die Sie Schreibzugriff haben.
Andere Serien sind daher hier nicht auswählbar.
errors:
failed-to-upload: Hochladen fehlgeschlagen.
unknown: Während des Dateiuploads ist ein unbekannter Fehler aufgetreten.
field-required: Dieses Feld darf nicht leer sein.
opencast-server-error: Opencast-Server-Fehler (unerwartete Antwort).
opencast-unreachable: 'Netzwerkfehler: Opencast kann nicht erreicht werden.'
jwt-invalid: 'Interner Fremdauthentifizierungsfehler: Opencast hat das Hochladen nicht autorisiert.'
failed-fetching-series-acl: Abruf der Serienzugangsberechtigungen fehlgeschlagen.

metadata-form:
title: Titel
description: Beschreibung
required: Pflichtfeld
save: Änderungen speichern
errors:
field-required: Dieses Feld darf nicht leer sein.

acl:
unknown-user-note: unbekannt
no-entries: Keine Einträge
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/i18n/locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -305,22 +305,26 @@ upload:
Your video was uploaded successfully and is now being processed. It can be
watched as soon as processing is done. You may close this page now.
metadata:
title: Video title
description: Description
required: required
save: Save and finish
note-writable-series: >
Videos can only be uploaded into series for which you have write-access.
Other series are therefore not listed as selectable here.
errors:
failed-to-upload: Failed to upload video.
unknown: Unknown error occurred during video upload.
field-required: This field is required (cannot be empty).
opencast-server-error: Opencast server error (unexpected response).
opencast-unreachable: 'Network error: Opencast cannot be reached.'
jwt-invalid: 'Internal cross-authentication error: Opencast did not authorize the upload.'
failed-fetching-series-acl: Failed to fetch series acl.

metadata-form:
title: Title
description: Description
required: required
save: Save changes
errors:
field-required: This field is required (cannot be empty).

acl:
unknown-user-note: unknown
no-entries: No entries
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ import { UploadRoute } from "./routes/Upload";
import { SearchRoute } from "./routes/Search";
import { InvalidUrlRoute } from "./routes/InvalidUrl";
import { BlockEmbedRoute, EmbedOpencastVideoRoute, EmbedVideoRoute } from "./routes/Embed";
import { ManageVideoDetailsRoute } from "./routes/manage/Video/Details";
import { ManageVideoDetailsRoute } from "./routes/manage/Video/VideoDetails";
import { ManageVideoTechnicalDetailsRoute } from "./routes/manage/Video/TechnicalDetails";
import React from "react";
import { ManageVideoAccessRoute } from "./routes/manage/Video/Access";
import { ManageVideoAccessRoute } from "./routes/manage/Video/VideoAccess";
import { DirectPlaylistOCRoute, DirectPlaylistRoute } from "./routes/Playlist";
import { ManageSeriesRoute } from "./routes/manage/Series";
import { ManageSeriesDetailsRoute } from "./routes/manage/Series/Details";
import { ManageSeriesAccessRoute } from "./routes/manage/Series/Access";
import { ManageSeriesDetailsRoute } from "./routes/manage/Series/SeriesDetails";
import { ManageSeriesAccessRoute } from "./routes/manage/Series/SeriesAccess";



Expand Down
151 changes: 54 additions & 97 deletions frontend/src/routes/Upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import React, { MutableRefObject, ReactNode, useEffect, useId, useRef, useState
import { useTranslation } from "react-i18next";
import { fetchQuery, graphql, useFragment } from "react-relay";
import { keyframes } from "@emotion/react";
import { Controller, useController, useForm } from "react-hook-form";
import { Controller, FormProvider, useController } from "react-hook-form";
import { LuCheckCircle, LuUpload, LuInfo } from "react-icons/lu";
import { Spinner, WithTooltip, assertNever, bug, unreachable } from "@opencast/appkit";
import {
Spinner, WithTooltip, assertNever, bug,
unreachable, Button, boxError, ErrorBox, Card,
} from "@opencast/appkit";

import { RootLoader } from "../layout/Root";
import { environment, loadQuery } from "../relay";
Expand All @@ -13,21 +16,24 @@ import { makeRoute } from "../rauta";
import { ErrorDisplay, errorDisplayInfo } from "../util/err";
import { mapAcl, useNavBlocker } from "./util";
import CONFIG from "../config";
import { Button, boxError, ErrorBox, Card } from "@opencast/appkit";
import { LinkButton } from "../ui/LinkButton";
import { Form } from "../ui/Form";
import { Input, TextArea } from "../ui/Input";
import { isRealUser, User, useUser } from "../User";
import { currentRef, useRefState } from "../util";
import { FieldIsRequiredNote, InputContainer, TitleLabel } from "../ui/metadata";
import {
FieldIsRequiredNote,
InputContainer,
MetadataFields,
MetadataForm,
useMetadataForm,
} from "../ui/metadata";
import { PageTitle } from "../layout/header/ui";
import { useRouter } from "../router";
import { getJwt } from "../relay/auth";
import { VideoListSelector } from "../ui/SearchableSelect";
import { Breadcrumbs } from "../ui/Breadcrumbs";
import { ManageNav, ManageRoute } from "./manage";
import { COLORS } from "../color";
import { COMMON_ROLES } from "../util/roles";
import { defaultAclMap } from "../util/roles";
import { Acl, AclSelector, knownRolesFragment } from "../ui/Access";
import {
AccessKnownRolesData$data,
Expand Down Expand Up @@ -690,8 +696,6 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
return unreachable();
}

const titleFieldId = useId();
const descriptionFieldId = useId();
const seriesFieldId = useId();
const [lockedAcl, setLockedAcl] = useState<Acl | null>(null);
const [aclError, setAclError] = useState<ReactNode>(null);
Expand Down Expand Up @@ -744,104 +748,57 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
}
};

const defaultAcl: Acl = new Map([
[user.userRole, {
actions: new Set(["read", "write"]),
info: {
label: { "default": user.displayName },
implies: null,
large: false,
},
}],
[COMMON_ROLES.ANONYMOUS, {
actions: new Set(["read"]),
info: null,
}],
]);

const { register, handleSubmit, control, formState: { isValid, errors } } = useForm<Metadata>({
mode: "onChange",
defaultValues: { acl: defaultAcl },
});
const { formMethods } = useMetadataForm<Metadata>({ acl: defaultAclMap(user) });
const { handleSubmit, control, formState: { isValid, errors } } = formMethods;

const { field: seriesField } = useController({
name: "series",
control,
rules: {
required: CONFIG.upload.requireSeries ? t("upload.errors.field-required") : false,
required: CONFIG.upload.requireSeries
? t("metadata-form.errors.field-required")
: false,
},
});

const onSubmit = handleSubmit(data => onSave(data));

// We only allow submitting the form on clicking the button below so that
// pressing 'enter' inside inputs doesn't lead to submit the form too
// early.
return (
<Form
noValidate
onSubmit={e => e.preventDefault()}
css={{
margin: "32px 2px",
"label": {
color: "var(--color-neutral90)",
},
}}
>
{/* Title */}
<InputContainer>
<TitleLabel htmlFor={titleFieldId} />
<Input
id={titleFieldId}
required
error={!!errors.title}
css={{ width: 400, maxWidth: "100%" }}
autoFocus
{...register("title", {
required: t("upload.errors.field-required") as string,
})}

return <FormProvider {...formMethods}>
<MetadataForm>
{/* Title & Description */}
<MetadataFields />

{/* Series */}
<InputContainer css={{ maxWidth: 750 }}>
<label htmlFor={seriesFieldId}>
{t("series.series")}
{CONFIG.upload.requireSeries && <FieldIsRequiredNote />}
<WithTooltip
tooltip={t("upload.metadata.note-writable-series")}
tooltipCss={{ width: 400 }}
css={{
display: "inline-block",
verticalAlign: "middle",
fontWeight: "normal",
marginLeft: 8,
}}
>
<span><LuInfo tabIndex={0} /></span>
</WithTooltip>
</label>
<VideoListSelector
type="series"
inputId={seriesFieldId}
writableOnly
menuPlacement="top"
onChange={data => onSeriesChange({ opencastId: data?.opencastId })}
onBlur={seriesField.onBlur}
required={CONFIG.upload.requireSeries}
/>
{boxError(errors.title?.message)}
{boxError(errors.series?.message)}
</InputContainer>

<div css={{ maxWidth: 750 }}>
{/* Description */}
<InputContainer>
<label htmlFor={descriptionFieldId}>{t("upload.metadata.description")}</label>
<TextArea id={descriptionFieldId} {...register("description")} />
</InputContainer>

{/* Series */}
<InputContainer>
<label htmlFor={seriesFieldId}>
{t("series.series")}
{CONFIG.upload.requireSeries && <FieldIsRequiredNote />}
<WithTooltip
tooltip={t("upload.metadata.note-writable-series")}
tooltipCss={{ width: 400 }}
css={{
display: "inline-block",
verticalAlign: "middle",
fontWeight: "normal",
marginLeft: 8,
}}
>
<span><LuInfo tabIndex={0} /></span>
</WithTooltip>
</label>
<VideoListSelector
type="series"
inputId={seriesFieldId}
writableOnly
menuPlacement="top"
onChange={data => onSeriesChange({ opencastId: data?.opencastId })}
onBlur={seriesField.onBlur}
required={CONFIG.upload.requireSeries}
/>
{boxError(errors.series?.message)}
</InputContainer>
</div>

{/* ACL */}
<InputContainer>
<h2 css={{
Expand Down Expand Up @@ -877,16 +834,16 @@ const MetaDataEdit: React.FC<MetaDataEditProps> = ({ onSave, disabled, knownRole
</div>
</InputContainer>

{/* Submit button */}
{/* Submit */}
<Button
kind="call-to-action"
disabled={!isValid || disabled}
css={{ marginTop: 32, marginBottom: 160 }}
onClick={onSubmit}>
{t("upload.metadata.save")}
</Button>
</Form>
);
</MetadataForm>
</FormProvider>;
};


Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/Video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ import { PlayerContextProvider, usePlayerContext } from "../ui/player/PlayerCont
import { CollapsibleDescription } from "../ui/metadata";
import { DirectSeriesRoute } from "./Series";
import { EmbedVideoRoute } from "./Embed";
import { ManageVideoDetailsRoute } from "./manage/Video/Details";
import { ManageVideoDetailsRoute } from "./manage/Video/VideoDetails";
import { PlaylistBlockFromPlaylist } from "../ui/Blocks/Playlist";
import { AuthenticationFormState, FormData, AuthenticationForm } from "./Login";
import {
Expand Down
32 changes: 0 additions & 32 deletions frontend/src/routes/manage/Series/Details.tsx

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@ import { graphql, useMutation } from "react-relay";
import { currentRef } from "@opencast/appkit";

import { AccessKnownRolesData$key } from "../../../ui/__generated__/AccessKnownRolesData.graphql";
import {
AccessUpdateSeriesAclMutation,
} from "./__generated__/AccessUpdateSeriesAclMutation.graphql";
import { makeManageSeriesRoute, Series } from "./Shared";
import { ManageSeriesRoute } from ".";
import { ManageSeriesDetailsRoute } from "./Details";
import { ManageSeriesDetailsRoute } from "./SeriesDetails";
import { displayCommitError } from "../Realm/util";
import { AccessEditor, AclPage, SubmitAclProps } from "../Shared/AccessUI";
import { AccessEditor, AclPage, SubmitAclProps } from "../Shared/Access";
import i18n from "../../../i18n";
import { SeriesAccessAclMutation } from "./__generated__/SeriesAccessAclMutation.graphql";


export const ManageSeriesAccessRoute = makeManageSeriesRoute(
Expand All @@ -28,7 +26,7 @@ export const ManageSeriesAccessRoute = makeManageSeriesRoute(


const updateSeriesAcl = graphql`
mutation AccessUpdateSeriesAclMutation($id: ID!, $acl: [AclInputEntry!]!) {
mutation SeriesAccessAclMutation($id: ID!, $acl: [AclInputEntry!]!) {
updateSeriesAcl(id: $id, acl: $acl) {
...on Series {
acl { role actions info { label implies large } }
Expand All @@ -44,7 +42,7 @@ type SeriesAclPageProps = {
};

const SeriesAclEditor: React.FC<SeriesAclPageProps> = ({ series, data }) => {
const [commit, inFlight] = useMutation<AccessUpdateSeriesAclMutation>(updateSeriesAcl);
const [commit, inFlight] = useMutation<SeriesAccessAclMutation>(updateSeriesAcl);

const onSubmit = async ({ selections, saveModalRef, setCommitError }: SubmitAclProps) => {
commit({
Expand All @@ -64,16 +62,6 @@ const SeriesAclEditor: React.FC<SeriesAclPageProps> = ({ series, data }) => {
});
};


return <>
<AccessEditor
rawAcl={series.acl}
{...{
onSubmit,
inFlight,
data,
}}
/>
</>;
return <AccessEditor {...{ onSubmit, inFlight, data }} rawAcl={series.acl} />;
};

Loading

0 comments on commit e53772d

Please sign in to comment.