import * as React from "react"; import {useEffect, useRef, useState} from "react"; import {FlatInputField} from "tc-shared/ui/react-elements/InputField"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {Switch} from "tc-shared/ui/react-elements/Switch"; import PermissionType from "tc-shared/permission/PermissionType"; import * as log from "tc-shared/log"; import {LogCategory} from "tc-shared/log"; import ResizeObserver from "resize-observer-polyfill"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {Button} from "tc-shared/ui/react-elements/Button"; import {IconRenderer, LocalIconRenderer} from "tc-shared/ui/react-elements/Icon"; import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {copy_to_clipboard} from "tc-shared/utils/helpers"; import {createInfoModal} from "tc-shared/ui/elements/Modal"; const cssStyle = require("./PermissionEditor.scss"); export interface EditorGroupedPermissions { groupId: string, groupName: string, permissions: { id: number, name: string; description: string; }[], children: EditorGroupedPermissions[] } type PermissionEditorMode = "unset" | "no-permissions" | "normal"; export interface PermissionEditorEvents { action_set_mode: { mode: PermissionEditorMode, failedPermission?: string } action_toggle_client_button: { visible: boolean }, action_toggle_client_list: { visible: boolean }, action_set_filter: { filter?: string } action_set_assigned_only: { value: boolean } action_set_default_value: { value: number }, action_open_icon_select: { iconId?: number } action_set_senseless_permissions: { permissions: string[] } action_remove_permissions: { permissions: { name: string; mode: "value" | "grant"; }[] } action_remove_permissions_result: { permissions: { name: string; mode: "value" | "grant"; success: boolean; }[] } action_set_permissions: { permissions: { name: string; mode: "value" | "grant"; value?: number; flagNegate?: boolean; flagSkip?: boolean; }[] } action_set_permissions_result: { permissions: { name: string; mode: "value" | "grant"; newValue?: number; /* undefined if it didnt worked */ flagNegate?: boolean; flagSkip?: boolean; }[] } action_toggle_group: { groupId: string | null; /* if null, all groups are affected */ collapsed: boolean; } action_start_permission_edit: { target: "value" | "grant"; permission: string; defaultValue: number; }, action_add_permission_group: { groupId: string, mode: "value" | "grant"; }, action_remove_permission_group: { groupId: string mode: "value" | "grant"; } query_permission_list: {}, query_permission_list_result: { hideSenselessPermissions: boolean; permissions: EditorGroupedPermissions[] }, query_permission_values: {}, query_permission_values_result: { status: "error" | "success"; /* no perms will cause a action_set_mode event with no permissions */ error?: string; permissions?: { name: string; value?: number; flagNegate?: boolean; flagSkip?: boolean; granted?: number; }[] } } const ButtonIconPreview = (props: { events: Registry, connection: ConnectionHandler }) => { const [iconId, setIconId] = useState(0); const [unset, setUnset] = useState(true); props.events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal")); props.events.reactUse("action_remove_permissions_result", event => { const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID); if (!iconPermission || !iconPermission.success) return; if (iconPermission.mode === "value") setIconId(0); }); props.events.reactUse("action_set_permissions_result", event => { const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID); if (!iconPermission) return; if (typeof iconPermission.newValue === "number") setIconId(iconPermission.newValue); }); props.events.reactUse("query_permission_values_result", event => { if (event.status !== "success") { setIconId(0); return; } const permission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID); if (!permission) { setIconId(0); return; } if (typeof permission.value === "number") { setIconId(permission.value >>> 0); } else { setIconId(0); } }); let icon; if (!unset && iconId > 0) icon = ; return (
props.events.fire("action_open_icon_select", {iconId: iconId})}> {icon}
{iconId ?
props.events.fire("action_open_icon_select", {iconId: iconId})}> Edit icon
: undefined} {iconId ?
props.events.fire("action_remove_permissions", { permissions: [{ name: PermissionType.I_ICON_ID, mode: "value" }] })}> Remove icon
: undefined} {!iconId ?
props.events.fire("action_open_icon_select", {iconId: 0})}> Add icon
: undefined}
); }; const ClientListButton = (props: { events: Registry }) => { const [visible, setVisible] = useState(true); const [toggled, setToggled] = useState(false); props.events.reactUse("action_toggle_client_button", event => setVisible(event.visible)); props.events.reactUse("action_toggle_client_list", event => setToggled(event.visible)); return }; const MenuBar = (props: { events: Registry, connection: ConnectionHandler }) => { return
Filter permissions} labelType={"floating"} labelClassName={cssStyle.label} labelFloatingClassName={cssStyle.labelFloating} onInput={text => props.events.fire("action_set_filter", {filter: text})} />
Assigned only} onChange={state => props.events.fire("action_set_assigned_only", {value: state})}/> { /* Editable only} /> */}
; }; interface LinkedGroupedPermissions { groupId: string; groupName: string; depth: number; parent: LinkedGroupedPermissions | undefined; children: LinkedGroupedPermissions[]; permissions: { id: number; name: string; description: string; elementVisible: boolean; }[], anyPermissionVisible: boolean; nextGroup: LinkedGroupedPermissions; nextIfCollapsed: LinkedGroupedPermissions; collapsed: boolean; elementVisible: boolean; } const PermissionEntryRow = (props: { events: Registry, groupId: string, permission: string, value: PermissionValue, isOdd: boolean, depth: number, offsetTop: number, defaultValue: number, description: string }) => { const [defaultValue, setDefaultValue] = useState(props.defaultValue); const [value, setValue] = useState(props.value.value); const [forceValueUpdate, setForceValueUpdate] = useState(false); const [valueEditing, setValueEditing] = useState(false); const [valueApplying, setValueApplying] = useState(false); const [flagNegated, setFlagNegated] = useState(props.value.flagNegate); const [flagSkip, setFlagSkip] = useState(props.value.flagSkip); const [granted, setGranted] = useState(props.value.granted); const [forceGrantedUpdate, setForceGrantedUpdate] = useState(false); const [grantedEditing, setGrantedEditing] = useState(false); const [grantedApplying, setGrantedApplying] = useState(false); const refGranted = useRef(); const refValueI = useRef(); const refValueB = useRef(); const refSkip = useRef(); const refNegate = useRef(); const isActive = typeof value === "number" || typeof granted === "number"; const isBoolPermission = props.permission.startsWith("b_"); let valueElement, skipElement, negateElement, grantedElement; if (typeof value === "number") { if (isBoolPermission) { valueElement = = 1} disabled={valueApplying} onChange={flag => { props.events.fire("action_set_permissions", { permissions: [{ name: props.permission, mode: "value", value: flag ? 1 : 0, flagSkip: flagSkip, flagNegate: flagNegated }] }); }} onBlur={() => setValueEditing(false)}/>; } else if (valueApplying) { valueElement = { }}/>; } else { valueElement = { setValueEditing(false); if (!refValueI.current) return; const newValue = refValueI.current.value; if (newValue === "") { if (typeof value !== "number" && !forceValueUpdate) { /* no change */ return; } setForceValueUpdate(false); props.events.fire("action_remove_permissions", { permissions: [{ name: props.permission, mode: "value" }] }); } else { const numberValue = parseInt(newValue); if (isNaN(numberValue)) return; if (numberValue === value && !forceValueUpdate) { /* no change */ return; } setForceValueUpdate(false); props.events.fire("action_set_permissions", { permissions: [{ name: props.permission, mode: "value", value: numberValue, flagSkip: flagSkip, flagNegate: flagNegated }] }); } }} onChange={() => { }} onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}/>; } skipElement = { props.events.fire("action_set_permissions", { permissions: [{ name: props.permission, mode: "value", value: value, flagSkip: flag, flagNegate: flagNegated }] }); }}/>; negateElement = { props.events.fire("action_set_permissions", { permissions: [{ name: props.permission, mode: "value", value: value, flagSkip: flagSkip, flagNegate: flag }] }); }}/>; } if (typeof granted === "number") { if (grantedApplying) { grantedElement = { }}/>; } else { grantedElement = { setGrantedEditing(false); if (!refGranted.current) return; const newValue = refGranted.current.value; if (newValue === "") { if (typeof granted === "undefined") return; setForceGrantedUpdate(true); props.events.fire("action_remove_permissions", { permissions: [{ name: props.permission, mode: "grant" }] }); } else { const numberValue = parseInt(newValue); if (isNaN(numberValue)) return; if (numberValue === granted && !forceGrantedUpdate) { /* no change */ return; } setForceGrantedUpdate(true); props.events.fire("action_set_permissions", { permissions: [{ name: props.permission, mode: "grant", value: numberValue }] }); } }} onChange={() => { }} onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}/>; } } props.events.reactUse("action_start_permission_edit", event => { if (event.permission !== props.permission) return; if (event.target === "grant") { setGranted(event.defaultValue); setGrantedEditing(true); setForceGrantedUpdate(true); } else { if (isBoolPermission && typeof value === "undefined") { setValue(event.defaultValue >= 1 ? 1 : 0); props.events.fire("action_set_permissions", { permissions: [{ name: props.permission, mode: "value", value: event.defaultValue >= 1 ? 1 : 0, flagSkip: flagSkip, flagNegate: flagNegated }] }); } else { setValue(event.defaultValue); setForceValueUpdate(true); setValueEditing(true); } } }); props.events.reactUse("action_set_permissions", event => { const values = event.permissions.find(e => e.name === props.permission); if (!values) return; if (values.mode === "value") { setValueApplying(true); refSkip.current?.setState({disabled: true}); refNegate.current?.setState({disabled: true}); } else { setGrantedApplying(true); } }); props.events.reactUse("action_set_permissions_result", event => { const result = event.permissions.find(e => e.name === props.permission); if (!result) return; if (result.mode === "value") { setValueApplying(false); if (typeof result.newValue === "number") { setValue(result.newValue); setFlagSkip(result.flagSkip); setFlagSkip(result.flagNegate); refValueB.current?.setState({disabled: false, checked: result.newValue >= 1}); refSkip.current?.setState({disabled: false, checked: result.flagSkip}); refNegate.current?.setState({disabled: false, checked: result.flagNegate}); refValueI.current && (refValueI.current.value = result.newValue.toString()); props.value.value = result.newValue; props.value.flagSkip = result.flagSkip; props.value.flagNegate = result.flagNegate; } else { refValueB.current?.setState({disabled: false, checked: props.value.value >= 1}); refSkip.current?.setState({disabled: false, checked: props.value.flagSkip}); refNegate.current?.setState({disabled: false, checked: props.value.flagNegate}); refValueI.current && (refValueI.current.value = props.value.value?.toString()); setValue(props.value.value); setFlagSkip(props.value.flagSkip); setFlagSkip(props.value.flagNegate); } } else { setGrantedApplying(false); if (typeof result.newValue === "number") { setGranted(result.newValue); refGranted.current && (refGranted.current.value = result.newValue.toString()); } else { setGranted(props.value.granted); refGranted.current && (refGranted.current.value = props.value.granted?.toString()); } } }); props.events.reactUse("action_remove_permissions", event => { const modes = event.permissions.find(e => e.name === props.permission); if (!modes) return; if (modes.mode === "value") { setValueApplying(true); refValueB.current?.setState({disabled: true}); refSkip.current?.setState({disabled: true}); refNegate.current?.setState({disabled: true}); } if (modes.mode === "grant") setGrantedApplying(true); }); props.events.reactUse("action_remove_permissions_result", event => { const modes = event.permissions.find(e => e.name === props.permission); if (!modes) return; if (modes.mode === "value") { modes.success && setValue(undefined); setValueApplying(false); setValueEditing(false); modes.success && setFlagSkip(false); modes.success && setFlagNegated(false); } if (modes.mode === "grant") { modes.success && setGranted(undefined); setGrantedEditing(false); setGrantedApplying(false); } }); props.events.reactUse("action_set_default_value", event => setDefaultValue(event.value)); useEffect(() => { if (grantedEditing) refGranted.current?.focus(); if (valueEditing) { refValueI.current?.focus(); refValueB.current?.focus(); } }); return (
{ if (e.isDefaultPrevented()) return; props.events.fire("action_start_permission_edit", { permission: props.permission, target: "value", defaultValue: defaultValue }); e.preventDefault(); }} onContextMenu={e => { e.preventDefault(); let entries: contextmenu.MenuEntry[] = []; if (typeof value === "undefined") { entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Add permission"), callback: () => props.events.fire("action_start_permission_edit", { permission: props.permission, target: "value", defaultValue: defaultValue }) }); } else { entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove permission"), callback: () => props.events.fire("action_remove_permissions", { permissions: [{ name: props.permission, mode: "value" }] }) }); } if (typeof granted === "undefined") { entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Add grant permission"), callback: () => props.events.fire("action_start_permission_edit", { permission: props.permission, target: "grant", defaultValue: defaultValue }) }); } else { entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove grant permission"), callback: () => props.events.fire("action_remove_permissions", { permissions: [{ name: props.permission, mode: "grant" }] }) }); } entries.push(contextmenu.Entry.HR()); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse group"), callback: () => props.events.fire("action_toggle_group", {groupId: props.groupId, collapsed: true}) }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: false}) }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse all"), callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: true}) }); entries.push(contextmenu.Entry.HR()); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Show permission description"), callback: () => { createInfoModal( tr("Permission description"), tr("Permission description for permission ") + props.permission + ":
" + props.description ).open(); } }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Copy permission name"), callback: () => copy_to_clipboard(props.permission) }); contextmenu.spawn_context_menu(e.pageX, e.pageY, ...entries); }} >
{props.permission}
{valueElement}
{skipElement}
{negateElement}
{ props.events.fire("action_start_permission_edit", { permission: props.permission, target: "grant", defaultValue: defaultValue }); e.preventDefault(); }}>{grantedElement}
); }; const PermissionGroupRow = (props: { events: Registry, group: LinkedGroupedPermissions, isOdd: boolean, offsetTop: number }) => { const [collapsed, setCollapsed] = useState(props.group.collapsed); props.events.reactUse("action_toggle_group", event => { if (event.groupId !== null && event.groupId !== props.group.groupId) return; setCollapsed(event.collapsed); }); return (
{ e.preventDefault(); let entries = []; entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Add permissions to this group"), callback: () => props.events.fire("action_add_permission_group", { groupId: props.group.groupId, mode: "value" }) }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove permissions from this group"), callback: () => props.events.fire("action_remove_permission_group", { groupId: props.group.groupId, mode: "value" }) }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Add granted permissions to this group"), callback: () => props.events.fire("action_add_permission_group", { groupId: props.group.groupId, mode: "grant" }) }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Remove granted permissions from this group"), callback: () => props.events.fire("action_remove_permission_group", { groupId: props.group.groupId, mode: "grant" }) }); entries.push(contextmenu.Entry.HR()); if (collapsed) { entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend group"), callback: () => props.events.fire("action_toggle_group", { groupId: props.group.groupId, collapsed: false }) }); } else { entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse group"), callback: () => props.events.fire("action_toggle_group", { groupId: props.group.groupId, collapsed: true }) }); } entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: false}) }); entries.push({ type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse all"), callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: true}) }); contextmenu.spawn_context_menu(e.pageX, e.pageY, ...entries); }} onDoubleClick={() => props.events.fire("action_toggle_group", { collapsed: !collapsed, groupId: props.group.groupId })} >
props.events.fire("action_toggle_group", { collapsed: !collapsed, groupId: props.group.groupId })}/>
{props.group.groupName}
); }; type PermissionValue = { value?: number, flagNegate?: boolean, flagSkip?: boolean, granted?: number }; @ReactEventHandler(e => e.props.events) class PermissionList extends React.Component<{ events: Registry }, { state: "loading" | "normal" | "error", viewHeight: number, scrollOffset: number, error?: string }> { private readonly refContainer = React.createRef(); private resizeObserver: ResizeObserver; private permissionsHead: LinkedGroupedPermissions; private permissionByGroupId: { [key: string]: LinkedGroupedPermissions } = {}; private permissionValuesByName: { [key: string]: PermissionValue } = {}; private hideSenselessPermissions = true; private senselessPermissions: string[] = []; private currentListElements: React.ReactElement[] = []; private heightPerElement = 28; /* default font size 28px */ private heightPerElementInitialized = false; private filterText: string | undefined; private filterAssignedOnly: boolean = false; private loadingPermissionList = true; private loadingPermissionValues = false; private defaultPermissionValue = 1; constructor(props) { super(props); this.state = { viewHeight: 0, scrollOffset: 0, state: "loading" } } render() { const view = this.visibleEntries(); let elements = this.state.state === "normal" ? this.currentListElements.slice(Math.max(0, view.begin - 5), Math.min(view.end + 5, this.currentListElements.length)) : []; return (
this.state.state === "normal" && this.setState({scrollOffset: this.refContainer.current.scrollTop})} onContextMenu={e => { if (e.isDefaultPrevented()) return; e.preventDefault(); contextmenu.spawn_context_menu(e.pageX, e.pageY, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Expend all"), callback: () => this.props.events.fire("action_toggle_group", { groupId: null, collapsed: false }) }, { type: contextmenu.MenuEntryType.ENTRY, name: tr("Collapse all"), callback: () => this.props.events.fire("action_toggle_group", {groupId: null, collapsed: true}) }); }}> {elements} ) } private visibleEntries() { let view_entry_count = Math.ceil(this.state.viewHeight / this.heightPerElement); const view_entry_begin = Math.floor(this.state.scrollOffset / this.heightPerElement); const view_entry_end = Math.min(this.currentListElements.length, view_entry_begin + view_entry_count); return { begin: view_entry_begin, end: view_entry_end } } componentDidMount(): void { this.resizeObserver = new ResizeObserver(entries => { if (entries.length !== 1) { if (entries.length === 0) log.warn(LogCategory.PERMISSIONS, tr("Permission editor resize observer fired resize event with no entries!")); else log.warn(LogCategory.PERMISSIONS, tr("Permission editor resize observer fired resize event with more than one entry which should not be possible (%d)!"), entries.length); return; } const bounds = entries[0].contentRect; if (this.state.viewHeight !== bounds.height) { log.debug(LogCategory.PERMISSIONS, tr("Handling height update and change permission view height to %d from %d"), bounds.height, this.state.viewHeight); this.setState({ viewHeight: bounds.height }); } }); this.resizeObserver.observe(this.refContainer.current); this.props.events.fire("query_permission_list"); } componentWillUnmount(): void { this.resizeObserver.disconnect(); this.resizeObserver = undefined; } private initializeElementHeight() { if (this.heightPerElementInitialized) return; requestAnimationFrame(() => { const firstElement = this.refContainer.current?.firstElementChild; /* the first element might be the space allocator in cases without any shown row elements */ if (firstElement && firstElement.classList.contains(cssStyle.row)) { this.heightPerElementInitialized = true; const rect = firstElement.getBoundingClientRect(); if (this.heightPerElement !== rect.height) { this.heightPerElement = rect.height; this.updateReactComponents(); } } }); } componentDidUpdate(prevProps: Readonly<{ events: Registry }>, prevState: Readonly<{ state: "loading" | "normal" | "error", viewHeight: number, scrollOffset: number, error?: string }>, snapshot?: any): void { if (prevState.state !== "normal" && this.state.state === "normal") requestAnimationFrame(() => this.refContainer.current.scrollTop = this.state.scrollOffset); if (this.state.state === "normal") this.initializeElementHeight(); } @EventHandler("query_permission_list") private handlePermissionListQuery() { this.loadingPermissionList = true; this.setState({state: "loading"}); } @EventHandler("query_permission_list_result") private handlePermissionList(event: PermissionEditorEvents["query_permission_list_result"]) { this.loadingPermissionList = false; this.hideSenselessPermissions = event.hideSenselessPermissions; const visitGroup = (group: EditorGroupedPermissions, parent: LinkedGroupedPermissions, depth: number): LinkedGroupedPermissions => { const result: LinkedGroupedPermissions = { groupName: group.groupName, groupId: group.groupId, collapsed: false, permissions: group.permissions.map(e => { return { name: e.name, id: e.id, description: e.description, elementVisible: true } }), anyPermissionVisible: true, depth: depth, parent: parent, children: [], nextGroup: undefined, nextIfCollapsed: undefined, /* will be set later */ elementVisible: true, }; if (group.children && group.children.length > 0) { result.nextGroup = visitGroup(group.children[0], result, depth + 1); result.children.push(result.nextGroup); let currentHead = result.nextGroup; for (let index = 1; index < group.children.length; index++) { currentHead.nextIfCollapsed = visitGroup(group.children[index], result, depth + 1); currentHead = currentHead.nextIfCollapsed; result.children.push(currentHead); } } return result; }; this.permissionsHead = visitGroup(event.permissions[0], undefined, 0); let currentHead = this.permissionsHead; for (let index = 1; index < event.permissions.length; index++) { currentHead.nextIfCollapsed = visitGroup(event.permissions[index], undefined, 0); currentHead = currentHead.nextIfCollapsed; } /* fixup the next group linkage */ currentHead = this.permissionsHead; while (currentHead) { if (!currentHead.nextIfCollapsed && currentHead.parent) currentHead.nextIfCollapsed = currentHead.parent.nextIfCollapsed; if (!currentHead.nextGroup) currentHead.nextGroup = currentHead.nextIfCollapsed; currentHead = currentHead.nextGroup; } /* build up the group key index */ this.permissionByGroupId = {}; currentHead = this.permissionsHead; while (currentHead) { this.permissionByGroupId[currentHead.groupId] = currentHead; currentHead = currentHead.nextGroup; } this.setState({state: this.loadingPermissionList || this.loadingPermissionValues ? "loading" : this.state.error ? "error" : "normal"}); this.updateReactComponents(); } @EventHandler("action_set_senseless_permissions") private handleSenselessPermissions(event: PermissionEditorEvents["action_set_senseless_permissions"]) { this.senselessPermissions = event.permissions.slice(0); this.updateReactComponents(); } @EventHandler("query_permission_values") private handleRequestPermissionValues() { this.loadingPermissionValues = true; this.setState({state: "loading"}); } @EventHandler("query_permission_values_result") private handleRequestPermissionValuesResult(event: PermissionEditorEvents["query_permission_values_result"]) { this.loadingPermissionValues = false; this.permissionValuesByName = {}; Object.values(this.permissionValuesByName).forEach(e => { e.value = undefined; e.granted = undefined; }); (event.permissions || []).forEach(permission => Object.assign(this.permissionValuesByName[permission.name] || (this.permissionValuesByName[permission.name] = {}), { value: permission.value, granted: permission.granted, flagNegate: permission.flagNegate, flagSkip: permission.flagSkip })); this.setState({ state: this.loadingPermissionList || this.loadingPermissionValues ? "loading" : event.status !== "success" ? "error" : "normal", error: event.error }); if (event.status === "success") this.updateReactComponents(); } @EventHandler("action_set_permissions_result") private handlePermissionSetResult(event: PermissionEditorEvents["action_set_permissions_result"]) { event.permissions.forEach(e => { if (typeof e.newValue !== "number") return; const values = this.permissionValuesByName[e.name] || (this.permissionValuesByName[e.name] = {}); if (e.mode === "value") { values.value = e.newValue; values.flagSkip = e.flagSkip; values.flagNegate = e.flagNegate; } else { values.granted = e.newValue; } }); } @EventHandler("action_remove_permissions_result") private handlePermissionRemoveResult(event: PermissionEditorEvents["action_remove_permissions_result"]) { event.permissions.forEach(e => { if (!e.success) return; const values = this.permissionValuesByName[e.name] || (this.permissionValuesByName[e.name] = {}); if (e.mode === "value") { values.value = undefined; values.flagSkip = false; values.flagNegate = false; } else { values.granted = undefined; } }); } @EventHandler("action_toggle_group") private handleToggleGroup(event: PermissionEditorEvents["action_toggle_group"]) { if (event.groupId === null) { Object.values(this.permissionByGroupId).forEach(e => e.collapsed = event.collapsed); } else { const group = this.permissionByGroupId[event.groupId]; if (!group) { console.warn(tr("Received group toogle for unknwon group: %s"), event.groupId); return; } if (group.collapsed === event.collapsed) return; group.collapsed = event.collapsed; } this.updateReactComponents(); } @EventHandler("action_set_filter") private handleSetFilter(event: PermissionEditorEvents["action_set_filter"]) { if (this.filterText === event.filter) return; this.filterText = event.filter; this.updateReactComponents(); } @EventHandler("action_set_assigned_only") private handleSetAssignedFilter(event: PermissionEditorEvents["action_set_assigned_only"]) { if (this.filterAssignedOnly === event.value) return; this.filterAssignedOnly = event.value; this.updateReactComponents(); } @EventHandler("action_set_default_value") private handleSetDefaultPermissionValue(event: PermissionEditorEvents["action_set_default_value"]) { this.defaultPermissionValue = event.value; } @EventHandler("action_add_permission_group") private handleEnablePermissionGroup(event: PermissionEditorEvents["action_add_permission_group"]) { const group = this.permissionByGroupId[event.groupId]; if (!group) return; const permissions: { id: number, name: string, elementVisible: boolean }[] = []; const visitGroup = (group: LinkedGroupedPermissions) => { permissions.push(...group.permissions); group.children.forEach(visitGroup); }; visitGroup(group); this.props.events.fire("action_set_permissions", { permissions: permissions.map(e => { return { name: e.name, mode: event.mode as "value" | "grant", value: e.name.startsWith("b_") && event.mode === "value" ? 1 : this.defaultPermissionValue, flagNegate: false, flagSkip: false } }) }); } @EventHandler("action_remove_permission_group") private handleDisablePermissionGroup(event: PermissionEditorEvents["action_remove_permission_group"]) { const group = this.permissionByGroupId[event.groupId]; if (!group) return; const permissions: { id: number, name: string, elementVisible: boolean }[] = []; const visitGroup = (group: LinkedGroupedPermissions) => { permissions.push(...group.permissions); group.children.forEach(visitGroup); }; visitGroup(group); this.props.events.fire("action_remove_permissions", { permissions: permissions.map(e => { return { name: e.name, mode: event.mode as "value" | "grant", } }) }); } private updateReactComponents() { let currentGroup = this.permissionsHead; let visibleGroups: LinkedGroupedPermissions[] = []; while (currentGroup) { visibleGroups.push(currentGroup); if (currentGroup.collapsed) { currentGroup = currentGroup.nextIfCollapsed; continue; } currentGroup.anyPermissionVisible = false; for (const permission of currentGroup.permissions) { permission.elementVisible = false; if (this.filterText && permission.name.indexOf(this.filterText) === -1) continue; if (this.hideSenselessPermissions && this.senselessPermissions.findIndex(e => e === permission.name) !== -1) continue; const permissionValue = this.permissionValuesByName[permission.name] || (this.permissionValuesByName[permission.name] = {}); if (this.filterAssignedOnly) { if (typeof permissionValue.value !== "number" && typeof permissionValue.granted !== "number") { continue; } } permission.elementVisible = true; currentGroup.anyPermissionVisible = true; } currentGroup = currentGroup.nextGroup; } /* update the visibility from the bottom to the top */ visibleGroups.sort((a, b) => b.depth - a.depth); visibleGroups.forEach(e => { for (const child of e.children) { if (child.elementVisible) { e.elementVisible = true; return; } } e.elementVisible = e.anyPermissionVisible; }); /* lets build up the final list view */ this.currentListElements = []; let index = 0; currentGroup = this.permissionsHead; while (currentGroup) { if (currentGroup.elementVisible) { this.currentListElements.push(); index++; } if (currentGroup.collapsed) { currentGroup = currentGroup.nextIfCollapsed; continue; } else if (!currentGroup.elementVisible) { currentGroup = currentGroup.nextGroup; continue; } currentGroup.permissions.forEach(e => { if (!e.elementVisible) return; this.currentListElements.push(); index++; }); currentGroup = currentGroup.nextGroup; } this.forceUpdate(); } } const PermissionTable = (props: { events: Registry }) => { const [mode, setMode] = useState("unset"); const [failedPermission, setFailedPermission] = useState(undefined); props.events.reactUse("action_set_mode", event => { setMode(event.mode); setFailedPermission(event.failedPermission); }); return (
); }; const RefreshButton = (props: { events: Registry }) => { const [unset, setUnset] = useState(true); const [nextTime, setNextTime] = useState(0); const refButton = useRef }; interface PermissionEditorProperties { connection: ConnectionHandler; events: Registry; } interface PermissionEditorState { state: "no-permissions" | "unset" | "normal"; } export class PermissionEditor extends React.Component { render() { return [ , ,
] } componentDidMount(): void { this.props.events.fire("action_set_mode", {mode: "unset"}); } }