diff --git a/CONFIGURATION.md b/CONFIGURATION.md index 363bc100..64ab6263 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -106,6 +106,20 @@ further below for information on that. # Default: 'optional'. #seriesField = 'optional' +# Whether and if so how to fill the presenter name in the upload step. +# This is a list of sources for potential presenter names to suggest, +# in order of descending preference. +# If it is empty, nothing is suggested. +# Note that right now there is only one such source, +# and that manual changes to the presenter form field +# will be persisted in the users' `localStorage`, +# and that stored value will always be preferred +# over the generated suggestion. +# +# Possible sources are: +# - `"opencast"`: Get the name from Opencast's `/info/me.json` API +#autofillPresenter = [] + [recording] # A list of preferred MIME types used by the media recorder. Studio uses the diff --git a/src/opencast.tsx b/src/opencast.tsx index de4c0fcd..99567877 100644 --- a/src/opencast.tsx +++ b/src/opencast.tsx @@ -288,6 +288,32 @@ export class Opencast { return await this.jsonRequest("info/me.json"); } + /** Returns the value from user.name from the `/info/me.json` endpoint. */ + getUsername(): string | null { + if (!(this.#currentUser)) { + return null; + } + if (!(typeof this.#currentUser === "object")) { + return null; + } + if (!("user" in this.#currentUser)) { + return null; + } + if (!this.#currentUser.user) { + return null; + } + if (!(typeof this.#currentUser.user === "object")) { + return null; + } + if (!("name" in this.#currentUser.user)) { + return null; + } + if (!(typeof this.#currentUser.user.name === "string")) { + return null; + } + return this.#currentUser.user.name; + } + /** Returns the response from the `/lti` endpoint. */ async getLti(): Promise { return await this.jsonRequest("lti"); diff --git a/src/settings.tsx b/src/settings.tsx index b4afa4ed..edf4ce3a 100644 --- a/src/settings.tsx +++ b/src/settings.tsx @@ -21,6 +21,9 @@ export type FormFieldState = /** Sources that setting values can come from. */ type SettingsSource = "src-server"| "src-url" | "src-local-storage"; +const PRESENTER_SOURCES = ["opencast"] as const; +type PresenterSource = typeof PRESENTER_SOURCES[number]; + /** Opencast Studio runtime settings. */ export type Settings = { opencast?: { @@ -37,6 +40,7 @@ export type Settings = { titleField?: FormFieldState; presenterField?: FormFieldState; seriesField?: FormFieldState; + autofillPresenter?: PresenterSource[]; }; recording?: { videoBitrate?: number; @@ -584,6 +588,19 @@ const SCHEMA = { titleField: metaDataField, presenterField: metaDataField, seriesField: metaDataField, + autofillPresenter: (v, allowParse, src) => { + const a = types.array(v => { + const s = types.string(v); + if (!(PRESENTER_SOURCES as readonly string[]).includes(s)) { + throw new Error("invalid presenter name source"); + } + return s; + })(v, allowParse, src); + if (new Set(a).size < a.length) { + throw new Error("duplicate presenter name source"); + } + return a; + }, }, recording: { videoBitrate: types.positiveInteger, diff --git a/src/steps/finish/upload.tsx b/src/steps/finish/upload.tsx index 9cea616e..995cc232 100644 --- a/src/steps/finish/upload.tsx +++ b/src/steps/finish/upload.tsx @@ -159,18 +159,27 @@ type UploadFormProps = { }; const UploadForm: React.FC = ({ handleUpload }) => { + const uploadSettings = useSettings().upload ?? {}; const { titleField = "required", presenterField = "required", seriesField = "optional", - } = useSettings().upload ?? {}; + autofillPresenter = [], + } = uploadSettings; const { t, i18n } = useTranslation(); const opencast = useOpencast(); const dispatch = useDispatch(); const settingsManager = useSettingsManager(); const { title, presenter, upload: uploadState, recordings } = useStudioState(); - const presenterValue = presenter || window.localStorage.getItem(LAST_PRESENTER_KEY) || ""; + const presenterValue = presenter + || window.localStorage.getItem(LAST_PRESENTER_KEY) + || autofillPresenter + .map(source => match(source, { + "opencast": () => opencast.getUsername(), + })) + .filter(Boolean)[0] + || ""; type FormState = "idle" | "testing"; const [state, setState] = useState("idle"); @@ -205,14 +214,6 @@ const UploadForm: React.FC = ({ handleUpload }) => { } } - // If the user has not yet changed the value of the field and the last used - // presenter name is used in local storage, use that. - useEffect(() => { - if (presenterValue !== presenter) { - dispatch({ type: "UPDATE_PRESENTER", value: presenterValue }); - } - }); - const configurableServerUrl = settingsManager.isConfigurable("opencast.serverUrl"); const configurableUsername = settingsManager.isUsernameConfigurable(); const configurablePassword = settingsManager.isPasswordConfigurable(); @@ -404,20 +405,23 @@ const UploadForm: React.FC = ({ handleUpload }) => { }; type InputProps = - Pick & + Pick< + JSX.IntrinsicElements["input"], + "onChange" | "autoComplete" | "defaultValue" | "onBlur" + > & Pick>, "register"> & { - /** Human readable string describing the field. */ - label: string; - name: Path; - /** Whether this field is required or may be empty. */ - required: boolean; - /** Function validating the value and returning a string in the case of error. */ - validate?: Validate; - errors: Partial>; - /** Passed to the ``. */ - type?: HTMLInputTypeAttribute; - autoFocus?: boolean; -}; + /** Human readable string describing the field. */ + label: string; + name: Path; + /** Whether this field is required or may be empty. */ + required: boolean; + /** Function validating the value and returning a string in the case of error. */ + validate?: Validate; + errors: Partial>; + /** Passed to the ``. */ + type?: HTMLInputTypeAttribute; + autoFocus?: boolean; + }; /** * A styled `` element with a label. Displays errors and integrated with