import m from "mithril" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {ModalView, ModalType, iModalView} from "./widgets/Modal" import {gridMountpoints} from "./mainlayout" import {iProjektUser, fetchProjektUsers, addProjektUserREST, removeProjektUserREST} from "../models/projekt_user" import {iProjekt, fetchProjektById} from "../models/projekt" import {iUser} from "./auth/models/users" import {showServerError} from "./auth/authmessages" /** * Liste der einem Projekt zugeordneten Nutzer (Sichtbarkeit). * * Der Liste wird die projekt_id über die Route-Params übergeben. Sie zeigt * alle Nutzer, die das Projekt sehen dürfen. Der AppAdmin (und SysAdmin) * kann Nutzer über ein Modal mit Select-Widget hinzufügen und über einen * Entfernen-Button pro Zeile wieder wegnehmen. */ export const projektuserliste: m.ClosureComponent = () => { let treenodes: iTreenode[] = [] let selectedNode: iTreenode | null = null let projektId: number = -1 let projektName: string = "" let projektUsers: iProjektUser[] = [] // Alle Nutzer aus der Auth-DB, für das Hinzufügen-Modal. let allUsers: iUser[] = [] const handleRowMarked = (node: iTreenode) => { selectedNode = node } /** * Lädt die zugeordneten Nutzer vom Server und aktualisiert die Treenodes. */ const loadProjektUsers = () => { fetchProjektUsers(projektId).then((result) => { projektUsers = result treenodes = result.map((pu: iProjektUser) => new Treenode(pu, true, [])) }).catch((e) => showServerError(e)) } /** * Lädt alle Nutzer aus der Auth-DB (für das Hinzufügen-Modal). */ const loadAllUsers = () => { m.request({ method: "GET", url: "/auth/getusers" }).then((result) => { allUsers = result as iUser[] }).catch((e) => showServerError(e)) } /** * Entfernt einen Nutzer aus dem Projekt. Wird beim Klick auf den * Entfernen-Button in der Zeile aufgerufen. */ const removeUser = (projektUser: iProjektUser, e: Event) => { e.stopPropagation() removeProjektUserREST(projektId, projektUser.user_id).then(() => { loadProjektUsers() }).catch((e) => showServerError(e)) } const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { let pu: iProjektUser = node.nodevalue return [ m("div", m("strong", pu.userid)), m("span", pu.username), m("span", pu.email), m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}}, m("button.w3-button.w3-small.w3-red", { title: "Nutzer entfernen", style: {padding: '0 4px'}, onclick: (e: Event) => removeUser(pu, e) }, m("i.material-icons", "person_remove")) ) ] } const filterFunction = (node: iTreenode, searchvalue: string): boolean => { let pu: iProjektUser = node.nodevalue let searchLower = searchvalue.toLowerCase() return (pu.userid || "").toLowerCase().includes(searchLower) || (pu.username || "").toLowerCase().includes(searchLower) || (pu.email || "").toLowerCase().includes(searchLower) } /** * Öffnet das Modal zum Hinzufügen eines Nutzers. Die verfügbaren * Nutzer werden clientseitig gegen die bereits zugeordneten gefiltert. */ const openAddModal = () => { // Alle Nutzer laden, falls noch nicht geschehen. if (allUsers.length === 0) { loadAllUsers() } m.mount(gridMountpoints.modalhook, addUserModal) } /** * Modal-Komponente zur Auswahl mehrerer Nutzer, die dem Projekt * zugeordnet werden sollen. Zeigt eine durchsuchbare Liste mit * Checkboxen pro noch nicht zugeordnetem Nutzer. Bereits zugeordnete * Nutzer werden ausgeblendet. */ const addUserModal: m.ClosureComponent = () => { // Menge der ausgewählten user_ids (Checkbox aktiviert). let selectedIds: Set = new Set() // Suchtext zum Filtern der Liste. let search: string = "" // Verfügbare Nutzer: alle, die noch nicht zugeordnet sind, nicht // SysAdmin oder AppAdmin sind (diese haben ohnehin immer Zugriff) // und deren Suchtext-Filter entsprechen. Sortiert nach username. const getAvailableUsers = (): iUser[] => { const assignedIds = new Set(projektUsers.map((pu) => pu.user_id)) const searchLower = search.toLowerCase() return allUsers.filter((u: iUser) => { // SysAdmin und AppAdmin haben immer Zugriff, // sie brauchen keine projektbezogene Zuordnung. if (u.isSysAdmin) return false if (u.userapps && u.userapps.some((ua) => ua.userapp_roles && ua.userapp_roles.some((r) => r.roleid === "AppAdmin") )) return false if (assignedIds.has(u.id)) return false if (searchLower === "") return true return (u.userid || "").toLowerCase().includes(searchLower) || (u.username || "").toLowerCase().includes(searchLower) || (u.email || "").toLowerCase().includes(searchLower) }).sort((a: iUser, b: iUser) => { return (a.username || "").localeCompare(b.username || "") }) } const closeModal = () => { m.mount(gridMountpoints.modalhook, null) } /** * Ordnet alle ausgewählten Nutzer dem Projekt zu. Die Aufrufe * erfolgen parallel; nachdem alle abgeschlossen sind, wird das * Modal geschlossen und die Liste neu geladen. */ const confirmAdd = () => { if (selectedIds.size === 0) { closeModal() return } const promises: Promise[] = [] selectedIds.forEach((uid: number) => { promises.push(addProjektUserREST(projektId, uid)) }) Promise.all(promises).then(() => { closeModal() loadProjektUsers() }).catch((e) => { showServerError(e) loadProjektUsers() }) } /** * Schaltet die Auswahl für einen Nutzer um. */ const toggleUser = (userId: number) => { if (selectedIds.has(userId)) { selectedIds.delete(userId) } else { selectedIds.add(userId) } } return { view: (vnode) => { const availableUsers = getAvailableUsers() return m(ModalView, { title: "Nutzer zuordnen", type: ModalType.MESSAGE, width: '500px' } as iModalView, m("div.w3-padding-16", m("p", "Wählen Sie die Nutzer aus, die dem Projekt \"" + projektName + "\" zugeordnet werden sollen:"), // Suchfeld zum Filtern der Nutzerliste. m("label", "Suche"), m("input.w3-input.w3-border", { type: "text", value: search, placeholder: "Nach userid, Name oder E-Mail suchen", style: {'margin-bottom': '8px'}, oninput: (e: Event) => { search = (e.target as HTMLInputElement).value } } ), // Scrollbare Liste mit Checkboxen. m("div", { style: { 'max-height': '300px', 'overflow-y': 'auto', 'margin-top': '8px', border: '1px solid #ccc' } }, availableUsers.length === 0 ? m("div.w3-padding-small", "Keine weiteren Nutzer verfügbar") : availableUsers.map((u: iUser) => m("label.w3-block.w3-hover-light-grey.w3-text-left", { style: {'border-bottom': '1px solid #eee', padding: '4px 8px'}, onclick: () => toggleUser(u.id) }, m("input", { type: "checkbox", checked: selectedIds.has(u.id), style: {'margin-right': '8px'}, onclick: (e: Event) => e.stopPropagation(), onchange: (e: Event) => toggleUser(u.id) } ), m("strong", u.userid), " (" + u.username + ")", u.email ? m("span.w3-small.w3-opacity", " — " + u.email) : null ) ) ), m("div.w3-bar.w3-padding-16", m("span.w3-small.w3-left", {style: {'line-height': '36px', 'margin-right': '8px'}}, selectedIds.size + " ausgewählt"), m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: confirmAdd}, "Zuordnen"), m("button.w3-button.w3-theme.w3-right", {style: {'margin-left': '4px'}, onclick: closeModal}, "Abbrechen") ) ) ) } } } return { oninit: (vnode) => { const routeParams = m.route.param() projektId = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1 projektName = routeParams && routeParams.projekt_name ? routeParams.projekt_name : "" // Projektname asynchron auflösen, falls nicht in Route-Params. if (projektId !== -1 && projektName === "") { fetchProjektById(projektId).then((projekt: iProjekt) => { projektName = projekt.projektname }).catch((e) => showServerError(e)) } // Alle Nutzer für das Hinzufügen-Modal vorab laden. loadAllUsers() // Zugeordnete Nutzer laden. loadProjektUsers() }, view: (vnode) => { return m("div.w3-container.w3-auto", { style: { display: 'flex', 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit' } }, m("h1", projektName ? "Nutzerberechtigungen für Projekt: " + projektName : "Nutzerberechtigungen"), m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m(Treetable, { treenodes: treenodes, columnmapper: columnmapper, filter: filterFunction, onRowMarked: handleRowMarked, isSelected: (node: iTreenode) => node === selectedNode } as iTreetable, m(TopbarButton, { onclick: openAddModal, tooltip: "Nutzer hinzufügen" } as iTopbarButton, m("i.material-icons", "person_add") ) ) ) ) } } }