TeaWeb/shared/js/ui/modal/permission/EditorRenderer.tsx
2021-03-23 12:03:00 +01:00

1315 lines
No EOL
52 KiB
TypeScript

import * as React from "react";
import {useContext, 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 {LogCategory, logDebug, logWarn} 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, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {copyToClipboard} from "tc-shared/utils/helpers";
import {createInfoModal} from "tc-shared/ui/elements/Modal";
import {getIconManager} from "tc-shared/file/Icons";
import {
EditorGroupedPermissions,
PermissionEditorEvents,
PermissionEditorMode
} from "tc-shared/ui/modal/permission/EditorDefinitions";
import {ContextMenuEntry, spawnContextMenu} from "tc-shared/ui/ContextMenu";
import {Arrow} from "tc-shared/ui/react-elements/Arrow";
const cssStyle = require("./EditorRenderer.scss");
const EventContext = React.createContext<Registry<PermissionEditorEvents>>(undefined);
const ServerInfoContext = React.createContext<{ handlerId: string, serverUniqueId: string }>(undefined);
const ButtonIconPreview = React.memo(() => {
const serverInfo = useContext(ServerInfoContext);
const events = useContext(EventContext);
const [iconId, setIconId] = useState(0);
const [unset, setUnset] = useState(true);
events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal"));
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);
});
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);
});
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 = <RemoteIconRenderer key={"icon-" + iconId} icon={getIconManager().resolveIcon(iconId, serverInfo.serverUniqueId, serverInfo.handlerId)} />;
}
return (
<div className={cssStyle.containerIconSelect}>
<div className={cssStyle.preview}
onClick={() => events.fire("action_open_icon_select", {iconId: iconId})}>
{icon}
</div>
<div className={cssStyle.containerDropdown}>
<div className={cssStyle.button}>
<Arrow direction={"down"} className={cssStyle.arrow} />
</div>
<div className={cssStyle.dropdown}>
{iconId ? (
<div className={cssStyle.entry} key={"edit-icon"}
onClick={() => events.fire("action_open_icon_select", {iconId: iconId})}>
<Translatable>Edit icon</Translatable>
</div>
) : undefined}
{iconId ? (
<div className={cssStyle.entry} key={"remove-icon"}
onClick={() => events.fire("action_remove_permissions", {
permissions: [{
name: PermissionType.I_ICON_ID,
mode: "value"
}]
})}>
<Translatable>Remove icon</Translatable>
</div>
) : undefined}
{!iconId ? (
<div className={cssStyle.entry} key={"add-icon"}
onClick={() => events.fire("action_open_icon_select", {iconId: 0})}>
<Translatable>Add icon</Translatable>
</div>
) : undefined}
</div>
</div>
</div>
);
});
const ClientListButton = () => {
const events = useContext(EventContext);
const [visible, setVisible] = useState(true);
const [toggled, setToggled] = useState(false);
events.reactUse("action_toggle_client_button", event => setVisible(event.visible));
events.reactUse("action_toggle_client_list", event => setToggled(event.visible));
return <Button
key={"button-clients"}
className={cssStyle.clients + " " + (visible ? "" : cssStyle.hidden)}
color={"green"}
onClick={() => events.fire("action_toggle_client_list", {visible: !toggled})}>
{toggled ? <Translatable key={"hide"}>Hide clients in group</Translatable> :
<Translatable key={"show"}>Show clients in group</Translatable>}
</Button>
};
const MenuBar = React.memo(() => {
const events = useContext(EventContext);
return (
<div className={cssStyle.containerMenuBar}>
<ClientListButton />
<FlatInputField
className={cssStyle.filter}
label={<Translatable>Filter permissions</Translatable>}
labelType={"floating"}
labelClassName={cssStyle.label}
labelFloatingClassName={cssStyle.labelFloating}
onInput={text => events.fire("action_set_filter", {filter: text})}
/>
<div className={cssStyle.options}>
<Switch
initialState={false}
label={<Translatable>Assigned only</Translatable>}
onChange={state => events.fire("action_set_assigned_only", {value: state})}
/>
{ /* <Switch initialState={true} label={<Translatable>Editable only</Translatable>} /> */}
</div>
<ButtonIconPreview />
</div>
);
});
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 = React.memo((props: {
groupId: string,
permission: string,
value: PermissionValue,
isOdd: boolean,
depth: number,
offsetTop: number,
defaultValue: number,
description: string
}) => {
const events = useContext(EventContext);
const [defaultValue, setDefaultValue] = useState(props.defaultValue);
const [value, setValue] = useState<number>(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 || false);
const [flagSkip, setFlagSkip] = useState(props.value.flagSkip || false);
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<HTMLInputElement>();
const refValueI = useRef<HTMLInputElement>();
const refValueB = useRef<Switch>();
const refSkip = useRef<Switch>();
const refNegate = useRef<Switch>();
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 = <Switch ref={refValueB} key={"value-b"} initialState={value >= 1} disabled={valueApplying}
onChange={flag => {
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 =
<input key={"value-i-applying"} className={cssStyle.applying} type="number" placeholder={tr("applying")}
readOnly={true} onChange={() => {
}}/>;
} else {
valueElement =
<input ref={refValueI} key={"value-i"} type="number" disabled={valueApplying} defaultValue={value}
onBlur={() => {
setValueEditing(false);
if (!refValueI.current)
return;
const newValue = refValueI.current.value;
if (newValue === "") {
if (typeof value !== "number" && !forceValueUpdate) {
/* no change */
return;
}
setForceValueUpdate(false);
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);
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 = <Switch key={"skip"} initialState={flagSkip} disabled={valueApplying} onChange={flag => {
events.fire("action_set_permissions", {
permissions: [{
name: props.permission,
mode: "value",
value: value,
flagSkip: flag,
flagNegate: flagNegated
}]
});
}}/>;
negateElement = <Switch key={"negate"} initialState={flagNegated} disabled={valueApplying} onChange={flag => {
events.fire("action_set_permissions", {
permissions: [{
name: props.permission,
mode: "value",
value: value,
flagSkip: flagSkip,
flagNegate: flag
}]
});
}}/>;
}
if (typeof granted === "number") {
if (grantedApplying) {
grantedElement = (
<input
key={"grant-applying"}
className={cssStyle.applying}
type="number"
placeholder={tr("applying")}
readOnly={true}
onChange={() => {}}
/>
);
} else {
grantedElement = (
<input
ref={refGranted}
key={"grant"}
type="number"
defaultValue={granted}
onBlur={() => {
setGrantedEditing(false);
if (!refGranted.current)
return;
const newValue = refGranted.current.value;
if (newValue === "") {
if (typeof granted === "undefined")
return;
setForceGrantedUpdate(true);
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);
events.fire("action_set_permissions", {
permissions: [{
name: props.permission,
mode: "grant",
value: numberValue
}]
});
}
}}
onChange={() => {
}}
onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}
/>
);
}
}
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);
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);
}
}
});
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);
}
});
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());
}
}
});
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);
});
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);
}
});
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 (
<div
className={cssStyle.row + " " + cssStyle.permission + " " + (props.isOdd ? "" : cssStyle.even) + " " + (isActive ? cssStyle.active : "")}
style={{paddingLeft: (props.depth + 1) + "em", top: props.offsetTop}}
onDoubleClick={e => {
if (e.isDefaultPrevented())
return;
events.fire("action_start_permission_edit", {
permission: props.permission,
target: "value",
defaultValue: defaultValue
});
e.preventDefault();
}}
onContextMenu={e => {
e.preventDefault();
let entries: ContextMenuEntry[] = [];
if (typeof value === "undefined") {
entries.push({
type: "normal",
label: tr("Add permission"),
click: () => events.fire("action_start_permission_edit", {
permission: props.permission,
target: "value",
defaultValue: defaultValue
})
});
} else {
entries.push({
type: "normal",
label: tr("Remove permission"),
click: () => events.fire("action_remove_permissions", {
permissions: [{
name: props.permission,
mode: "value"
}]
})
});
}
if (typeof granted === "undefined") {
entries.push({
type: "normal",
label: tr("Add grant permission"),
click: () => events.fire("action_start_permission_edit", {
permission: props.permission,
target: "grant",
defaultValue: defaultValue
})
});
} else {
entries.push({
type: "normal",
label: tr("Remove grant permission"),
click: () => events.fire("action_remove_permissions", {
permissions: [{
name: props.permission,
mode: "grant"
}]
})
});
}
entries.push({ type: "separator" });
entries.push({
type: "normal",
label: tr("Collapse group"),
click: () => events.fire("action_toggle_group", {groupId: props.groupId, collapsed: true})
});
entries.push({
type: "normal",
label: tr("Expend all"),
click: () => events.fire("action_toggle_group", {groupId: null, collapsed: false})
});
entries.push({
type: "normal",
label: tr("Collapse all"),
click: () => events.fire("action_toggle_group", {groupId: null, collapsed: true})
});
entries.push({ type: "separator" });
entries.push({
type: "normal",
label: tr("Show permission description"),
click: () => {
createInfoModal(
tr("Permission description"),
tr("Permission description for permission ") + props.permission + ": <br>" + props.description
).open();
}
});
entries.push({
type: "normal",
label: tr("Copy permission name"),
click: () => copyToClipboard(props.permission)
});
spawnContextMenu({ pageX: e.pageX, pageY: e.pageY }, entries);
}}
>
<div className={cssStyle.columnName}>
{props.permission}
</div>
<div className={cssStyle.columnValue}>{valueElement}</div>
<div className={cssStyle.columnSkip}>{skipElement}</div>
<div className={cssStyle.columnNegate}>{negateElement}</div>
<div className={cssStyle.columnGranted} onDoubleClick={e => {
events.fire("action_start_permission_edit", {
permission: props.permission,
target: "grant",
defaultValue: defaultValue
});
e.preventDefault();
}}>{grantedElement}</div>
</div>
);
});
const PermissionGroupRow = React.memo((props: { group: LinkedGroupedPermissions, isOdd: boolean, offsetTop: number }) => {
const events = useContext(EventContext);
const [collapsed, setCollapsed] = useState(props.group.collapsed);
events.reactUse("action_toggle_group", event => {
if (event.groupId !== null && event.groupId !== props.group.groupId)
return;
setCollapsed(event.collapsed);
});
return (
<div className={cssStyle.row + " " + cssStyle.group + " " + (props.isOdd ? "" : cssStyle.even)}
style={{paddingLeft: props.group.depth + "em", top: props.offsetTop}} onContextMenu={e => {
e.preventDefault();
let entries: ContextMenuEntry[] = [];
entries.push({
type: "normal",
label: tr("Add permissions to this group"),
click: () => events.fire("action_add_permission_group", {
groupId: props.group.groupId,
mode: "value"
})
});
entries.push({
type: "normal",
label: tr("Remove permissions from this group"),
click: () => events.fire("action_remove_permission_group", {
groupId: props.group.groupId,
mode: "value"
})
});
entries.push({
type: "normal",
label: tr("Add granted permissions to this group"),
click: () => events.fire("action_add_permission_group", {
groupId: props.group.groupId,
mode: "grant"
})
});
entries.push({
type: "normal",
label: tr("Remove granted permissions from this group"),
click: () => events.fire("action_remove_permission_group", {
groupId: props.group.groupId,
mode: "grant"
})
});
entries.push({ type: "separator" });
if (collapsed) {
entries.push({
type: "normal",
label: tr("Expend group"),
click: () => events.fire("action_toggle_group", {
groupId: props.group.groupId,
collapsed: false
})
});
} else {
entries.push({
type: "normal",
label: tr("Collapse group"),
click: () => events.fire("action_toggle_group", {
groupId: props.group.groupId,
collapsed: true
})
});
}
entries.push({
type: "normal",
label: tr("Expend all"),
click: () => events.fire("action_toggle_group", {groupId: null, collapsed: false})
});
entries.push({
type: "normal",
label: tr("Collapse all"),
click: () => events.fire("action_toggle_group", {groupId: null, collapsed: true})
});
spawnContextMenu({ pageX: e.pageX, pageY: e.pageY }, entries);
}}
onDoubleClick={() => events.fire("action_toggle_group", {
collapsed: !collapsed,
groupId: props.group.groupId
})}
>
<div className={cssStyle.columnName}>
<Arrow
className={cssStyle.arrow}
direction={collapsed ? "right" : "down"}
onClick={() => events.fire("action_toggle_group", {
collapsed: !collapsed,
groupId: props.group.groupId
})}
/>
<div className={cssStyle.groupName} title={/* @tr-ignore */ tr(props.group.groupName)}>
<Translatable trIgnore={true}>{props.group.groupName}</Translatable>
</div>
</div>
<div className={cssStyle.columnValue}/>
<div className={cssStyle.columnSkip}/>
<div className={cssStyle.columnNegate}/>
<div className={cssStyle.columnGranted}/>
</div>
);
});
type PermissionValue = { value?: number, flagNegate?: boolean, flagSkip?: boolean, granted?: number };
@ReactEventHandler<PermissionList>(e => e.props.events)
class PermissionList extends React.Component<{ events: Registry<PermissionEditorEvents> }, { state: "loading" | "normal" | "error", viewHeight: number, scrollOffset: number, error?: string }> {
private readonly refContainer = React.createRef<HTMLDivElement>();
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 (
<div className={cssStyle.body} ref={this.refContainer}
onScroll={() => this.state.state === "normal" && this.setState({scrollOffset: this.refContainer.current.scrollTop})}
onContextMenu={e => {
if (e.isDefaultPrevented())
return;
e.preventDefault();
spawnContextMenu({ pageX: e.pageX, pageY: e.pageY }, [
{
type: "normal",
label: tr("Expend all"),
click: () => this.props.events.fire("action_toggle_group", {
groupId: null,
collapsed: false
})
}, {
type: "normal",
label: tr("Collapse all"),
click: () => this.props.events.fire("action_toggle_group", {groupId: null, collapsed: true})
}
]);
}}>
{elements}
<div key={"space"} className={cssStyle.spaceAllocator}
style={{height: this.state.state === "normal" ? this.currentListElements.length * this.heightPerElement : 0}}/>
<div key={"loading"}
className={cssStyle.overlay + " " + (this.state.state === "loading" ? "" : cssStyle.hidden)}>
<a title={tr("loading")}><Translatable>loading</Translatable> <LoadingDots maxDots={3}/></a>
</div>
<div key={"error"}
className={cssStyle.overlay + " " + cssStyle.error + " " + (this.state.state === "error" ? "" : cssStyle.hidden)}>
<a title={tr("An error happened")}>{this.state.error}</a>
</div>
</div>
)
}
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)
logWarn(LogCategory.PERMISSIONS, tr("Permission editor resize observer fired resize event with no entries!"));
else
logWarn(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) {
logDebug(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<PermissionEditorEvents> }>, 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<PermissionEditorEvents>("query_permission_list")
private handlePermissionListQuery() {
this.loadingPermissionList = true;
this.setState({state: "loading"});
}
@EventHandler<PermissionEditorEvents>("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<PermissionEditorEvents>("action_set_senseless_permissions")
private handleSenselessPermissions(event: PermissionEditorEvents["action_set_senseless_permissions"]) {
this.senselessPermissions = event.permissions.slice(0);
this.updateReactComponents();
}
@EventHandler<PermissionEditorEvents>("query_permission_values")
private handleRequestPermissionValues() {
this.loadingPermissionValues = true;
this.setState({state: "loading"});
}
@EventHandler<PermissionEditorEvents>("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<PermissionEditorEvents>("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<PermissionEditorEvents>("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<PermissionEditorEvents>("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) {
logWarn(LogCategory.PERMISSIONS, tr("Received group toogle for unknwon group: %s"), event.groupId);
return;
}
if (group.collapsed === event.collapsed)
return;
group.collapsed = event.collapsed;
}
this.updateReactComponents();
}
@EventHandler<PermissionEditorEvents>("action_set_filter")
private handleSetFilter(event: PermissionEditorEvents["action_set_filter"]) {
if (this.filterText === event.filter)
return;
this.filterText = event.filter;
this.updateReactComponents();
}
@EventHandler<PermissionEditorEvents>("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<PermissionEditorEvents>("action_set_default_value")
private handleSetDefaultPermissionValue(event: PermissionEditorEvents["action_set_default_value"]) {
this.defaultPermissionValue = event.value;
}
@EventHandler<PermissionEditorEvents>("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<PermissionEditorEvents>("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(<PermissionGroupRow key={"group-" + currentGroup.groupId}
group={currentGroup}
isOdd={index % 2 === 1}
offsetTop={this.heightPerElement * index}/>);
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(<PermissionEntryRow
key={"permission-" + e.name + " - " + Math.random()} /* force a update of this */
permission={e.name}
groupId={currentGroup.groupId}
isOdd={index % 2 === 1}
depth={currentGroup.depth}
offsetTop={this.heightPerElement * index}
value={this.permissionValuesByName[e.name] || {}}
defaultValue={this.defaultPermissionValue}
description={e.description}
/>);
index++;
});
currentGroup = currentGroup.nextGroup;
}
this.forceUpdate();
}
}
const PermissionTable = React.memo(() => {
const events = useContext(EventContext);
const [mode, setMode] = useState<PermissionEditorMode>("unset");
const [failedPermission, setFailedPermission] = useState(undefined);
events.reactUse("action_set_mode", event => {
setMode(event.mode);
setFailedPermission(event.failedPermission);
});
return (
<div className={cssStyle.permissionTable}>
<div className={cssStyle.header}>
<div className={cssStyle.row + " " + cssStyle.header}>
<div className={cssStyle.columnName}>
<a title={tr("Permission Name")}><Translatable>Permission Name</Translatable></a>
</div>
<div className={cssStyle.columnValue}>
<a title={tr("Value")}><Translatable>Value</Translatable></a>
</div>
<div className={cssStyle.columnSkip}>
<a title={tr("Skip")}><Translatable>Skip</Translatable></a>
</div>
<div className={cssStyle.columnNegate}>
<a title={tr("Negate")}><Translatable>Negate</Translatable></a>
</div>
<div className={cssStyle.columnGranted}>
<a title={tr("Granted")}><Translatable>Granted</Translatable></a>
</div>
</div>
</div>
<PermissionList events={events} />
<div className={cssStyle.overlay + " " + cssStyle.unset + " " + (mode === "unset" ? "" : cssStyle.hidden)}/>
<div
className={cssStyle.overlay + " " + cssStyle.noPermissions + " " + (mode === "no-permissions" ? "" : cssStyle.hidden)}>
<a>
<Translatable>You don't have the permissions to view this permissions</Translatable><br/>
({failedPermission})
</a>
</div>
</div>
);
});
const RefreshButton = React.memo(() => {
const events = useContext(EventContext);
const [unset, setUnset] = useState(true);
const [nextTime, setNextTime] = useState(0);
const refButton = useRef<Button>();
events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal" && event.mode !== "no-permissions"));
events.reactUse("query_permission_values", () => {
setNextTime(Date.now() + 5000);
refButton.current?.setState({disabled: true});
});
useEffect(() => {
if (Date.now() >= nextTime) {
refButton.current?.setState({disabled: false});
return;
}
const id = setTimeout(() => refButton.current?.setState({disabled: false}), Math.max(0, nextTime - Date.now()));
return () => clearTimeout(id);
});
return <Button
ref={refButton}
disabled={unset || Date.now() < nextTime}
onClick={() => events.fire("query_permission_values")}
>
<IconRenderer icon={"client-check_update"}/> <Translatable>Update</Translatable>
</Button>
});
interface PermissionEditorProperties {
handlerId: string;
serverUniqueId: string;
events: Registry<PermissionEditorEvents>;
}
interface PermissionEditorState {
state: "no-permissions" | "unset" | "normal";
}
export class EditorRenderer extends React.Component<PermissionEditorProperties, PermissionEditorState> {
render() {
return (
<EventContext.Provider value={this.props.events}>
<ServerInfoContext.Provider value={{ serverUniqueId: this.props.serverUniqueId, handlerId: this.props.handlerId }}>
<MenuBar key={"menu-bar"} />
<PermissionTable key={"table"} />
<div key={"footer"} className={cssStyle.containerFooter}>
<RefreshButton />
</div>
</ServerInfoContext.Provider>
</EventContext.Provider>
);
}
componentDidMount(): void {
this.props.events.fire("action_set_mode", { mode: "unset" });
}
}