ProjektUser-Modal: Mehrfachauswahl, Filterung und Fehlermeldungs-Verbesserung
- Modal von Einzelauswahl (Select) auf Mehrfachauswahl mit Checkboxen umgestellt - Nutzerliste sortiert nach username, scrollbar (eine Zeile pro Nutzer) - SysAdmin und AppAdmin aus der Auswahlliste ausgeblendet (immer Zugriff) - Bereits zugeordnete Nutzer ausgeblendet - Fehleranzeige bei fehlender Rolle: closeModal im catch entfernt, sodass showServerError nicht sofort überschrieben wird - AutorisationError: Meldung "Erforderliche Rollen" nur bei nicht-leerer allowed_roles-Liste (entfällt bei @authorisation_required([])) Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
4cb0084066
commit
505177b5e2
2 files changed
+119
-36
No files matched your search
@@ -22,10 +22,17 @@ class AutorisationError(ApplicationError):
|
|||||||
else:
|
else:
|
||||||
userrolestxt = ", ".join([role.name for role in self.user_roles])
|
userrolestxt = ", ".join([role.name for role in self.user_roles])
|
||||||
|
|
||||||
ApplicationError.__init__(self,
|
# Die Meldung über erforderliche Rollen nur aufnehmen, wenn
|
||||||
"Sie haben nicht die passende Rolle für diese Funktion",
|
# welche spezifiziert sind. Bei leerer allowed_roles-Liste
|
||||||
"Ihre Rollen sind: " + userrolestxt,
|
# (z.B. Endpunkte, die nur AppAdmin/SysAdmin erlauben) entfällt
|
||||||
"Erforderliche Rollen sind: " + ", ".join(self.allowed_roles), htmlstatus=HtmlStatusCode.Unauthorized)
|
# dieser Teil der Fehlermeldung komplett.
|
||||||
|
messages = [
|
||||||
|
"Sie haben nicht die passende Rolle für diese Funktion",
|
||||||
|
"Ihre Rollen sind: " + userrolestxt,
|
||||||
|
]
|
||||||
|
if self.allowed_roles:
|
||||||
|
messages.append("Erforderliche Rollen sind: " + ", ".join(self.allowed_roles))
|
||||||
|
ApplicationError.__init__(self, *messages, htmlstatus=HtmlStatusCode.Unauthorized)
|
||||||
|
|
||||||
class HtmlStatusCode():
|
class HtmlStatusCode():
|
||||||
Bad_Request = 400
|
Bad_Request = 400
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import m from "mithril"
|
import m from "mithril"
|
||||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||||
import {Select, iSelect, iSelectOption} from "./widgets/select"
|
|
||||||
import {ModalView, ModalType, iModalView} from "./widgets/Modal"
|
import {ModalView, ModalType, iModalView} from "./widgets/Modal"
|
||||||
import {gridMountpoints} from "./mainlayout"
|
import {gridMountpoints} from "./mainlayout"
|
||||||
import {iProjektUser, fetchProjektUsers, addProjektUserREST, removeProjektUserREST} from "../models/projekt_user"
|
import {iProjektUser, fetchProjektUsers, addProjektUserREST, removeProjektUserREST} from "../models/projekt_user"
|
||||||
@@ -101,43 +100,81 @@ export const projektuserliste: m.ClosureComponent = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal-Komponente zur Auswahl eines Nutzers, der dem Projekt
|
* Modal-Komponente zur Auswahl mehrerer Nutzer, die dem Projekt
|
||||||
* zugeordnet werden soll. Verwendet das Select-Widget für die
|
* zugeordnet werden sollen. Zeigt eine durchsuchbare Liste mit
|
||||||
* Suche/Auswahl. Bereits zugeordnete Nutzer werden ausgeblendet.
|
* Checkboxen pro noch nicht zugeordnetem Nutzer. Bereits zugeordnete
|
||||||
|
* Nutzer werden ausgeblendet.
|
||||||
*/
|
*/
|
||||||
const addUserModal: m.ClosureComponent = () => {
|
const addUserModal: m.ClosureComponent = () => {
|
||||||
let selectedUserId: number = -1
|
// Menge der ausgewählten user_ids (Checkbox aktiviert).
|
||||||
|
let selectedIds: Set<number> = new Set()
|
||||||
|
// Suchtext zum Filtern der Liste.
|
||||||
|
let search: string = ""
|
||||||
|
|
||||||
// Verfügbare Nutzer: alle, die noch nicht zugeordnet sind.
|
// Verfügbare Nutzer: alle, die noch nicht zugeordnet sind, nicht
|
||||||
const getAvailableOptions = (): iSelectOption[] => {
|
// 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 assignedIds = new Set(projektUsers.map((pu) => pu.user_id))
|
||||||
return allUsers
|
const searchLower = search.toLowerCase()
|
||||||
.filter((u: iUser) => !assignedIds.has(u.id))
|
return allUsers.filter((u: iUser) => {
|
||||||
.map((u: iUser) => ({
|
// SysAdmin und AppAdmin haben immer Zugriff,
|
||||||
value: u.id,
|
// sie brauchen keine projektbezogene Zuordnung.
|
||||||
label: `${u.userid} (${u.username})`
|
if (u.isSysAdmin) return false
|
||||||
} as iSelectOption))
|
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 = () => {
|
const closeModal = () => {
|
||||||
m.mount(gridMountpoints.modalhook, null)
|
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 = () => {
|
const confirmAdd = () => {
|
||||||
if (selectedUserId > 0) {
|
if (selectedIds.size === 0) {
|
||||||
addProjektUserREST(projektId, selectedUserId).then(() => {
|
closeModal()
|
||||||
closeModal()
|
return
|
||||||
loadProjektUsers()
|
}
|
||||||
}).catch((e) => {
|
const promises: Promise<void>[] = []
|
||||||
showServerError(e)
|
selectedIds.forEach((uid: number) => {
|
||||||
closeModal()
|
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 {
|
return {
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
const options = getAvailableOptions()
|
const availableUsers = getAvailableUsers()
|
||||||
return m(ModalView,
|
return m(ModalView,
|
||||||
{
|
{
|
||||||
title: "Nutzer zuordnen",
|
title: "Nutzer zuordnen",
|
||||||
@@ -145,18 +182,57 @@ export const projektuserliste: m.ClosureComponent = () => {
|
|||||||
width: '500px'
|
width: '500px'
|
||||||
} as iModalView,
|
} as iModalView,
|
||||||
m("div.w3-padding-16",
|
m("div.w3-padding-16",
|
||||||
m("p", "Wählen Sie einen Nutzer aus, der dem Projekt \"" + projektName + "\" zugeordnet werden soll:"),
|
m("p", "Wählen Sie die Nutzer aus, die dem Projekt \"" + projektName + "\" zugeordnet werden sollen:"),
|
||||||
m("label", "Nutzer"),
|
// Suchfeld zum Filtern der Nutzerliste.
|
||||||
m(Select, {
|
m("label", "Suche"),
|
||||||
name: "user_id",
|
m("input.w3-input.w3-border",
|
||||||
value: selectedUserId,
|
{
|
||||||
options: options,
|
type: "text",
|
||||||
required: true,
|
value: search,
|
||||||
onchanged: (value: any) => {
|
placeholder: "Nach userid, Name oder E-Mail suchen",
|
||||||
selectedUserId = value
|
style: {'margin-bottom': '8px'},
|
||||||
|
oninput: (e: Event) => {
|
||||||
|
search = (e.target as HTMLInputElement).value
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} as iSelect),
|
),
|
||||||
|
// 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("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",
|
m("button.w3-button.w3-theme.w3-right",
|
||||||
{style: {'margin-left': '4px'}, onclick: confirmAdd},
|
{style: {'margin-left': '4px'}, onclick: confirmAdd},
|
||||||
"Zuordnen"),
|
"Zuordnen"),
|
||||||
|
|||||||
Reference in new issue
Block a user