diff --git a/src/videoag/form/TypeEditor.tsx b/src/videoag/form/TypeEditor.tsx
index 8c934e9914a092dad3a3c9b09151ada2828b9633..176077727c773883f58512850abab67207b91c73 100644
--- a/src/videoag/form/TypeEditor.tsx
+++ b/src/videoag/form/TypeEditor.tsx
@@ -1,19 +1,9 @@
 import React, { useEffect, useRef, useState } from "react";
-import Dropdown from "react-bootstrap/Dropdown";
 
-import type {
-    GetUsersResponse,
-    field_description,
-    view_permissions,
-    user,
-    int,
-} from "@/videoag/api/types";
-import { useApi } from "@/videoag/api/ApiProvider";
-import { showError } from "@/videoag/error/ErrorDisplay";
+import type { int } from "@/videoag/api/types";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
-import { deepEquals } from "@/videoag/miscellaneous/Util";
 
-type EditorArgs = {
+export type EditorArgs = {
     value?: any;
     //affectNow specifies whether value should be applied (e.g. reload data, apply filter) immediately (e.g. user hit enter)
     //or a bit later (debounced) (e.g. user typed in text field)
@@ -23,79 +13,6 @@ type EditorArgs = {
     hideErrors?: boolean;
 };
 
-export function OMFieldEditor({
-    description,
-    ...args
-}: {
-    description: field_description;
-} & EditorArgs) {
-    const { value } = args;
-    switch (description.type) {
-        // TODO check which types actually exist now
-        case "string":
-            return (
-                <StringEditor
-                    {...args}
-                    minLength={description.min_length}
-                    maxLength={description.max_length}
-                    regex={description.pattern ? new RegExp(description.pattern) : undefined}
-                    regexInvalidMessage={
-                        description.pattern &&
-                        `Invalid value. Must match pattern: ${description.pattern}`
-                    }
-                />
-            );
-
-        case "boolean":
-            return <BooleanEditor {...args} />;
-
-        case "int":
-            return (
-                <IntEditor
-                    {...args}
-                    minValue={description.min_value}
-                    maxValue={description.max_value}
-                />
-            );
-
-        case "datetime":
-            return <DatetimeEditor {...args} mayBeNull={description.may_be_null} />;
-
-        case "enum":
-            return (
-                <ChooserEditor
-                    {...args}
-                    langKeyPrefix={`enum.${description.name!}`}
-                    possibleValues={description.enums!}
-                />
-            );
-
-        case "view_permissions":
-            return <ViewPermissionsEditor {...args} />;
-
-        case "object_list":
-            // TODO other objects
-            if (description.object_type === "user") {
-                return <UserIdListEditor {...args} />;
-            }
-            return (
-                <span>
-                    Unknown field type {description.type}. {JSON.stringify(value)}
-                </span>
-            );
-
-        // TODO media_process
-        // TODO object
-
-        default:
-            return (
-                <span>
-                    Unknown field type {description.type}. {JSON.stringify(value)}
-                </span>
-            );
-    }
-}
-
 export function StringEditor({
     value,
     updateValue,
@@ -427,423 +344,3 @@ export function ChooserEditor({
         </select>
     );
 }
-
-export function UserIdListEditor({
-    value,
-    updateValue,
-    disabled,
-    autoFocus,
-    hideErrors,
-}: EditorArgs) {
-    const api = useApi();
-    const knownValue = useRef<int[]>();
-    const [users, setUsers] = useState<GetUsersResponse>();
-    const [search, setSearch] = useState<string>("");
-
-    useEffect(() => {
-        if (knownValue.current === value) return;
-        // Value was not changed by us, reset internal state
-        knownValue.current = value;
-        setUsers(undefined);
-        setSearch("");
-        api.getUsers()
-            .then(setUsers)
-            .catch((err) => {
-                showError(err, "Unable to load users");
-            });
-    }, [api, knownValue, value]);
-
-    useEffect(() => {
-        if (value !== undefined) return;
-        const newValue: int[] = [];
-        knownValue.current = newValue;
-        updateValue(newValue, true, true);
-    });
-
-    const selectedArray = value ?? [];
-
-    let userList = selectedArray.length > 0 ? selectedArray.join(", ") : "None";
-    if (users !== undefined && selectedArray.length > 0) {
-        userList = selectedArray
-            .map((id: int) => {
-                const user = users.users.find((u: user) => u.id === id);
-                if (user === undefined) return `#${id}`;
-                return `${user.display_name} (${user.handle}#${user.id})`;
-            })
-            .join(", ");
-    }
-    if (userList.length > 100) userList = userList.slice(0, 100) + "...";
-
-    const searchTerm = search.toLowerCase();
-
-    const userElements = (users === undefined ? [] : users.users).flatMap((user: user) => {
-        if (
-            !user.display_name.toLowerCase().includes(searchTerm) &&
-            !user.full_name.toLowerCase().includes(searchTerm) &&
-            !user.handle.toLowerCase().includes(searchTerm)
-        )
-            return [];
-        const toggle = () => {
-            const index = selectedArray.indexOf(user.id);
-            let newArray = selectedArray.slice();
-            if (index === -1) {
-                newArray.splice(newArray.length, 0, user.id); //Add user.id at end
-            } else {
-                newArray.splice(index, 1); //Remove user.id
-            }
-            knownValue.current = newArray;
-            updateValue(newArray, true, true);
-        };
-        return [
-            <tr key={user.id} className={selectedArray.includes(user.id) ? "table-primary" : ""}>
-                <td>
-                    <input
-                        type="checkbox"
-                        checked={selectedArray.includes(user.id)}
-                        onChange={toggle}
-                    />
-                </td>
-                <td onClick={toggle} className="pe-auto" style={{ cursor: "pointer" }}>
-                    <span>{`${user.display_name} (${user.handle}#${user.id})`}</span>
-                </td>
-            </tr>,
-        ];
-    });
-
-    return (
-        <Dropdown>
-            <Dropdown.Toggle variant="primary" style={{ textWrap: "wrap" }} disabled={disabled}>
-                {userList}
-            </Dropdown.Toggle>
-            <Dropdown.Menu className="p-1">
-                <div className="input-group mb-2 p-1">
-                    <span className="input-group-text">
-                        <i className="bi bi-search" />
-                    </span>
-                    <input
-                        type="text"
-                        className="form-control"
-                        placeholder="Username"
-                        value={search}
-                        autoFocus
-                        onChange={(e) => setSearch(e.target.value)}
-                    />
-                    <button
-                        type="button"
-                        className="btn btn-outline-secondary"
-                        onClick={() => setSearch("")}
-                        disabled={search === ""}
-                    >
-                        <i className="bi bi-x-circle" />
-                    </button>
-                </div>
-                {users === undefined ? (
-                    <div className="spinner-border" role="status" />
-                ) : (
-                    <>
-                        {userElements.length === 0 && (
-                            <div className="text-center">No users found</div>
-                        )}
-                        <div className="overflow-auto" style={{ maxHeight: "500px" }}>
-                            <table className="table table-sm table-hover w-100">
-                                <thead>
-                                    <tr>
-                                        <th></th>
-                                        <th className="w-100"></th>
-                                    </tr>
-                                </thead>
-                                <tbody>{userElements}</tbody>
-                            </table>
-                        </div>
-                    </>
-                )}
-            </Dropdown.Menu>
-        </Dropdown>
-    );
-}
-
-function createListEditor<V>(
-    disabled: boolean,
-    list: V[],
-    defaultNewValue: V,
-    onNewValueList: (newList: V[]) => void,
-    elementFactory: (value: V, onNewValue: (newVal: V) => void) => React.ReactNode,
-) {
-    return (
-        <>
-            {list.map((value: V, index: int) => (
-                <div key={index} className="d-block">
-                    {elementFactory(value, (newVal: V) =>
-                        onNewValueList(
-                            list.map((subValue: V, subIndex: int) =>
-                                index === subIndex ? newVal : subValue,
-                            ),
-                        ),
-                    )}
-                    <button
-                        type="button"
-                        className={"btn btn-outline-danger m-1"}
-                        onClick={(e) => {
-                            const newList = list.slice();
-                            newList.splice(index, 1);
-                            onNewValueList(newList);
-                        }}
-                        title="Delete"
-                    >
-                        <i className="bi bi-trash-fill" />
-                    </button>
-                </div>
-            ))}
-            <button
-                className="btn btn-secondary"
-                type="button"
-                title="Add entry"
-                onClick={(e) => {
-                    onNewValueList(list.concat([defaultNewValue]));
-                }}
-                disabled={disabled}
-            >
-                +
-            </button>
-        </>
-    );
-}
-
-export function ViewPermissionsEditor({
-    value,
-    updateValue,
-    disabled,
-    autoFocus,
-    hideErrors,
-}: EditorArgs) {
-    const permTypes = ["inherit", "public", "private", "authentication"];
-
-    // We need to store the current value locally since there is no order for the passwords and it would be really
-    // annoying if the order changes while typing
-    // This also gives the benefit of storing values for types which are currently not selected
-    type view_permissions_with_invalid = Omit<view_permissions, "passwords"> & {
-        passwords?: null | { [key: string]: string };
-    };
-    const { language } = useLanguage();
-    const knownValue = useRef<view_permissions_with_invalid>();
-    const [allDataValue, setAllDataValue] = useState<view_permissions_with_invalid>({
-        type: "inherit",
-    });
-    const [passwordsArray, setPasswordsArray] = useState<[string, string][]>([]);
-
-    useEffect(() => {
-        if (deepEquals(value, knownValue.current)) return;
-        // Value was not changed by us, update internal state
-        const effectiveValue = value ?? { type: "inherit" };
-        const passwordsMap = effectiveValue.passwords ?? {};
-        setPasswordsArray(
-            Object.keys(passwordsMap).map((username: string) => [username, passwordsMap[username]]),
-        );
-        setAllDataValue(effectiveValue);
-        knownValue.current = effectiveValue;
-        if (effectiveValue !== value) updateValue(effectiveValue, true, true);
-    }, [value, updateValue]);
-
-    const updateData = (key: string, newData: any) =>
-        setAllDataValue((prev: view_permissions_with_invalid | undefined) => {
-            let isValid = true;
-
-            prev = prev ?? { type: "inherit" };
-            let newAllData: view_permissions_with_invalid = { ...prev, [key]: newData };
-            if (key === "passwords") {
-                setPasswordsArray(newData);
-                let newMap: { [key: string]: string } | null = {};
-                for (let ele of newData) {
-                    if (newMap[ele[0]] !== undefined) {
-                        newMap = null;
-                        break;
-                    }
-                    newMap[ele[0]] = ele[1];
-                }
-                newAllData = { ...prev, passwords: newMap };
-            }
-
-            let newValue: view_permissions_with_invalid = { type: newAllData.type };
-            if (newValue.type === "authentication") {
-                newValue.rwth_authentication = newAllData.rwth_authentication ?? false;
-                newValue.fsmpi_authentication = newAllData.fsmpi_authentication ?? false;
-                newValue.moodle_course_ids = newAllData.moodle_course_ids ?? [];
-                newValue.passwords = newAllData.passwords !== undefined ? newAllData.passwords : {};
-                if (newValue.passwords === null) {
-                    isValid = false;
-                } else if (
-                    !newValue.rwth_authentication &&
-                    !newValue.fsmpi_authentication &&
-                    newValue.moodle_course_ids.length === 0 &&
-                    Object.keys(newValue.passwords).length === 0
-                ) {
-                    isValid = false;
-                }
-            }
-            knownValue.current = newValue;
-            updateValue(newValue, isValid, true);
-            return newAllData;
-        });
-
-    let invalidMessage = undefined;
-    if (
-        hideErrors !== true &&
-        allDataValue?.type === "authentication" &&
-        allDataValue.passwords !== null &&
-        !(
-            allDataValue.rwth_authentication === true ||
-            allDataValue.fsmpi_authentication === true ||
-            (allDataValue.moodle_course_ids ?? []).length > 0 ||
-            Object.keys(allDataValue.passwords ?? {}).length > 0
-        )
-    ) {
-        invalidMessage = (
-            <div className="d-block invalid-feedback">
-                Für Typ <i>Authentifizierung</i> muss mindestens eine Authentifizierungsmethode
-                vorhanden sein
-            </div>
-        );
-    }
-
-    let passwordsInvalidMessage;
-    if (hideErrors !== true && allDataValue?.type === "authentication") {
-        const passwordsMap: { [key: string]: null } = {};
-        for (let ele of passwordsArray) {
-            if (passwordsMap[ele[0]] !== undefined) {
-                passwordsInvalidMessage = (
-                    <div className="d-block invalid-feedback">
-                        Doppelter Nutzername &apos;{ele[0]}&apos;
-                    </div>
-                );
-                break;
-            }
-            passwordsMap[ele[0]] = null;
-        }
-    }
-
-    return (
-        <div className="d-inline-grid">
-            <select
-                className="w-auto form-select"
-                value={allDataValue?.type ?? "inherit"}
-                onChange={(e) => updateData("type", e.target.value)}
-                disabled={disabled}
-                autoFocus={autoFocus}
-            >
-                {permTypes.map((type) => (
-                    <option key={type} value={type}>
-                        {language.get(`ui.view_permissions.type.${type}`)}
-                    </option>
-                ))}
-            </select>
-            {allDataValue?.type === "authentication" && (
-                <table className="w-100 table">
-                    <thead>
-                        <tr>
-                            <th scope="col" style={{ width: "30%" }} />
-                            <th scope="col" style={{ width: "70%" }} />
-                        </tr>
-                    </thead>
-                    <tbody>
-                        <tr>
-                            <td>RWTH authentication</td>
-                            <td>
-                                <input
-                                    type="checkbox"
-                                    className="form-check-input"
-                                    name="rwth_authentication"
-                                    defaultChecked={allDataValue?.rwth_authentication ?? false}
-                                    disabled={disabled}
-                                    onChange={(e) =>
-                                        updateData("rwth_authentication", e.target.checked)
-                                    }
-                                />
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>FSMPI authentication</td>
-                            <td>
-                                <input
-                                    type="checkbox"
-                                    className="form-check-input"
-                                    name="fsmpi_authentication"
-                                    defaultChecked={allDataValue?.fsmpi_authentication ?? false}
-                                    disabled={disabled}
-                                    onChange={(e) =>
-                                        updateData("fsmpi_authentication", e.target.checked)
-                                    }
-                                />
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>Moodle course ids</td>
-                            <td>
-                                {createListEditor<int>(
-                                    disabled ?? false,
-                                    allDataValue?.moodle_course_ids ?? [],
-                                    0,
-                                    (newList: int[]) => updateData("moodle_course_ids", newList),
-                                    (value: int, onNewValue: (newVal: int) => void) => (
-                                        <input
-                                            className="mb-1"
-                                            type="number"
-                                            value={value}
-                                            onChange={(e) => {
-                                                onNewValue(parseInt(e.target.value));
-                                            }}
-                                            disabled={disabled ?? false}
-                                        />
-                                    ),
-                                )}
-                            </td>
-                        </tr>
-                        <tr>
-                            <td>Passwords</td>
-                            <td>
-                                <div className="d-inline-grid">
-                                    <span>
-                                        {createListEditor<[string, string]>(
-                                            disabled ?? false,
-                                            passwordsArray,
-                                            ["", ""],
-                                            (newList: [string, string][]) =>
-                                                updateData("passwords", newList),
-                                            (
-                                                value: [string, string],
-                                                onNewValue: (newVal: [string, string]) => void,
-                                            ) => (
-                                                <>
-                                                    <input
-                                                        className="me-1"
-                                                        type="text"
-                                                        placeholder="Username"
-                                                        value={value[0]}
-                                                        onChange={(e) =>
-                                                            onNewValue([e.target.value, value[1]])
-                                                        }
-                                                        disabled={disabled ?? false}
-                                                    />
-                                                    <input
-                                                        type="text"
-                                                        placeholder="Password"
-                                                        value={value[1]}
-                                                        onChange={(e) =>
-                                                            onNewValue([value[0], e.target.value])
-                                                        }
-                                                        disabled={disabled ?? false}
-                                                    />
-                                                </>
-                                            ),
-                                        )}
-                                    </span>
-                                    {passwordsInvalidMessage}
-                                </div>
-                            </td>
-                        </tr>
-                    </tbody>
-                </table>
-            )}
-            {invalidMessage}
-        </div>
-    );
-}
diff --git a/src/videoag/miscellaneous/Util.tsx b/src/videoag/miscellaneous/Util.tsx
index 21b6d0a7969a8d1978b6bbcb9ab4198a698538e3..9fc94cc2bfc42f8d3dd618371e873c30cd2ced39 100644
--- a/src/videoag/miscellaneous/Util.tsx
+++ b/src/videoag/miscellaneous/Util.tsx
@@ -1,5 +1,6 @@
 import type React from "react";
 import { OverlayTrigger, Tooltip } from "react-bootstrap";
+import { toast, Bounce } from "react-toastify";
 
 export function deepEquals(value: any, other: any) {
     if (value === null || other === null) {
@@ -46,6 +47,18 @@ export function mapMap<K, V, R>(map: Map<K, V>, func: (val: V) => R): Map<K, R>
     return newMap;
 }
 
+export function showInfoToast(message: React.ReactNode, autoClose: boolean) {
+    toast.info(message, {
+        position: "top-center",
+        autoClose: autoClose,
+        closeOnClick: false,
+        draggable: false,
+        progress: undefined,
+        theme: "light", // Colors overridden in css
+        transition: Bounce,
+    });
+}
+
 export function TooltipButton({
     children,
     iconClassName,
diff --git a/src/videoag/object_management/OMConfigComponent.tsx b/src/videoag/object_management/OMConfigComponent.tsx
index a88541d90d85770d63965dbcdc537c2cfbef5749..bfbb29a83f8e5ca9ab0d8eb27062349afff05df8 100644
--- a/src/videoag/object_management/OMConfigComponent.tsx
+++ b/src/videoag/object_management/OMConfigComponent.tsx
@@ -11,7 +11,6 @@ import type {
 } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
 import { showError, showErrorToast } from "@/videoag/error/ErrorDisplay";
-import { OMFieldEditor } from "@/videoag/form/TypeEditor";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
 import { useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
 import { useDebounceUpdateData } from "@/videoag/miscellaneous/PromiseHelpers";
@@ -22,6 +21,7 @@ import StopNavigation from "@/videoag/miscellaneous/StopNavigation";
 import { basePath } from "@/../basepath";
 
 import { LockEditMode, useEditMode } from "./EditModeProvider";
+import { OMFieldEditor } from "./OMFieldEditor";
 
 const markdownSupportingFields = [
     "announcement.text",
@@ -249,7 +249,10 @@ export function EmbeddedOMFieldComponent({
         let formatted: any = "Value of unknown type: " + value;
         switch (field_type) {
             case "string":
-                formatted = <StylizedText markdown={allowMarkdown}>{value}</StylizedText>;
+                if (editMode && !value)
+                    formatted = <p><i className="text-muted">{"<empty>"}</i></p>
+                else
+                    formatted = <StylizedText markdown={allowMarkdown}>{value}</StylizedText>;
                 break;
             case "datetime":
                 formatted = datetimeToString(value);
@@ -625,15 +628,6 @@ export function OMEdit({
                     )}
                     {config?.fields !== undefined && (
                         <>
-                            <div className="alert alert-warning">
-                                {/* TODO: remove after release */}
-                                Achtung: Die alte Webseite kann kein Markdown rendern, nur HTML.
-                                Wenn hier also Markdown verwendet wird, wird dies auf der alten
-                                Seite nicht richtig gerendert. Während der Transition werden aber
-                                auf der neuen Seite die folgenden HTML-Tags gerendert: a, b, br, p
-                                und strong (Welche auf der alten Seite auch korrekt angezeigt
-                                werden)
-                            </div>
                             <small className="text-muted">
                                 Tipp: Viele Felder können im Edit-Mode auch direkt durch einen
                                 Doppelklick bearbeitet werden
diff --git a/src/videoag/object_management/OMFieldEditor.tsx b/src/videoag/object_management/OMFieldEditor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..96ce0528277e7e8856aab8c7e2dd0a40817b666e
--- /dev/null
+++ b/src/videoag/object_management/OMFieldEditor.tsx
@@ -0,0 +1,86 @@
+import React from "react";
+
+import type { field_description } from "@/videoag/api/types";
+import {
+    EditorArgs,
+    StringEditor,
+    BooleanEditor,
+    IntEditor,
+    DatetimeEditor,
+    ChooserEditor,
+} from "@/videoag/form/TypeEditor";
+
+import { UserIdListEditor } from "./ObjectEditor";
+import ViewPermissionsEditor from "./ViewPermissionsEditor";
+
+export function OMFieldEditor({
+    description,
+    ...args
+}: {
+    description: field_description;
+} & EditorArgs) {
+    const { value } = args;
+    switch (description.type) {
+        case "string":
+            return (
+                <StringEditor
+                    {...args}
+                    minLength={description.min_length}
+                    maxLength={description.max_length}
+                    regex={description.pattern ? new RegExp(description.pattern) : undefined}
+                    regexInvalidMessage={
+                        description.pattern &&
+                        `Invalid value. Must match pattern: ${description.pattern}`
+                    }
+                />
+            );
+
+        case "boolean":
+            return <BooleanEditor {...args} />;
+
+        case "int":
+            return (
+                <IntEditor
+                    {...args}
+                    minValue={description.min_value}
+                    maxValue={description.max_value}
+                />
+            );
+
+        case "datetime":
+            return <DatetimeEditor {...args} mayBeNull={description.may_be_null} />;
+
+        case "enum":
+            return (
+                <ChooserEditor
+                    {...args}
+                    langKeyPrefix={`enum.${description.name!}`}
+                    possibleValues={description.enums!}
+                />
+            );
+
+        case "view_permissions":
+            return <ViewPermissionsEditor {...args} />;
+
+        case "object_list":
+            // TODO other objects
+            if (description.object_type === "user") {
+                return <UserIdListEditor {...args} />;
+            }
+            return (
+                <span>
+                    Unknown field type {description.type}. {JSON.stringify(value)}
+                </span>
+            );
+
+        // TODO media_process
+        // TODO object
+
+        default:
+            return (
+                <span>
+                    Unknown field type {description.type}. {JSON.stringify(value)}
+                </span>
+            );
+    }
+}
diff --git a/src/videoag/object_management/ObjectEditor.tsx b/src/videoag/object_management/ObjectEditor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b441bc4129c0294b2a64a197b71501ce99dc91d9
--- /dev/null
+++ b/src/videoag/object_management/ObjectEditor.tsx
@@ -0,0 +1,141 @@
+import React, { useEffect, useRef, useState } from "react";
+import Dropdown from "react-bootstrap/Dropdown";
+
+import type { GetUsersResponse, user, int } from "@/videoag/api/types";
+import { useApi } from "@/videoag/api/ApiProvider";
+import { EditorArgs } from "@/videoag/form/TypeEditor";
+import { showError } from "@/videoag/error/ErrorDisplay";
+
+export function UserIdListEditor({
+    value,
+    updateValue,
+    disabled,
+    autoFocus,
+    hideErrors,
+}: EditorArgs) {
+    const api = useApi();
+    const knownValue = useRef<int[]>();
+    const [users, setUsers] = useState<GetUsersResponse>();
+    const [search, setSearch] = useState<string>("");
+
+    useEffect(() => {
+        if (knownValue.current === value) return;
+        // Value was not changed by us, reset internal state
+        knownValue.current = value;
+        setUsers(undefined);
+        setSearch("");
+        api.getUsers()
+            .then(setUsers)
+            .catch((err) => {
+                showError(err, "Unable to load users");
+            });
+    }, [api, knownValue, value]);
+
+    useEffect(() => {
+        if (value !== undefined) return;
+        const newValue: int[] = [];
+        knownValue.current = newValue;
+        updateValue(newValue, true, true);
+    });
+
+    const selectedArray = value ?? [];
+
+    let userList = selectedArray.length > 0 ? selectedArray.join(", ") : "None";
+    if (users !== undefined && selectedArray.length > 0) {
+        userList = selectedArray
+            .map((id: int) => {
+                const user = users.users.find((u: user) => u.id === id);
+                if (user === undefined) return `#${id}`;
+                return `${user.display_name} (${user.handle}#${user.id})`;
+            })
+            .join(", ");
+    }
+    if (userList.length > 100) userList = userList.slice(0, 100) + "...";
+
+    const searchTerm = search.toLowerCase();
+
+    const userElements = (users === undefined ? [] : users.users).flatMap((user: user) => {
+        if (
+            !user.display_name.toLowerCase().includes(searchTerm) &&
+            !user.full_name.toLowerCase().includes(searchTerm) &&
+            !user.handle.toLowerCase().includes(searchTerm)
+        )
+            return [];
+        const toggle = () => {
+            const index = selectedArray.indexOf(user.id);
+            let newArray = selectedArray.slice();
+            if (index === -1) {
+                newArray.splice(newArray.length, 0, user.id); //Add user.id at end
+            } else {
+                newArray.splice(index, 1); //Remove user.id
+            }
+            knownValue.current = newArray;
+            updateValue(newArray, true, true);
+        };
+        return [
+            <tr key={user.id} className={selectedArray.includes(user.id) ? "table-primary" : ""}>
+                <td>
+                    <input
+                        type="checkbox"
+                        checked={selectedArray.includes(user.id)}
+                        onChange={toggle}
+                    />
+                </td>
+                <td onClick={toggle} className="pe-auto" style={{ cursor: "pointer" }}>
+                    <span>{`${user.display_name} (${user.handle}#${user.id})`}</span>
+                </td>
+            </tr>,
+        ];
+    });
+
+    return (
+        <Dropdown>
+            <Dropdown.Toggle variant="primary" style={{ textWrap: "wrap" }} disabled={disabled}>
+                {userList}
+            </Dropdown.Toggle>
+            <Dropdown.Menu className="p-1">
+                <div className="input-group mb-2 p-1">
+                    <span className="input-group-text">
+                        <i className="bi bi-search" />
+                    </span>
+                    <input
+                        type="text"
+                        className="form-control"
+                        placeholder="Username"
+                        value={search}
+                        autoFocus
+                        onChange={(e) => setSearch(e.target.value)}
+                    />
+                    <button
+                        type="button"
+                        className="btn btn-outline-secondary"
+                        onClick={() => setSearch("")}
+                        disabled={search === ""}
+                    >
+                        <i className="bi bi-x-circle" />
+                    </button>
+                </div>
+                {users === undefined ? (
+                    <div className="spinner-border" role="status" />
+                ) : (
+                    <>
+                        {userElements.length === 0 && (
+                            <div className="text-center">No users found</div>
+                        )}
+                        <div className="overflow-auto" style={{ maxHeight: "500px" }}>
+                            <table className="table table-sm table-hover w-100">
+                                <thead>
+                                    <tr>
+                                        <th></th>
+                                        <th className="w-100"></th>
+                                    </tr>
+                                </thead>
+                                <tbody>{userElements}</tbody>
+                            </table>
+                        </div>
+                    </>
+                )}
+            </Dropdown.Menu>
+        </Dropdown>
+    );
+}
diff --git a/src/videoag/object_management/ViewPermissionsEditor.tsx b/src/videoag/object_management/ViewPermissionsEditor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5591bc8dd95c5400aeeee6e5471501fc4f5e4c55
--- /dev/null
+++ b/src/videoag/object_management/ViewPermissionsEditor.tsx
@@ -0,0 +1,292 @@
+import React, { useEffect, useRef, useState } from "react";
+
+import type { view_permissions, int } from "@/videoag/api/types";
+import { EditorArgs } from "@/videoag/form/TypeEditor";
+import { useLanguage } from "@/videoag/localization/LanguageProvider";
+import { deepEquals } from "@/videoag/miscellaneous/Util";
+
+function createListEditor<V>(
+    disabled: boolean,
+    list: V[],
+    defaultNewValue: V,
+    onNewValueList: (newList: V[]) => void,
+    elementFactory: (value: V, onNewValue: (newVal: V) => void) => React.ReactNode,
+) {
+    return (
+        <>
+            {list.map((value: V, index: int) => (
+                <div key={index} className="d-block">
+                    {elementFactory(value, (newVal: V) =>
+                        onNewValueList(
+                            list.map((subValue: V, subIndex: int) =>
+                                index === subIndex ? newVal : subValue,
+                            ),
+                        ),
+                    )}
+                    <button
+                        type="button"
+                        className={"btn btn-outline-danger m-1"}
+                        onClick={(e) => {
+                            const newList = list.slice();
+                            newList.splice(index, 1);
+                            onNewValueList(newList);
+                        }}
+                        title="Delete"
+                    >
+                        <i className="bi bi-trash-fill" />
+                    </button>
+                </div>
+            ))}
+            <button
+                className="btn btn-secondary"
+                type="button"
+                title="Add entry"
+                onClick={(e) => {
+                    onNewValueList(list.concat([defaultNewValue]));
+                }}
+                disabled={disabled}
+            >
+                +
+            </button>
+        </>
+    );
+}
+
+export default function ViewPermissionsEditor({
+    value,
+    updateValue,
+    disabled,
+    autoFocus,
+    hideErrors,
+}: EditorArgs) {
+    const permTypes = ["inherit", "public", "private", "authentication"];
+
+    // We need to store the current value locally since there is no order for the passwords and it would be really
+    // annoying if the order changes while typing
+    // This also gives the benefit of storing values for types which are currently not selected
+    type view_permissions_with_invalid = Omit<view_permissions, "passwords"> & {
+        passwords?: null | { [key: string]: string };
+    };
+    const { language } = useLanguage();
+    const knownValue = useRef<view_permissions_with_invalid>();
+    const [allDataValue, setAllDataValue] = useState<view_permissions_with_invalid>({
+        type: "inherit",
+    });
+    const [passwordsArray, setPasswordsArray] = useState<[string, string][]>([]);
+
+    useEffect(() => {
+        if (deepEquals(value, knownValue.current)) return;
+        // Value was not changed by us, update internal state
+        const effectiveValue = value ?? { type: "inherit" };
+        const passwordsMap = effectiveValue.passwords ?? {};
+        setPasswordsArray(
+            Object.keys(passwordsMap).map((username: string) => [username, passwordsMap[username]]),
+        );
+        setAllDataValue(effectiveValue);
+        knownValue.current = effectiveValue;
+        if (effectiveValue !== value) updateValue(effectiveValue, true, true);
+    }, [value, updateValue]);
+
+    const updateData = (key: string, newData: any) =>
+        setAllDataValue((prev: view_permissions_with_invalid | undefined) => {
+            let isValid = true;
+
+            prev = prev ?? { type: "inherit" };
+            let newAllData: view_permissions_with_invalid = { ...prev, [key]: newData };
+            if (key === "passwords") {
+                setPasswordsArray(newData);
+                let newMap: { [key: string]: string } | null = {};
+                for (let ele of newData) {
+                    if (newMap[ele[0]] !== undefined) {
+                        newMap = null;
+                        break;
+                    }
+                    newMap[ele[0]] = ele[1];
+                }
+                newAllData = { ...prev, passwords: newMap };
+            }
+
+            let newValue: view_permissions_with_invalid = { type: newAllData.type };
+            if (newValue.type === "authentication") {
+                newValue.rwth_authentication = newAllData.rwth_authentication ?? false;
+                newValue.fsmpi_authentication = newAllData.fsmpi_authentication ?? false;
+                newValue.moodle_course_ids = newAllData.moodle_course_ids ?? [];
+                newValue.passwords = newAllData.passwords !== undefined ? newAllData.passwords : {};
+                if (newValue.passwords === null) {
+                    isValid = false;
+                } else if (
+                    !newValue.rwth_authentication &&
+                    !newValue.fsmpi_authentication &&
+                    newValue.moodle_course_ids.length === 0 &&
+                    Object.keys(newValue.passwords).length === 0
+                ) {
+                    isValid = false;
+                }
+            }
+            knownValue.current = newValue;
+            updateValue(newValue, isValid, true);
+            return newAllData;
+        });
+
+    let invalidMessage = undefined;
+    if (
+        hideErrors !== true &&
+        allDataValue?.type === "authentication" &&
+        allDataValue.passwords !== null &&
+        !(
+            allDataValue.rwth_authentication === true ||
+            allDataValue.fsmpi_authentication === true ||
+            (allDataValue.moodle_course_ids ?? []).length > 0 ||
+            Object.keys(allDataValue.passwords ?? {}).length > 0
+        )
+    ) {
+        invalidMessage = (
+            <div className="d-block invalid-feedback">
+                Für Typ <i>Authentifizierung</i> muss mindestens eine Authentifizierungsmethode
+                vorhanden sein
+            </div>
+        );
+    }
+
+    let passwordsInvalidMessage;
+    if (hideErrors !== true && allDataValue?.type === "authentication") {
+        const passwordsMap: { [key: string]: null } = {};
+        for (let ele of passwordsArray) {
+            if (passwordsMap[ele[0]] !== undefined) {
+                passwordsInvalidMessage = (
+                    <div className="d-block invalid-feedback">
+                        Doppelter Nutzername &apos;{ele[0]}&apos;
+                    </div>
+                );
+                break;
+            }
+            passwordsMap[ele[0]] = null;
+        }
+    }
+
+    return (
+        <div className="d-inline-grid">
+            <select
+                className="w-auto form-select"
+                value={allDataValue?.type ?? "inherit"}
+                onChange={(e) => updateData("type", e.target.value)}
+                disabled={disabled}
+                autoFocus={autoFocus}
+            >
+                {permTypes.map((type) => (
+                    <option key={type} value={type}>
+                        {language.get(`ui.view_permissions.type.${type}`)}
+                    </option>
+                ))}
+            </select>
+            {allDataValue?.type === "authentication" && (
+                <table className="w-100 table">
+                    <thead>
+                        <tr>
+                            <th scope="col" style={{ width: "30%" }} />
+                            <th scope="col" style={{ width: "70%" }} />
+                        </tr>
+                    </thead>
+                    <tbody>
+                        <tr>
+                            <td>RWTH authentication</td>
+                            <td>
+                                <input
+                                    type="checkbox"
+                                    className="form-check-input"
+                                    name="rwth_authentication"
+                                    defaultChecked={allDataValue?.rwth_authentication ?? false}
+                                    disabled={disabled}
+                                    onChange={(e) =>
+                                        updateData("rwth_authentication", e.target.checked)
+                                    }
+                                />
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>FSMPI authentication</td>
+                            <td>
+                                <input
+                                    type="checkbox"
+                                    className="form-check-input"
+                                    name="fsmpi_authentication"
+                                    defaultChecked={allDataValue?.fsmpi_authentication ?? false}
+                                    disabled={disabled}
+                                    onChange={(e) =>
+                                        updateData("fsmpi_authentication", e.target.checked)
+                                    }
+                                />
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Moodle course ids</td>
+                            <td>
+                                {createListEditor<int>(
+                                    disabled ?? false,
+                                    allDataValue?.moodle_course_ids ?? [],
+                                    0,
+                                    (newList: int[]) => updateData("moodle_course_ids", newList),
+                                    (value: int, onNewValue: (newVal: int) => void) => (
+                                        <input
+                                            className="mb-1"
+                                            type="number"
+                                            value={value}
+                                            onChange={(e) => {
+                                                onNewValue(parseInt(e.target.value));
+                                            }}
+                                            disabled={disabled ?? false}
+                                        />
+                                    ),
+                                )}
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>Passwords</td>
+                            <td>
+                                <div className="d-inline-grid">
+                                    <span>
+                                        {createListEditor<[string, string]>(
+                                            disabled ?? false,
+                                            passwordsArray,
+                                            ["", ""],
+                                            (newList: [string, string][]) =>
+                                                updateData("passwords", newList),
+                                            (
+                                                value: [string, string],
+                                                onNewValue: (newVal: [string, string]) => void,
+                                            ) => (
+                                                <>
+                                                    <input
+                                                        className="me-1"
+                                                        type="text"
+                                                        placeholder="Username"
+                                                        value={value[0]}
+                                                        onChange={(e) =>
+                                                            onNewValue([e.target.value, value[1]])
+                                                        }
+                                                        disabled={disabled ?? false}
+                                                    />
+                                                    <input
+                                                        type="text"
+                                                        placeholder="Password"
+                                                        value={value[1]}
+                                                        onChange={(e) =>
+                                                            onNewValue([value[0], e.target.value])
+                                                        }
+                                                        disabled={disabled ?? false}
+                                                    />
+                                                </>
+                                            ),
+                                        )}
+                                    </span>
+                                    {passwordsInvalidMessage}
+                                </div>
+                            </td>
+                        </tr>
+                    </tbody>
+                </table>
+            )}
+            {invalidMessage}
+        </div>
+    );
+}