Updated the permission editor to be popoutable

master
WolverinDEV 2021-03-23 12:03:00 +01:00
parent dd7fa648c4
commit b1c2b80ec8
19 changed files with 1152 additions and 960 deletions

View File

@ -8,7 +8,7 @@ import {openBanList} from "../ui/modal/ModalBanList";
import {formatMessage} from "../ui/frames/chat"; import {formatMessage} from "../ui/frames/chat";
import {CommandResult} from "../connection/ServerConnectionDeclaration"; import {CommandResult} from "../connection/ServerConnectionDeclaration";
import {spawnSettingsModal} from "../ui/modal/ModalSettings"; import {spawnSettingsModal} from "../ui/modal/ModalSettings";
import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor"; import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalController";
import {tr, tra} from "../i18n/localize"; import {tr, tra} from "../i18n/localize";
import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller"; import {spawnGlobalSettingsEditor} from "tc-shared/ui/modal/global-settings-editor/Controller";
import {spawnModalCssVariableEditor} from "tc-shared/ui/modal/css-editor/Controller"; import {spawnModalCssVariableEditor} from "tc-shared/ui/modal/css-editor/Controller";

View File

@ -1,8 +1,8 @@
import {ConnectionHandler} from "../ConnectionHandler"; import {ConnectionHandler} from "../ConnectionHandler";
import {Registry} from "../events"; import {Registry} from "../events";
import {VideoBroadcastType} from "tc-shared/connection/VideoConnection"; import {VideoBroadcastType} from "tc-shared/connection/VideoConnection";
import {PermissionEditorTab} from "tc-shared/ui/modal/permission/ModalDefinitions";
export type PermissionEditorTab = "groups-server" | "groups-channel" | "channel" | "client" | "client-channel";
export interface ClientGlobalControlEvents { export interface ClientGlobalControlEvents {
/* open a basic window */ /* open a basic window */
action_open_window: { action_open_window: {

View File

@ -20,7 +20,7 @@ import {spawnYesNo} from "../ui/modal/ModalYesNo";
import * as hex from "../crypto/hex"; import * as hex from "../crypto/hex";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry"; import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew"; import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew";
import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalPermissionEditor"; import {spawnPermissionEditorModal} from "../ui/modal/permission/ModalController";
import {global_client_actions} from "../events/GlobalEvents"; import {global_client_actions} from "../events/GlobalEvents";
import {ClientIcon} from "svg-sprites/client-icons"; import {ClientIcon} from "svg-sprites/client-icons";
import {VoiceClient} from "../voice/VoiceClient"; import {VoiceClient} from "../voice/VoiceClient";
@ -465,7 +465,10 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
type: contextmenu.MenuEntryType.ENTRY, type: contextmenu.MenuEntryType.ENTRY,
icon_class: "client-permission_client", icon_class: "client-permission_client",
name: tr("Client channel permissions"), name: tr("Client channel permissions"),
callback: () => spawnPermissionEditorModal(this.channelTree.client, "client-channel", { clientDatabaseId: this.properties.client_database_id }) callback: () => spawnPermissionEditorModal(this.channelTree.client, "client-channel", {
clientDatabaseId: this.properties.client_database_id,
channelId: this.currentChannel()?.channelId
})
} }
] ]
}]; }];

View File

@ -0,0 +1,102 @@
export interface EditorGroupedPermissions {
groupId: string,
groupName: string,
permissions: {
id: number,
name: string;
description: string;
}[],
children: EditorGroupedPermissions[]
}
export 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;
}[]
}
}

View File

@ -121,7 +121,7 @@ html:root {
.button { .button {
text-align: center; text-align: center;
:global(.arrow) { .arrow {
border-color: var(--text); border-color: var(--text);
} }
} }
@ -271,7 +271,7 @@ html:root {
color: var(--modal-permissions-table-entry-group-text) !important; color: var(--modal-permissions-table-entry-group-text) !important;
font-weight: bold; font-weight: bold;
:global(.arrow) { .arrow {
cursor: pointer; cursor: pointer;
border-color: var(--modal-permissions-table-entry-active-text); border-color: var(--modal-permissions-table-entry-active-text);
} }

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import {useEffect, useRef, useState} from "react"; import {useContext, useEffect, useRef, useState} from "react";
import {FlatInputField} from "tc-shared/ui/react-elements/InputField"; import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events";
@ -11,125 +11,32 @@ import ResizeObserver from "resize-observer-polyfill";
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
import {Button} from "tc-shared/ui/react-elements/Button"; import {Button} from "tc-shared/ui/react-elements/Button";
import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; import {IconRenderer, RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
import {copyToClipboard} from "tc-shared/utils/helpers"; import {copyToClipboard} from "tc-shared/utils/helpers";
import {createInfoModal} from "tc-shared/ui/elements/Modal"; import {createInfoModal} from "tc-shared/ui/elements/Modal";
import {getIconManager} from "tc-shared/file/Icons"; 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("./PermissionEditor.scss"); const cssStyle = require("./EditorRenderer.scss");
export interface EditorGroupedPermissions { const EventContext = React.createContext<Registry<PermissionEditorEvents>>(undefined);
groupId: string, const ServerInfoContext = React.createContext<{ handlerId: string, serverUniqueId: string }>(undefined);
groupName: string,
permissions: {
id: number,
name: string;
description: string;
}[],
children: EditorGroupedPermissions[]
}
type PermissionEditorMode = "unset" | "no-permissions" | "normal"; const ButtonIconPreview = React.memo(() => {
const serverInfo = useContext(ServerInfoContext);
const events = useContext(EventContext);
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<PermissionEditorEvents>, connection: ConnectionHandler }) => {
const [iconId, setIconId] = useState(0); const [iconId, setIconId] = useState(0);
const [unset, setUnset] = useState(true); const [unset, setUnset] = useState(true);
props.events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal")); events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal"));
props.events.reactUse("action_remove_permissions_result", event => { events.reactUse("action_remove_permissions_result", event => {
const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID); const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID);
if (!iconPermission || !iconPermission.success) return; if (!iconPermission || !iconPermission.success) return;
@ -137,7 +44,7 @@ const ButtonIconPreview = (props: { events: Registry<PermissionEditorEvents>, co
setIconId(0); setIconId(0);
}); });
props.events.reactUse("action_set_permissions_result", event => { events.reactUse("action_set_permissions_result", event => {
const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID); const iconPermission = event.permissions.find(e => e.name === PermissionType.I_ICON_ID);
if (!iconPermission) return; if (!iconPermission) return;
@ -145,7 +52,7 @@ const ButtonIconPreview = (props: { events: Registry<PermissionEditorEvents>, co
setIconId(iconPermission.newValue); setIconId(iconPermission.newValue);
}); });
props.events.reactUse("query_permission_values_result", event => { events.reactUse("query_permission_values_result", event => {
if (event.status !== "success") { if (event.status !== "success") {
setIconId(0); setIconId(0);
return; return;
@ -166,63 +73,73 @@ const ButtonIconPreview = (props: { events: Registry<PermissionEditorEvents>, co
let icon; let icon;
if (!unset && iconId > 0) { if (!unset && iconId > 0) {
icon = <RemoteIconRenderer key={"icon-" + iconId} icon={getIconManager().resolveIcon(iconId, props.connection.getCurrentServerUniqueId(), props.connection.handlerId)} />; icon = <RemoteIconRenderer key={"icon-" + iconId} icon={getIconManager().resolveIcon(iconId, serverInfo.serverUniqueId, serverInfo.handlerId)} />;
} }
return ( return (
<div className={cssStyle.containerIconSelect}> <div className={cssStyle.containerIconSelect}>
<div className={cssStyle.preview} <div className={cssStyle.preview}
onClick={() => props.events.fire("action_open_icon_select", {iconId: iconId})}> onClick={() => events.fire("action_open_icon_select", {iconId: iconId})}>
{icon} {icon}
</div> </div>
<div className={cssStyle.containerDropdown}> <div className={cssStyle.containerDropdown}>
<div className={cssStyle.button}> <div className={cssStyle.button}>
<div className="arrow down"/> <Arrow direction={"down"} className={cssStyle.arrow} />
</div> </div>
<div className={cssStyle.dropdown}> <div className={cssStyle.dropdown}>
{iconId ? <div className={cssStyle.entry} key={"edit-icon"} {iconId ? (
onClick={() => props.events.fire("action_open_icon_select", {iconId: iconId})}> <div className={cssStyle.entry} key={"edit-icon"}
onClick={() => events.fire("action_open_icon_select", {iconId: iconId})}>
<Translatable>Edit icon</Translatable> <Translatable>Edit icon</Translatable>
</div> : undefined} </div>
{iconId ? <div className={cssStyle.entry} key={"remove-icon"} ) : undefined}
onClick={() => props.events.fire("action_remove_permissions", { {iconId ? (
<div className={cssStyle.entry} key={"remove-icon"}
onClick={() => events.fire("action_remove_permissions", {
permissions: [{ permissions: [{
name: PermissionType.I_ICON_ID, name: PermissionType.I_ICON_ID,
mode: "value" mode: "value"
}] }]
})}> })}>
<Translatable>Remove icon</Translatable> <Translatable>Remove icon</Translatable>
</div> : undefined} </div>
{!iconId ? <div className={cssStyle.entry} key={"add-icon"} ) : undefined}
onClick={() => props.events.fire("action_open_icon_select", {iconId: 0})}> {!iconId ? (
<div className={cssStyle.entry} key={"add-icon"}
onClick={() => events.fire("action_open_icon_select", {iconId: 0})}>
<Translatable>Add icon</Translatable> <Translatable>Add icon</Translatable>
</div> : undefined} </div>
) : undefined}
</div> </div>
</div> </div>
</div> </div>
); );
}; });
const ClientListButton = (props: { events: Registry<PermissionEditorEvents> }) => { const ClientListButton = () => {
const events = useContext(EventContext);
const [visible, setVisible] = useState(true); const [visible, setVisible] = useState(true);
const [toggled, setToggled] = useState(false); const [toggled, setToggled] = useState(false);
props.events.reactUse("action_toggle_client_button", event => setVisible(event.visible)); events.reactUse("action_toggle_client_button", event => setVisible(event.visible));
props.events.reactUse("action_toggle_client_list", event => setToggled(event.visible)); events.reactUse("action_toggle_client_list", event => setToggled(event.visible));
return <Button return <Button
key={"button-clients"} key={"button-clients"}
className={cssStyle.clients + " " + (visible ? "" : cssStyle.hidden)} className={cssStyle.clients + " " + (visible ? "" : cssStyle.hidden)}
color={"green"} color={"green"}
onClick={() => props.events.fire("action_toggle_client_list", {visible: !toggled})}> onClick={() => events.fire("action_toggle_client_list", {visible: !toggled})}>
{toggled ? <Translatable key={"hide"}>Hide clients in group</Translatable> : {toggled ? <Translatable key={"hide"}>Hide clients in group</Translatable> :
<Translatable key={"show"}>Show clients in group</Translatable>} <Translatable key={"show"}>Show clients in group</Translatable>}
</Button> </Button>
}; };
const MenuBar = (props: { events: Registry<PermissionEditorEvents>, connection: ConnectionHandler }) => { const MenuBar = React.memo(() => {
return <div className={cssStyle.containerMenuBar}> const events = useContext(EventContext);
<ClientListButton events={props.events}/>
return (
<div className={cssStyle.containerMenuBar}>
<ClientListButton />
<FlatInputField <FlatInputField
className={cssStyle.filter} className={cssStyle.filter}
@ -230,16 +147,20 @@ const MenuBar = (props: { events: Registry<PermissionEditorEvents>, connection:
labelType={"floating"} labelType={"floating"}
labelClassName={cssStyle.label} labelClassName={cssStyle.label}
labelFloatingClassName={cssStyle.labelFloating} labelFloatingClassName={cssStyle.labelFloating}
onInput={text => props.events.fire("action_set_filter", {filter: text})} onInput={text => events.fire("action_set_filter", {filter: text})}
/> />
<div className={cssStyle.options}> <div className={cssStyle.options}>
<Switch initialState={false} label={<Translatable>Assigned only</Translatable>} <Switch
onChange={state => props.events.fire("action_set_assigned_only", {value: state})}/> 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>} /> */} { /* <Switch initialState={true} label={<Translatable>Editable only</Translatable>} /> */}
</div> </div>
<ButtonIconPreview events={props.events} connection={props.connection}/> <ButtonIconPreview />
</div>; </div>
}; );
});
interface LinkedGroupedPermissions { interface LinkedGroupedPermissions {
groupId: string; groupId: string;
@ -266,8 +187,7 @@ interface LinkedGroupedPermissions {
elementVisible: boolean; elementVisible: boolean;
} }
const PermissionEntryRow = (props: { const PermissionEntryRow = React.memo((props: {
events: Registry<PermissionEditorEvents>,
groupId: string, groupId: string,
permission: string, permission: string,
value: PermissionValue, value: PermissionValue,
@ -277,6 +197,8 @@ const PermissionEntryRow = (props: {
defaultValue: number, defaultValue: number,
description: string description: string
}) => { }) => {
const events = useContext(EventContext);
const [defaultValue, setDefaultValue] = useState(props.defaultValue); const [defaultValue, setDefaultValue] = useState(props.defaultValue);
const [value, setValue] = useState<number>(props.value.value); const [value, setValue] = useState<number>(props.value.value);
const [forceValueUpdate, setForceValueUpdate] = useState(false); const [forceValueUpdate, setForceValueUpdate] = useState(false);
@ -306,7 +228,7 @@ const PermissionEntryRow = (props: {
if (isBoolPermission) { if (isBoolPermission) {
valueElement = <Switch ref={refValueB} key={"value-b"} initialState={value >= 1} disabled={valueApplying} valueElement = <Switch ref={refValueB} key={"value-b"} initialState={value >= 1} disabled={valueApplying}
onChange={flag => { onChange={flag => {
props.events.fire("action_set_permissions", { events.fire("action_set_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value", mode: "value",
@ -337,7 +259,7 @@ const PermissionEntryRow = (props: {
} }
setForceValueUpdate(false); setForceValueUpdate(false);
props.events.fire("action_remove_permissions", { events.fire("action_remove_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value" mode: "value"
@ -352,7 +274,7 @@ const PermissionEntryRow = (props: {
} }
setForceValueUpdate(false); setForceValueUpdate(false);
props.events.fire("action_set_permissions", { events.fire("action_set_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value", mode: "value",
@ -367,7 +289,7 @@ const PermissionEntryRow = (props: {
} }
skipElement = <Switch key={"skip"} initialState={flagSkip} disabled={valueApplying} onChange={flag => { skipElement = <Switch key={"skip"} initialState={flagSkip} disabled={valueApplying} onChange={flag => {
props.events.fire("action_set_permissions", { events.fire("action_set_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value", mode: "value",
@ -378,7 +300,7 @@ const PermissionEntryRow = (props: {
}); });
}}/>; }}/>;
negateElement = <Switch key={"negate"} initialState={flagNegated} disabled={valueApplying} onChange={flag => { negateElement = <Switch key={"negate"} initialState={flagNegated} disabled={valueApplying} onChange={flag => {
props.events.fire("action_set_permissions", { events.fire("action_set_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value", mode: "value",
@ -392,12 +314,24 @@ const PermissionEntryRow = (props: {
if (typeof granted === "number") { if (typeof granted === "number") {
if (grantedApplying) { if (grantedApplying) {
grantedElement = grantedElement = (
<input key={"grant-applying"} className={cssStyle.applying} type="number" placeholder={tr("applying")} <input
readOnly={true} onChange={() => { key={"grant-applying"}
}}/>; className={cssStyle.applying}
type="number"
placeholder={tr("applying")}
readOnly={true}
onChange={() => {}}
/>
);
} else { } else {
grantedElement = <input ref={refGranted} key={"grant"} type="number" defaultValue={granted} onBlur={() => { grantedElement = (
<input
ref={refGranted}
key={"grant"}
type="number"
defaultValue={granted}
onBlur={() => {
setGrantedEditing(false); setGrantedEditing(false);
if (!refGranted.current) if (!refGranted.current)
return; return;
@ -408,7 +342,7 @@ const PermissionEntryRow = (props: {
return; return;
setForceGrantedUpdate(true); setForceGrantedUpdate(true);
props.events.fire("action_remove_permissions", { events.fire("action_remove_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "grant" mode: "grant"
@ -423,7 +357,7 @@ const PermissionEntryRow = (props: {
} }
setForceGrantedUpdate(true); setForceGrantedUpdate(true);
props.events.fire("action_set_permissions", { events.fire("action_set_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "grant", mode: "grant",
@ -431,12 +365,16 @@ const PermissionEntryRow = (props: {
}] }]
}); });
} }
}} onChange={() => { }}
}} onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}/>; onChange={() => {
}}
onKeyPress={e => e.key === "Enter" && e.currentTarget.blur()}
/>
);
} }
} }
props.events.reactUse("action_start_permission_edit", event => { events.reactUse("action_start_permission_edit", event => {
if (event.permission !== props.permission) if (event.permission !== props.permission)
return; return;
@ -447,7 +385,7 @@ const PermissionEntryRow = (props: {
} else { } else {
if (isBoolPermission && typeof value === "undefined") { if (isBoolPermission && typeof value === "undefined") {
setValue(event.defaultValue >= 1 ? 1 : 0); setValue(event.defaultValue >= 1 ? 1 : 0);
props.events.fire("action_set_permissions", { events.fire("action_set_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value", mode: "value",
@ -464,7 +402,7 @@ const PermissionEntryRow = (props: {
} }
}); });
props.events.reactUse("action_set_permissions", event => { events.reactUse("action_set_permissions", event => {
const values = event.permissions.find(e => e.name === props.permission); const values = event.permissions.find(e => e.name === props.permission);
if (!values) return; if (!values) return;
@ -477,7 +415,7 @@ const PermissionEntryRow = (props: {
} }
}); });
props.events.reactUse("action_set_permissions_result", event => { events.reactUse("action_set_permissions_result", event => {
const result = event.permissions.find(e => e.name === props.permission); const result = event.permissions.find(e => e.name === props.permission);
if (!result) return; if (!result) return;
@ -518,7 +456,7 @@ const PermissionEntryRow = (props: {
} }
}); });
props.events.reactUse("action_remove_permissions", event => { events.reactUse("action_remove_permissions", event => {
const modes = event.permissions.find(e => e.name === props.permission); const modes = event.permissions.find(e => e.name === props.permission);
if (!modes) return; if (!modes) return;
@ -533,7 +471,7 @@ const PermissionEntryRow = (props: {
setGrantedApplying(true); setGrantedApplying(true);
}); });
props.events.reactUse("action_remove_permissions_result", event => { events.reactUse("action_remove_permissions_result", event => {
const modes = event.permissions.find(e => e.name === props.permission); const modes = event.permissions.find(e => e.name === props.permission);
if (!modes) return; if (!modes) return;
@ -553,7 +491,7 @@ const PermissionEntryRow = (props: {
} }
}); });
props.events.reactUse("action_set_default_value", event => setDefaultValue(event.value)); events.reactUse("action_set_default_value", event => setDefaultValue(event.value));
useEffect(() => { useEffect(() => {
if (grantedEditing) if (grantedEditing)
@ -573,7 +511,7 @@ const PermissionEntryRow = (props: {
if (e.isDefaultPrevented()) if (e.isDefaultPrevented())
return; return;
props.events.fire("action_start_permission_edit", { events.fire("action_start_permission_edit", {
permission: props.permission, permission: props.permission,
target: "value", target: "value",
defaultValue: defaultValue defaultValue: defaultValue
@ -583,12 +521,12 @@ const PermissionEntryRow = (props: {
onContextMenu={e => { onContextMenu={e => {
e.preventDefault(); e.preventDefault();
let entries: contextmenu.MenuEntry[] = []; let entries: ContextMenuEntry[] = [];
if (typeof value === "undefined") { if (typeof value === "undefined") {
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Add permission"), label: tr("Add permission"),
callback: () => props.events.fire("action_start_permission_edit", { click: () => events.fire("action_start_permission_edit", {
permission: props.permission, permission: props.permission,
target: "value", target: "value",
defaultValue: defaultValue defaultValue: defaultValue
@ -596,9 +534,9 @@ const PermissionEntryRow = (props: {
}); });
} else { } else {
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Remove permission"), label: tr("Remove permission"),
callback: () => props.events.fire("action_remove_permissions", { click: () => events.fire("action_remove_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "value" mode: "value"
@ -609,9 +547,9 @@ const PermissionEntryRow = (props: {
if (typeof granted === "undefined") { if (typeof granted === "undefined") {
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Add grant permission"), label: tr("Add grant permission"),
callback: () => props.events.fire("action_start_permission_edit", { click: () => events.fire("action_start_permission_edit", {
permission: props.permission, permission: props.permission,
target: "grant", target: "grant",
defaultValue: defaultValue defaultValue: defaultValue
@ -619,9 +557,9 @@ const PermissionEntryRow = (props: {
}); });
} else { } else {
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Remove grant permission"), label: tr("Remove grant permission"),
callback: () => props.events.fire("action_remove_permissions", { click: () => events.fire("action_remove_permissions", {
permissions: [{ permissions: [{
name: props.permission, name: props.permission,
mode: "grant" mode: "grant"
@ -629,28 +567,27 @@ const PermissionEntryRow = (props: {
}) })
}); });
} }
entries.push({ type: "separator" });
entries.push(contextmenu.Entry.HR());
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Collapse group"), label: tr("Collapse group"),
callback: () => props.events.fire("action_toggle_group", {groupId: props.groupId, collapsed: true}) click: () => events.fire("action_toggle_group", {groupId: props.groupId, collapsed: true})
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Expend all"), label: tr("Expend all"),
callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: false}) click: () => events.fire("action_toggle_group", {groupId: null, collapsed: false})
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Collapse all"), label: tr("Collapse all"),
callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: true}) click: () => events.fire("action_toggle_group", {groupId: null, collapsed: true})
}); });
entries.push(contextmenu.Entry.HR()); entries.push({ type: "separator" });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Show permission description"), label: tr("Show permission description"),
callback: () => { click: () => {
createInfoModal( createInfoModal(
tr("Permission description"), tr("Permission description"),
tr("Permission description for permission ") + props.permission + ": <br>" + props.description tr("Permission description for permission ") + props.permission + ": <br>" + props.description
@ -658,12 +595,12 @@ const PermissionEntryRow = (props: {
} }
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Copy permission name"), label: tr("Copy permission name"),
callback: () => copyToClipboard(props.permission) click: () => copyToClipboard(props.permission)
}); });
contextmenu.spawn_context_menu(e.pageX, e.pageY, ...entries); spawnContextMenu({ pageX: e.pageX, pageY: e.pageY }, entries);
}} }}
> >
<div className={cssStyle.columnName}> <div className={cssStyle.columnName}>
@ -673,7 +610,7 @@ const PermissionEntryRow = (props: {
<div className={cssStyle.columnSkip}>{skipElement}</div> <div className={cssStyle.columnSkip}>{skipElement}</div>
<div className={cssStyle.columnNegate}>{negateElement}</div> <div className={cssStyle.columnNegate}>{negateElement}</div>
<div className={cssStyle.columnGranted} onDoubleClick={e => { <div className={cssStyle.columnGranted} onDoubleClick={e => {
props.events.fire("action_start_permission_edit", { events.fire("action_start_permission_edit", {
permission: props.permission, permission: props.permission,
target: "grant", target: "grant",
defaultValue: defaultValue defaultValue: defaultValue
@ -682,12 +619,13 @@ const PermissionEntryRow = (props: {
}}>{grantedElement}</div> }}>{grantedElement}</div>
</div> </div>
); );
}; });
const PermissionGroupRow = (props: { events: Registry<PermissionEditorEvents>, group: LinkedGroupedPermissions, isOdd: boolean, offsetTop: number }) => { const PermissionGroupRow = React.memo((props: { group: LinkedGroupedPermissions, isOdd: boolean, offsetTop: number }) => {
const events = useContext(EventContext);
const [collapsed, setCollapsed] = useState(props.group.collapsed); const [collapsed, setCollapsed] = useState(props.group.collapsed);
props.events.reactUse("action_toggle_group", event => { events.reactUse("action_toggle_group", event => {
if (event.groupId !== null && event.groupId !== props.group.groupId) if (event.groupId !== null && event.groupId !== props.group.groupId)
return; return;
@ -699,82 +637,86 @@ const PermissionGroupRow = (props: { events: Registry<PermissionEditorEvents>, g
style={{paddingLeft: props.group.depth + "em", top: props.offsetTop}} onContextMenu={e => { style={{paddingLeft: props.group.depth + "em", top: props.offsetTop}} onContextMenu={e => {
e.preventDefault(); e.preventDefault();
let entries = []; let entries: ContextMenuEntry[] = [];
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Add permissions to this group"), label: tr("Add permissions to this group"),
callback: () => props.events.fire("action_add_permission_group", { click: () => events.fire("action_add_permission_group", {
groupId: props.group.groupId, groupId: props.group.groupId,
mode: "value" mode: "value"
}) })
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Remove permissions from this group"), label: tr("Remove permissions from this group"),
callback: () => props.events.fire("action_remove_permission_group", { click: () => events.fire("action_remove_permission_group", {
groupId: props.group.groupId, groupId: props.group.groupId,
mode: "value" mode: "value"
}) })
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Add granted permissions to this group"), label: tr("Add granted permissions to this group"),
callback: () => props.events.fire("action_add_permission_group", { click: () => events.fire("action_add_permission_group", {
groupId: props.group.groupId, groupId: props.group.groupId,
mode: "grant" mode: "grant"
}) })
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Remove granted permissions from this group"), label: tr("Remove granted permissions from this group"),
callback: () => props.events.fire("action_remove_permission_group", { click: () => events.fire("action_remove_permission_group", {
groupId: props.group.groupId, groupId: props.group.groupId,
mode: "grant" mode: "grant"
}) })
}); });
entries.push(contextmenu.Entry.HR()); entries.push({ type: "separator" });
if (collapsed) { if (collapsed) {
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Expend group"), label: tr("Expend group"),
callback: () => props.events.fire("action_toggle_group", { click: () => events.fire("action_toggle_group", {
groupId: props.group.groupId, groupId: props.group.groupId,
collapsed: false collapsed: false
}) })
}); });
} else { } else {
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Collapse group"), label: tr("Collapse group"),
callback: () => props.events.fire("action_toggle_group", { click: () => events.fire("action_toggle_group", {
groupId: props.group.groupId, groupId: props.group.groupId,
collapsed: true collapsed: true
}) })
}); });
} }
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Expend all"), label: tr("Expend all"),
callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: false}) click: () => events.fire("action_toggle_group", {groupId: null, collapsed: false})
}); });
entries.push({ entries.push({
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Collapse all"), label: tr("Collapse all"),
callback: () => props.events.fire("action_toggle_group", {groupId: null, collapsed: true}) click: () => events.fire("action_toggle_group", {groupId: null, collapsed: true})
}); });
contextmenu.spawn_context_menu(e.pageX, e.pageY, ...entries);
spawnContextMenu({ pageX: e.pageX, pageY: e.pageY }, entries);
}} }}
onDoubleClick={() => props.events.fire("action_toggle_group", { onDoubleClick={() => events.fire("action_toggle_group", {
collapsed: !collapsed, collapsed: !collapsed,
groupId: props.group.groupId groupId: props.group.groupId
})} })}
> >
<div className={cssStyle.columnName}> <div className={cssStyle.columnName}>
<div className={"arrow " + (collapsed ? "right" : "down")} <Arrow
onClick={() => props.events.fire("action_toggle_group", { className={cssStyle.arrow}
direction={collapsed ? "right" : "down"}
onClick={() => events.fire("action_toggle_group", {
collapsed: !collapsed, collapsed: !collapsed,
groupId: props.group.groupId groupId: props.group.groupId
})}/> })}
/>
<div className={cssStyle.groupName} title={/* @tr-ignore */ tr(props.group.groupName)}> <div className={cssStyle.groupName} title={/* @tr-ignore */ tr(props.group.groupName)}>
<Translatable trIgnore={true}>{props.group.groupName}</Translatable> <Translatable trIgnore={true}>{props.group.groupName}</Translatable>
</div> </div>
@ -785,7 +727,7 @@ const PermissionGroupRow = (props: { events: Registry<PermissionEditorEvents>, g
<div className={cssStyle.columnGranted}/> <div className={cssStyle.columnGranted}/>
</div> </div>
); );
}; });
type PermissionValue = { value?: number, flagNegate?: boolean, flagSkip?: boolean, granted?: number }; type PermissionValue = { value?: number, flagNegate?: boolean, flagSkip?: boolean, granted?: number };
@ -835,18 +777,20 @@ class PermissionList extends React.Component<{ events: Registry<PermissionEditor
return; return;
e.preventDefault(); e.preventDefault();
contextmenu.spawn_context_menu(e.pageX, e.pageY, { spawnContextMenu({ pageX: e.pageX, pageY: e.pageY }, [
type: contextmenu.MenuEntryType.ENTRY, {
name: tr("Expend all"), type: "normal",
callback: () => this.props.events.fire("action_toggle_group", { label: tr("Expend all"),
click: () => this.props.events.fire("action_toggle_group", {
groupId: null, groupId: null,
collapsed: false collapsed: false
}) })
}, { }, {
type: contextmenu.MenuEntryType.ENTRY, type: "normal",
name: tr("Collapse all"), label: tr("Collapse all"),
callback: () => this.props.events.fire("action_toggle_group", {groupId: null, collapsed: true}) click: () => this.props.events.fire("action_toggle_group", {groupId: null, collapsed: true})
}); }
]);
}}> }}>
{elements} {elements}
<div key={"space"} className={cssStyle.spaceAllocator} <div key={"space"} className={cssStyle.spaceAllocator}
@ -1225,7 +1169,7 @@ class PermissionList extends React.Component<{ events: Registry<PermissionEditor
while (currentGroup) { while (currentGroup) {
if (currentGroup.elementVisible) { if (currentGroup.elementVisible) {
this.currentListElements.push(<PermissionGroupRow key={"group-" + currentGroup.groupId} this.currentListElements.push(<PermissionGroupRow key={"group-" + currentGroup.groupId}
events={this.props.events} group={currentGroup} group={currentGroup}
isOdd={index % 2 === 1} isOdd={index % 2 === 1}
offsetTop={this.heightPerElement * index}/>); offsetTop={this.heightPerElement * index}/>);
index++; index++;
@ -1245,7 +1189,6 @@ class PermissionList extends React.Component<{ events: Registry<PermissionEditor
this.currentListElements.push(<PermissionEntryRow this.currentListElements.push(<PermissionEntryRow
key={"permission-" + e.name + " - " + Math.random()} /* force a update of this */ key={"permission-" + e.name + " - " + Math.random()} /* force a update of this */
events={this.props.events}
permission={e.name} permission={e.name}
groupId={currentGroup.groupId} groupId={currentGroup.groupId}
isOdd={index % 2 === 1} isOdd={index % 2 === 1}
@ -1265,11 +1208,12 @@ class PermissionList extends React.Component<{ events: Registry<PermissionEditor
} }
} }
const PermissionTable = (props: { events: Registry<PermissionEditorEvents> }) => { const PermissionTable = React.memo(() => {
const events = useContext(EventContext);
const [mode, setMode] = useState<PermissionEditorMode>("unset"); const [mode, setMode] = useState<PermissionEditorMode>("unset");
const [failedPermission, setFailedPermission] = useState(undefined); const [failedPermission, setFailedPermission] = useState(undefined);
props.events.reactUse("action_set_mode", event => { events.reactUse("action_set_mode", event => {
setMode(event.mode); setMode(event.mode);
setFailedPermission(event.failedPermission); setFailedPermission(event.failedPermission);
}); });
@ -1295,25 +1239,28 @@ const PermissionTable = (props: { events: Registry<PermissionEditorEvents> }) =>
</div> </div>
</div> </div>
</div> </div>
<PermissionList events={props.events}/> <PermissionList events={events} />
<div className={cssStyle.overlay + " " + cssStyle.unset + " " + (mode === "unset" ? "" : cssStyle.hidden)}/> <div className={cssStyle.overlay + " " + cssStyle.unset + " " + (mode === "unset" ? "" : cssStyle.hidden)}/>
<div <div
className={cssStyle.overlay + " " + cssStyle.noPermissions + " " + (mode === "no-permissions" ? "" : cssStyle.hidden)}> className={cssStyle.overlay + " " + cssStyle.noPermissions + " " + (mode === "no-permissions" ? "" : cssStyle.hidden)}>
<a><Translatable>You don't have the permissions to view this <a>
permissions</Translatable><br/>({failedPermission})</a> <Translatable>You don't have the permissions to view this permissions</Translatable><br/>
({failedPermission})
</a>
</div> </div>
</div> </div>
); );
}; });
const RefreshButton = (props: { events: Registry<PermissionEditorEvents> }) => { const RefreshButton = React.memo(() => {
const events = useContext(EventContext);
const [unset, setUnset] = useState(true); const [unset, setUnset] = useState(true);
const [nextTime, setNextTime] = useState(0); const [nextTime, setNextTime] = useState(0);
const refButton = useRef<Button>(); const refButton = useRef<Button>();
props.events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal" && event.mode !== "no-permissions")); events.reactUse("action_set_mode", event => setUnset(event.mode !== "normal" && event.mode !== "no-permissions"));
props.events.reactUse("query_permission_values", () => { events.reactUse("query_permission_values", () => {
setNextTime(Date.now() + 5000); setNextTime(Date.now() + 5000);
refButton.current?.setState({disabled: true}); refButton.current?.setState({disabled: true});
}); });
@ -1331,14 +1278,15 @@ const RefreshButton = (props: { events: Registry<PermissionEditorEvents> }) => {
return <Button return <Button
ref={refButton} ref={refButton}
disabled={unset || Date.now() < nextTime} disabled={unset || Date.now() < nextTime}
onClick={() => props.events.fire("query_permission_values")} onClick={() => events.fire("query_permission_values")}
> >
<IconRenderer icon={"client-check_update"}/> <Translatable>Update</Translatable> <IconRenderer icon={"client-check_update"}/> <Translatable>Update</Translatable>
</Button> </Button>
}; });
interface PermissionEditorProperties { interface PermissionEditorProperties {
connection: ConnectionHandler; handlerId: string;
serverUniqueId: string;
events: Registry<PermissionEditorEvents>; events: Registry<PermissionEditorEvents>;
} }
@ -1346,18 +1294,22 @@ interface PermissionEditorState {
state: "no-permissions" | "unset" | "normal"; state: "no-permissions" | "unset" | "normal";
} }
export class PermissionEditor extends React.Component<PermissionEditorProperties, PermissionEditorState> { export class EditorRenderer extends React.Component<PermissionEditorProperties, PermissionEditorState> {
render() { render() {
return [ return (
<MenuBar key={"menu-bar"} events={this.props.events} connection={this.props.connection}/>, <EventContext.Provider value={this.props.events}>
<PermissionTable key={"table"} events={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}> <div key={"footer"} className={cssStyle.containerFooter}>
<RefreshButton events={this.props.events}/> <RefreshButton />
</div> </div>
]; </ServerInfoContext.Provider>
</EventContext.Provider>
);
} }
componentDidMount(): void { componentDidMount(): void {
this.props.events.fire("action_set_mode", {mode: "unset"}); this.props.events.fire("action_set_mode", { mode: "unset" });
} }
} }

View File

@ -1,16 +1,7 @@
import {spawnReactModal} from "tc-shared/ui/react-elements/modal"; import {spawnModal} from "tc-shared/ui/react-elements/modal";
import {ConnectionHandler} from "tc-shared/ConnectionHandler"; import {ConnectionHandler, ConnectionState} from "tc-shared/ConnectionHandler";
import * as React from "react";
import {useState} from "react";
import {ContextDivider} from "tc-shared/ui/react-elements/ContextDivider";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {Registry} from "tc-shared/events"; import {Registry} from "tc-shared/events";
import { import {DefaultTabValues} from "tc-shared/ui/modal/permission/ModalRenderer";
EditorGroupedPermissions,
PermissionEditor,
PermissionEditorEvents
} from "tc-shared/ui/modal/permission/PermissionEditor";
import {SideBar} from "tc-shared/ui/modal/permission/TabHandler";
import {Group, GroupTarget, GroupType} from "tc-shared/permission/GroupManager"; import {Group, GroupTarget, GroupType} from "tc-shared/permission/GroupManager";
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal"; import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {ClientNameInfo, CommandResult} from "tc-shared/connection/ServerConnectionDeclaration"; import {ClientNameInfo, CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
@ -31,325 +22,65 @@ import {
import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate"; import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroupCreate";
import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy"; import {spawnModalGroupPermissionCopy} from "tc-shared/ui/modal/ModalGroupPermissionCopy";
import {ErrorCode} from "tc-shared/connection/ErrorCode"; import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {PermissionEditorTab} from "tc-shared/events/GlobalEvents";
import {LogCategory, logError, logWarn} from "tc-shared/log"; import {LogCategory, logError, logWarn} from "tc-shared/log";
import {useTr} from "tc-shared/ui/react-elements/Helper";
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
const cssStyle = require("./ModalPermissionEditor.scss"); import {
GroupProperties,
GroupUpdateEntry,
PermissionEditorSubject,
PermissionEditorTab,
PermissionModalEvents
} from "tc-shared/ui/modal/permission/ModalDefinitions";
import {EditorGroupedPermissions, PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
export type PermissionEditorSubject = export function spawnPermissionEditorModal(connection: ConnectionHandler, defaultTab: PermissionEditorTab = "groups-server", defaultTabValues?: DefaultTabValues) {
"groups-server" const modalEvents = new Registry<PermissionModalEvents>();
| "groups-channel" const editorEvents = new Registry<PermissionEditorEvents>();
| "channel"
| "client"
| "client-channel"
| "none";
export const PermissionTabName: { [T in PermissionEditorTab]: { name: string, useTranslate: () => string, renderTranslate: () => React.ReactNode } } = {
"groups-server": {name: "Server Groups", useTranslate: () => useTr("Server Groups"), renderTranslate: () => <Translatable>Server Groups</Translatable>},
"groups-channel": {name: "Channel Groups", useTranslate: () => useTr("Channel Groups"), renderTranslate: () => <Translatable>Channel Groups</Translatable>},
"channel": {name: "Channel Permissions", useTranslate: () => useTr("Channel Permissions"), renderTranslate: () => <Translatable>Channel Permissions</Translatable>},
"client": {name: "Client Permissions", useTranslate: () => useTr("Client Permissions"), renderTranslate: () => <Translatable>Client Permissions</Translatable>},
"client-channel": {name: "Client Channel Permissions", useTranslate: () => useTr("Client Channel Permissions"), renderTranslate: () => <Translatable>Client Channel Permissions</Translatable>},
};
export type GroupProperties = { modalEvents.enableDebug("modal-permissions");
id: number, editorEvents.enableDebug("permissions-editor");
type: "query" | "template" | "normal";
name: string, initializePermissionModalResultHandlers(modalEvents);
iconId: number, initializePermissionModalController(connection, modalEvents);
initializePermissionEditor(connection, modalEvents, editorEvents);
sortId: number; modalEvents.on("action_activate_tab", event => editorEvents.fire("action_toggle_client_button", { visible: event.tab === "groups-server" }));
saveDB: boolean; editorEvents.on("action_toggle_client_list", event => modalEvents.fire("notify_client_list_toggled", { visible: event.visible }));
needed_modify_power: number; modalEvents.on("notify_initial_rendered", () => {
needed_member_add: number; modalEvents.fire_react("action_activate_tab", {
needed_member_remove: number; tab: defaultTab,
}; activeChannelId: defaultTabValues?.channelId,
export type GroupUpdateEntry = { activeGroupId: defaultTabValues?.groupId,
property: "name" | "icon" | "sort" | "save"; activeClientDatabaseId: defaultTabValues?.clientDatabaseId
value: any
};
export type ChannelInfo = {
id: number;
iconId: number;
name: string;
depth: number;
}
export interface PermissionModalEvents {
action_activate_tab: {
tab: PermissionEditorTab,
activeGroupId?: number;
activeChannelId?: number;
activeClientDatabaseId?: number;
},
action_select_group: {
target: "server" | "channel",
id: number
},
action_select_channel: {
target: "channel" | "client-channel";
id: number
},
action_select_client: {
target: "client" | "client-channel";
id: number | string | undefined;
}
action_set_permission_editor_subject: {
mode: PermissionEditorSubject | undefined;
groupId?: number;
channelId?: number;
clientDatabaseId?: number;
}
action_create_group: { target: "server" | "channel", sourceGroup?: number },
action_rename_group: { target: "server" | "channel", id: number | "selected", newName: string },
action_rename_group_result: {
target: "server" | "channel";
id: number;
status: "success" | "error";
error?: string;
}
action_delete_group: { target: "server" | "channel", id: number | "selected", mode: "ask" | "force" },
action_delete_group_result: {
target: "server" | "channel";
id: number;
status: "success" | "error";
error?: string;
},
action_group_copy_permissions: { target: "server" | "channel", sourceGroup: number },
action_server_group_add_client: {
id: number;
client: number | string; /* string would be the unique id */
},
action_server_group_add_client_result: {
id: number;
client: number | string;
status: "success" | "error" | "no-permissions";
error?: string;
}
action_server_group_remove_client: {
id: number;
client: number;
},
action_server_group_remove_client_result: {
id: number;
client: number;
status: "success" | "error" | "no-permissions";
error?: string;
}
query_groups: {
target: "server" | "channel",
},
query_group_clients: {
id: number
},
query_channels: {},
query_client_permissions: {},
query_client_info: {
client: number | string; /* client database id or unique id */
},
notify_channels: {
channels: ChannelInfo[]
},
notify_client_info: {
client: number | string;
state: "success" | "error" | "no-such-client" | "no-permission";
error?: string;
info?: { name: string, uniqueId: string, databaseId: number },
failedPermission?: string;
},
notify_group_updated: {
target: "server" | "channel";
id: number;
properties: GroupUpdateEntry[];
},
notify_groups_created: {
target: "server" | "channel";
groups: GroupProperties[]
},
notify_groups_deleted: {
target: "server" | "channel";
groups: number[]
},
notify_group_clients: {
id: number,
status: "success" | "error" | "no-permissions",
error?: string;
clients?: {
name: string;
databaseId: number;
uniqueId: string;
}[]
},
notify_groups_reset: {},
notify_groups: {
target: "server" | "channel",
groups: GroupProperties[]
},
notify_client_permissions: {
permissionModifyPower: number;
serverGroupCreate: boolean,
channelGroupCreate: boolean,
serverGroupModifyPower: number,
channelGroupModifyPower: number,
modifyQueryGroups: boolean,
modifyTemplateGroups: boolean
serverGroupMemberAddPower: number,
serverGroupMemberRemovePower: number,
serverGroupPermissionList: boolean,
channelGroupPermissionList: boolean,
channelPermissionList: boolean,
clientPermissionList: boolean,
clientChannelPermissionList: boolean
},
notify_client_list_toggled: { visible: boolean },
notify_channel_updated: { id: number, property: "name" | "icon", value: any },
notify_destroy: {}
}
const ActiveTabInfo = (props: { events: Registry<PermissionModalEvents> }) => {
const [activeTab, setActiveTab] = useState<PermissionEditorTab>("groups-server");
props.events.reactUse("action_activate_tab", event => setActiveTab(event.tab));
return (
<div className={cssStyle.header + " " + cssStyle.activeTabInfo}>
<div className={cssStyle.entry}>
<a title={PermissionTabName[activeTab].useTranslate()} key={"tab-" + activeTab}>
{PermissionTabName[activeTab].renderTranslate()}
</a>
</div>
</div>
);
};
const TabSelectorEntry = (props: { events: Registry<PermissionModalEvents>, entry: PermissionEditorTab }) => {
const [active, setActive] = useState(props.entry === "groups-server");
props.events.reactUse("action_activate_tab", event => setActive(event.tab === props.entry));
return (
<div className={cssStyle.entry + " " + (active ? cssStyle.selected : "")}
onClick={() => !active && props.events.fire("action_activate_tab", {tab: props.entry})}>
<a title={PermissionTabName[props.entry].useTranslate()}>
{PermissionTabName[props.entry].renderTranslate()}
</a>
</div>
);
};
const TabSelector = (props: { events: Registry<PermissionModalEvents> }) => {
return (
<div className={cssStyle.header + " " + cssStyle.tabSelector}>
<TabSelectorEntry events={props.events} entry={"groups-server"}/>
<TabSelectorEntry events={props.events} entry={"groups-channel"}/>
<TabSelectorEntry events={props.events} entry={"channel"}/>
<TabSelectorEntry events={props.events} entry={"client"}/>
<TabSelectorEntry events={props.events} entry={"client-channel"}/>
</div>
);
};
export type DefaultTabValues = { groupId?: number, channelId?: number, clientDatabaseId?: number };
class PermissionEditorModal extends InternalModal {
readonly modalEvents = new Registry<PermissionModalEvents>();
readonly editorEvents = new Registry<PermissionEditorEvents>();
readonly connection: ConnectionHandler;
readonly defaultTab: PermissionEditorTab;
readonly defaultTabValues: DefaultTabValues;
constructor(connection: ConnectionHandler, defaultTab: PermissionEditorTab, defaultTabValues?: DefaultTabValues) {
super();
this.defaultTab = defaultTab;
this.defaultTabValues = defaultTabValues || {};
this.modalEvents.enableDebug("modal-permissions");
this.editorEvents.enableDebug("permissions-editor");
this.connection = connection;
initializePermissionModalResultHandlers(this.modalEvents);
initializePermissionModalController(connection, this.modalEvents);
initializePermissionEditor(connection, this.modalEvents, this.editorEvents);
this.modalEvents.on("action_activate_tab", event => this.editorEvents.fire("action_toggle_client_button", {visible: event.tab === "groups-server"}));
this.editorEvents.on("action_toggle_client_list", event => this.modalEvents.fire("notify_client_list_toggled", {visible: event.visible}));
}
protected onInitialize() {
this.modalEvents.fire_later("action_activate_tab", {
tab: this.defaultTab,
activeChannelId: this.defaultTabValues?.channelId,
activeGroupId: this.defaultTabValues?.groupId,
activeClientDatabaseId: this.defaultTabValues?.clientDatabaseId
}); });
this.modalEvents.fire_later("query_client_permissions"); modalEvents.fire_react("query_client_permissions");
});
const modal = spawnModal("modal-permission-edit", [
{
serverUniqueId: connection.getCurrentServerUniqueId(),
handlerId: connection.handlerId
},
modalEvents.generateIpcDescription(),
editorEvents.generateIpcDescription()
], {
popoutable: true
});
modalEvents.on("notify_destroy", connection.events().on("notify_connection_state_changed", event => {
if(event.newState !== ConnectionState.CONNECTED) {
modal.destroy();
} }
}));
protected onDestroy() { modal.getEvents().on("destroy", () => {
this.modalEvents.fire("notify_destroy"); modalEvents.fire("notify_destroy");
this.modalEvents.destroy(); modalEvents.destroy();
} editorEvents.destroy();
});
renderBody() { modal.show().then(undefined);
return (
<div className={cssStyle.container}>
<div className={cssStyle.contextContainer + " " + cssStyle.left}>
<ActiveTabInfo events={this.modalEvents}/>
<SideBar modalEvents={this.modalEvents} editorEvents={this.editorEvents}
connection={this.connection}/>
</div>
<ContextDivider id={"permission-editor"} defaultValue={25} direction={"horizontal"} />
<div className={cssStyle.contextContainer + " " + cssStyle.right}>
<TabSelector events={this.modalEvents}/>
<PermissionEditor events={this.editorEvents} connection={this.connection}/>
</div>
</div>
);
}
renderTitle(): React.ReactElement<Translatable> {
return <Translatable>Server permissions</Translatable>;
}
}
export function spawnPermissionEditorModal(connection: ConnectionHandler, defaultTab: PermissionEditorTab = "groups-server", values?: DefaultTabValues) {
const modal = spawnReactModal(PermissionEditorModal, connection, defaultTab, values);
modal.show();
} }
function initializePermissionModalResultHandlers(events: Registry<PermissionModalEvents>) { function initializePermissionModalResultHandlers(events: Registry<PermissionModalEvents>) {

View File

@ -0,0 +1,196 @@
export type PermissionEditorTab = "groups-server" | "groups-channel" | "channel" | "client" | "client-channel";
export type PermissionEditorSubject =
"groups-server"
| "groups-channel"
| "channel"
| "client"
| "client-channel"
| "none";
export type GroupProperties = {
id: number,
type: "query" | "template" | "normal";
name: string,
iconId: number,
sortId: number;
saveDB: boolean;
needed_modify_power: number;
needed_member_add: number;
needed_member_remove: number;
};
export type GroupUpdateEntry = {
property: "name" | "icon" | "sort" | "save";
value: any
};
export type ChannelInfo = {
id: number;
iconId: number;
name: string;
depth: number;
}
export interface PermissionModalEvents {
action_activate_tab: {
tab: PermissionEditorTab,
activeGroupId?: number;
activeChannelId?: number;
activeClientDatabaseId?: number;
},
action_select_group: {
target: "server" | "channel",
id: number
},
action_select_channel: {
target: "channel" | "client-channel";
id: number
},
action_select_client: {
target: "client" | "client-channel";
id: number | string | undefined;
}
action_set_permission_editor_subject: {
mode: PermissionEditorSubject | undefined;
groupId?: number;
channelId?: number;
clientDatabaseId?: number;
}
action_create_group: { target: "server" | "channel", sourceGroup?: number },
action_rename_group: { target: "server" | "channel", id: number | "selected", newName: string },
action_rename_group_result: {
target: "server" | "channel";
id: number;
status: "success" | "error";
error?: string;
}
action_delete_group: { target: "server" | "channel", id: number | "selected", mode: "ask" | "force" },
action_delete_group_result: {
target: "server" | "channel";
id: number;
status: "success" | "error";
error?: string;
},
action_group_copy_permissions: { target: "server" | "channel", sourceGroup: number },
action_server_group_add_client: {
id: number;
client: number | string; /* string would be the unique id */
},
action_server_group_add_client_result: {
id: number;
client: number | string;
status: "success" | "error" | "no-permissions";
error?: string;
}
action_server_group_remove_client: {
id: number;
client: number;
},
action_server_group_remove_client_result: {
id: number;
client: number;
status: "success" | "error" | "no-permissions";
error?: string;
}
query_groups: {
target: "server" | "channel",
},
query_group_clients: {
id: number
},
query_channels: {},
query_client_permissions: {},
query_client_info: {
client: number | string; /* client database id or unique id */
},
notify_channels: {
channels: ChannelInfo[]
},
notify_client_info: {
client: number | string;
state: "success" | "error" | "no-such-client" | "no-permission";
error?: string;
info?: { name: string, uniqueId: string, databaseId: number },
failedPermission?: string;
},
notify_group_updated: {
target: "server" | "channel";
id: number;
properties: GroupUpdateEntry[];
},
notify_groups_created: {
target: "server" | "channel";
groups: GroupProperties[]
},
notify_groups_deleted: {
target: "server" | "channel";
groups: number[]
},
notify_group_clients: {
id: number,
status: "success" | "error" | "no-permissions",
error?: string;
clients?: {
name: string;
databaseId: number;
uniqueId: string;
}[]
},
notify_groups_reset: {},
notify_groups: {
target: "server" | "channel",
groups: GroupProperties[]
},
notify_client_permissions: {
permissionModifyPower: number;
serverGroupCreate: boolean,
channelGroupCreate: boolean,
serverGroupModifyPower: number,
channelGroupModifyPower: number,
modifyQueryGroups: boolean,
modifyTemplateGroups: boolean
serverGroupMemberAddPower: number,
serverGroupMemberRemovePower: number,
serverGroupPermissionList: boolean,
channelGroupPermissionList: boolean,
channelPermissionList: boolean,
clientPermissionList: boolean,
clientChannelPermissionList: boolean
},
notify_client_list_toggled: { visible: boolean },
notify_channel_updated: { id: number, property: "name" | "icon", value: any },
notify_initial_rendered: {},
notify_destroy: {}
}

View File

@ -1,194 +0,0 @@
@import "../../../../css/static/mixin";
@import "../../../../css/static/properties";
html:root {
--modal-permissions-header-text: #e1e1e1;
--modal-permissions-header-background: #19191b;
--modal-permissions-header-hover: #4e4e4e;
--modal-permissions-header-selected: #0073d4;
--modal-permission-right: #303036;
--modal-permission-left: #222226;
--modal-permissions-entry-hover: #28282c;
--modal-permissions-entry-selected: #111111;
--modal-permissions-current-group: #101012;
--modal-permissions-buttons-background: #0f0f0f;
--modal-permissions-buttons-hover: #262626;
--modal-permissions-buttons-disabled: #1b1b1b;
--modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */
--modal-permissions-container-seperator: #222224; /* the seperator between left and right */
--modal-permissions-icon-select: #121213;
--modal-permissions-icon-select-border: #0d0d0d;
--modal-permissions-icon-select-hover: #17171a;
--modal-permissions-icon-select-hover-border: #333333;
--modal-permission-no-permnissions: #18171c;
--modal-permissions-table-border: #1e2025;
--modal-permissions-table-header: #303036;
--modal-permissions-table-row-odd: #303036;
--modal-permissions-table-row-even: #25252a;
--modal-permissions-table-row-hover: #343a47;
--modal-permissions-table-header-text: #e1e1e1;
--modal-permissions-table-row-text: #535455;
--modal-permissions-table-entry-active-text: #e1e1e1;
--modal-permissions-table-entry-group-text: #e1e1e1;
--modal-permissions-table-input: #e1e1e1;
--modal-permissions-table-input-focus: #3f7dbf;
}
.container {
@include user-select(none);
display: flex;
flex-direction: row;
justify-content: stretch;
width: 1000em;
min-width: 20em;
max-width: 100%;
min-height: 20em;
flex-shrink: 1;
flex-grow: 1;
.contextContainer {
display: flex;
flex-direction: column;
justify-content: stretch;
&.left {
min-width: 10em;
min-height: 10em;
overflow: hidden;
background-color: var(--modal-permission-left);
}
&.right {
min-width: 30em;
background-color: var(--modal-permission-right);
}
}
.header {
flex-shrink: 0;
flex-grow: 0;
height: 4em;
background-color: var(--modal-permissions-header-background);
color: var(--modal-permissions-header-text);
display: flex;
flex-direction: row;
justify-content: stretch;
.entry {
flex-grow: 1;
flex-shrink: 1;
text-align: center;
height: 100%;
padding-left: .5em;
padding-right: .5em;
display: flex;
flex-direction: column;
justify-content: space-around;
}
&.tabSelector {
min-width: 8em;
.entry {
position: relative;
overflow: hidden;
cursor: pointer;
padding-bottom: 2px;
a {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
border: none;
border-bottom: 2px solid var(--modal-permissions-header-hover);
padding-bottom: 0;
&:before {
position: absolute;
content: '';
margin-right: -10em;
margin-left: -10em;
margin-bottom: -.2em;
bottom: 0;
height: 100%;
width: calc(100% + 20em);
box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-hover);
}
}
&.selected {
border: none;
border-bottom: 2px solid var(--modal-permissions-header-selected);
padding-bottom: 0;
&:before {
position: absolute;
content: '';
margin-right: -10em;
margin-left: -10em;
margin-bottom: -.2em;
bottom: 0;
height: 100%;
width: calc(100% + 20em);
box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-selected);
}
}
}
}
&.activeTabInfo {
min-width: 6em;
font-weight: bold;
.entry {
overflow: hidden;
a {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> * {
font-size: 1.5em;
}
}
}
.body {
flex-grow: 1;
flex-shrink: 1;
}
}

View File

@ -1,6 +1,199 @@
@import "../../../../css/static/mixin"; @import "../../../../css/static/mixin";
@import "../../../../css/static/properties"; @import "../../../../css/static/properties";
html:root {
--modal-permissions-header-text: #e1e1e1;
--modal-permissions-header-background: #19191b;
--modal-permissions-header-hover: #4e4e4e;
--modal-permissions-header-selected: #0073d4;
--modal-permission-right: #303036;
--modal-permission-left: #222226;
--modal-permissions-entry-hover: #28282c;
--modal-permissions-entry-selected: #111111;
--modal-permissions-current-group: #101012;
--modal-permissions-buttons-background: #0f0f0f;
--modal-permissions-buttons-hover: #262626;
--modal-permissions-buttons-disabled: #1b1b1b;
--modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */
--modal-permissions-container-seperator: #222224; /* the seperator between left and right */
--modal-permissions-icon-select: #121213;
--modal-permissions-icon-select-border: #0d0d0d;
--modal-permissions-icon-select-hover: #17171a;
--modal-permissions-icon-select-hover-border: #333333;
--modal-permission-no-permnissions: #18171c;
--modal-permissions-table-border: #1e2025;
--modal-permissions-table-header: #303036;
--modal-permissions-table-row-odd: #303036;
--modal-permissions-table-row-even: #25252a;
--modal-permissions-table-row-hover: #343a47;
--modal-permissions-table-header-text: #e1e1e1;
--modal-permissions-table-row-text: #535455;
--modal-permissions-table-entry-active-text: #e1e1e1;
--modal-permissions-table-entry-group-text: #e1e1e1;
--modal-permissions-table-input: #e1e1e1;
--modal-permissions-table-input-focus: #3f7dbf;
}
.container {
@include user-select(none);
display: flex;
flex-direction: row;
justify-content: stretch;
width: 1000em;
min-width: 20em;
max-width: 100%;
min-height: 20em;
flex-shrink: 1;
flex-grow: 1;
.contextContainer {
display: flex;
flex-direction: column;
justify-content: stretch;
&.left {
min-width: 10em;
min-height: 10em;
overflow: hidden;
background-color: var(--modal-permission-left);
}
&.right {
min-width: 30em;
background-color: var(--modal-permission-right);
}
}
.header {
flex-shrink: 0;
flex-grow: 0;
height: 4em;
background-color: var(--modal-permissions-header-background);
color: var(--modal-permissions-header-text);
display: flex;
flex-direction: row;
justify-content: stretch;
.entry {
flex-grow: 1;
flex-shrink: 1;
text-align: center;
height: 100%;
padding-left: .5em;
padding-right: .5em;
display: flex;
flex-direction: column;
justify-content: space-around;
}
&.tabSelector {
min-width: 8em;
.entry {
position: relative;
overflow: hidden;
cursor: pointer;
padding-bottom: 2px;
a {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
&:hover {
border: none;
border-bottom: 2px solid var(--modal-permissions-header-hover);
padding-bottom: 0;
&:before {
position: absolute;
content: '';
margin-right: -10em;
margin-left: -10em;
margin-bottom: -.2em;
bottom: 0;
height: 100%;
width: calc(100% + 20em);
box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-hover);
}
}
&.selected {
border: none;
border-bottom: 2px solid var(--modal-permissions-header-selected);
padding-bottom: 0;
&:before {
position: absolute;
content: '';
margin-right: -10em;
margin-left: -10em;
margin-bottom: -.2em;
bottom: 0;
height: 100%;
width: calc(100% + 20em);
box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-selected);
}
}
}
}
&.activeTabInfo {
min-width: 6em;
font-weight: bold;
.entry {
overflow: hidden;
a {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
> * {
font-size: 1.5em;
}
}
}
.body {
flex-grow: 1;
flex-shrink: 1;
}
}
.containerList { .containerList {
color: var(--text); color: var(--text);

View File

@ -1,9 +1,6 @@
import * as React from "react"; import * as React from "react";
import {useRef, useState} from "react"; import {useContext, useEffect, useRef, useState} from "react";
import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; import {EventHandler, IpcRegistryDescription, ReactEventHandler, Registry} from "tc-shared/events";
import {ChannelInfo, GroupProperties, PermissionModalEvents} from "tc-shared/ui/modal/permission/ModalPermissionEditor";
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/PermissionEditor";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon"; import {RemoteIconRenderer} from "tc-shared/ui/react-elements/Icon";
import {createInputModal} from "tc-shared/ui/elements/Modal"; import {createInputModal} from "tc-shared/ui/elements/Modal";
import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {Translatable} from "tc-shared/ui/react-elements/i18n";
@ -15,23 +12,26 @@ import {FlatInputField} from "tc-shared/ui/react-elements/InputField";
import {arrayBufferBase64} from "tc-shared/utils/buffers"; import {arrayBufferBase64} from "tc-shared/utils/buffers";
import {tra} from "tc-shared/i18n/localize"; import {tra} from "tc-shared/i18n/localize";
import {getIconManager} from "tc-shared/file/Icons"; import {getIconManager} from "tc-shared/file/Icons";
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
import {
ChannelInfo,
GroupProperties,
PermissionEditorTab,
PermissionModalEvents
} from "tc-shared/ui/modal/permission/ModalDefinitions";
import {useTr} from "tc-shared/ui/react-elements/Helper";
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import {ContextDivider} from "tc-shared/ui/react-elements/ContextDivider";
import {EditorRenderer} from "tc-shared/ui/modal/permission/EditorRenderer";
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
import {ClientIcon} from "svg-sprites/client-icons";
const cssStyle = require("./TabHandler.scss"); const cssStyle = require("./ModalRenderer.scss");
export class SideBar extends React.Component<{ connection: ConnectionHandler, modalEvents: Registry<PermissionModalEvents>, editorEvents: Registry<PermissionEditorEvents> }, {}> { export type PermissionEditorServerInfo = { handlerId: string, serverUniqueId: string };
render() { const ModalEventContext = React.createContext<Registry<PermissionModalEvents>>(undefined);
return [ const EditorEventContext = React.createContext<Registry<PermissionEditorEvents>>(undefined);
<ServerGroupsSideBar key={"server-groups"} connection={this.props.connection} const ServerInfoContext = React.createContext<PermissionEditorServerInfo>(undefined);
modalEvents={this.props.modalEvents}/>,
<ChannelGroupsSideBar key={"channel-groups"} connection={this.props.connection}
modalEvents={this.props.modalEvents}/>,
<ChannelSideBar key={"channel"} connection={this.props.connection} modalEvents={this.props.modalEvents}/>,
<ClientSideBar key={"client"} connection={this.props.connection} modalEvents={this.props.modalEvents}/>,
<ClientChannelSideBar key={"client-channel"} connection={this.props.connection}
modalEvents={this.props.modalEvents}/>
];
}
}
const GroupsButton = (props: { image: string, alt: string, onClick?: () => void, disabled: boolean }) => { const GroupsButton = (props: { image: string, alt: string, onClick?: () => void, disabled: boolean }) => {
return ( return (
@ -43,7 +43,9 @@ const GroupsButton = (props: { image: string, alt: string, onClick?: () => void,
}; };
const GroupsListEntry = (props: { connection: ConnectionHandler, group: GroupProperties, selected: boolean, callbackSelect: () => void, onContextMenu: (event: React.MouseEvent) => void }) => { const GroupsListEntry = React.memo((props: { group: GroupProperties, selected: boolean, callbackSelect: () => void, onContextMenu: (event: React.MouseEvent) => void }) => {
const serverInfo = useContext(ServerInfoContext);
let groupTypePrefix = ""; let groupTypePrefix = "";
switch (props.group.type) { switch (props.group.type) {
case "query": case "query":
@ -57,14 +59,14 @@ const GroupsListEntry = (props: { connection: ConnectionHandler, group: GroupPro
return ( return (
<div className={cssStyle.entry + " " + (props.selected ? cssStyle.selected : "")} onClick={props.callbackSelect} <div className={cssStyle.entry + " " + (props.selected ? cssStyle.selected : "")} onClick={props.callbackSelect}
onContextMenu={props.onContextMenu}> onContextMenu={props.onContextMenu}>
<RemoteIconRenderer icon={getIconManager().resolveIcon(props.group.iconId, props.connection.getCurrentServerUniqueId(), props.connection.handlerId)} /> <RemoteIconRenderer icon={getIconManager().resolveIcon(props.group.iconId, serverInfo.serverUniqueId, serverInfo.handlerId)} />
<div className={cssStyle.name}>{groupTypePrefix + props.group.name + " (" + props.group.id + ")"}</div> <div className={cssStyle.name}>{groupTypePrefix + props.group.name + " (" + props.group.id + ")"}</div>
</div> </div>
) )
}; });
@ReactEventHandler<GroupsList>(e => e.props.events) @ReactEventHandler<GroupsList>(e => e.props.events)
class GroupsList extends React.Component<{ connection: ConnectionHandler, events: Registry<PermissionModalEvents>, target: "server" | "channel" }, { class GroupsList extends React.PureComponent<{ events: Registry<PermissionModalEvents>, target: "server" | "channel" }, {
selectedGroupId: number, selectedGroupId: number,
showQueryGroups: boolean, showQueryGroups: boolean,
showTemplateGroups: boolean, showTemplateGroups: boolean,
@ -118,8 +120,8 @@ class GroupsList extends React.Component<{ connection: ConnectionHandler, events
}); });
}}> }}>
<div className={cssStyle.entries}> <div className={cssStyle.entries}>
{this.visibleGroups.map(e => <GroupsListEntry key={"group-" + e.id} {this.visibleGroups.map(e => (
connection={this.props.connection} <GroupsListEntry key={"group-" + e.id}
group={e} group={e}
selected={e.id === this.state.selectedGroupId} selected={e.id === this.state.selectedGroupId}
callbackSelect={() => this.props.events.fire("action_select_group", { callbackSelect={() => this.props.events.fire("action_select_group", {
@ -132,39 +134,45 @@ class GroupsList extends React.Component<{ connection: ConnectionHandler, events
target: this.props.target, target: this.props.target,
id: e.id id: e.id
}); });
spawn_context_menu(event.pageX, event.pageY, {
name: tr("Rename group"), spawnContextMenu({ pageX: event.pageX, pageY: event.pageY }, [
type: MenuEntryType.ENTRY, {
callback: () => this.onGroupRename(), type: "normal",
icon_class: "client-change_nickname", label: tr("Rename group"),
invalidPermission: this.state.disableGroupRename click: () => this.onGroupRename(),
icon: ClientIcon.ChangeNickname,
enabled: !this.state.disableGroupRename
}, { }, {
name: tr("Copy permissions"), type: "normal",
type: MenuEntryType.ENTRY, label: tr("Copy permissions"),
icon_class: "client-copy", icon: ClientIcon.Copy,
callback: () => this.props.events.fire("action_group_copy_permissions", { click: () => this.props.events.fire("action_group_copy_permissions", {
target: this.props.target, target: this.props.target,
sourceGroup: e.id sourceGroup: e.id
}), }),
invalidPermission: this.state.disablePermissionCopy enabled: !this.state.disablePermissionCopy
}, { }, {
name: tr("Delete group"), type: "normal",
type: MenuEntryType.ENTRY, label: tr("Delete group"),
icon_class: "client-delete", click: () => this.onGroupDelete(),
callback: () => this.onGroupDelete(), icon: ClientIcon.Delete,
invalidPermission: this.state.disableDelete enabled: !this.state.disableDelete
}, contextmenu.Entry.HR(), { }, {
name: tr("Add group"), type: "separator"
icon_class: "client-add", }, {
type: MenuEntryType.ENTRY, type: "normal",
callback: () => this.props.events.fire("action_create_group", { label: tr("Add group"),
click: () => this.props.events.fire("action_create_group", {
target: this.props.target, target: this.props.target,
sourceGroup: e.id sourceGroup: e.id
}), }),
invalidPermission: this.state.disableGroupAdd icon: ClientIcon.Add,
}); enabled: !this.state.disableGroupAdd
}
]);
}} }}
/>)} />
))}
</div> </div>
</div>, </div>,
<div key={"buttons"} className={cssStyle.buttons}> <div key={"buttons"} className={cssStyle.buttons}>
@ -426,7 +434,7 @@ class GroupsList extends React.Component<{ connection: ConnectionHandler, events
@ReactEventHandler<GroupsList>(e => e.props.events) @ReactEventHandler<GroupsList>(e => e.props.events)
class ServerClientList extends React.Component<{ connection: ConnectionHandler, events: Registry<PermissionModalEvents> }, { class ServerClientList extends React.Component<{ events: Registry<PermissionModalEvents> }, {
selectedGroupId: number, selectedGroupId: number,
selectedClientId: number, selectedClientId: number,
@ -475,16 +483,18 @@ class ServerClientList extends React.Component<{ connection: ConnectionHandler,
return [ return [
<div key={"list"} className={cssStyle.list + " " + cssStyle.containerList} <div key={"list"} className={cssStyle.list + " " + cssStyle.containerList}
onContextMenu={e => this.onListContextMenu(e)}> onContextMenu={e => this.onListContextMenu(e)}>
{selectedGroup ? {selectedGroup ? (
<ServerInfoContext.Consumer>
{serverInfo => (
<div key={"selected-group"} className={cssStyle.entry + " " + cssStyle.selectedGroup}> <div key={"selected-group"} className={cssStyle.entry + " " + cssStyle.selectedGroup}>
<div className={cssStyle.icon}> <div className={cssStyle.icon}>
<RemoteIconRenderer icon={getIconManager().resolveIcon(selectedGroup.iconId, this.props.connection.getCurrentServerUniqueId(), this.props.connection.handlerId)} /> <RemoteIconRenderer icon={getIconManager().resolveIcon(selectedGroup.iconId, serverInfo.serverUniqueId, serverInfo.handlerId)} />
</div> </div>
<div <div className={cssStyle.name}>{groupTypePrefix + selectedGroup.name + " (" + selectedGroup.id + ")"}</div>
className={cssStyle.name}>{groupTypePrefix + selectedGroup.name + " (" + selectedGroup.id + ")"}</div>
</div> </div>
: undefined )}
} </ServerInfoContext.Consumer>
) : undefined}
<div className={cssStyle.entries}> <div className={cssStyle.entries}>
{this.clients.map(client => <div {this.clients.map(client => <div
key={"client-" + client.databaseId} key={"client-" + client.databaseId}
@ -781,41 +791,43 @@ class ServerClientList extends React.Component<{ connection: ConnectionHandler,
} }
} }
const ServerGroupsSideBar = (props: { connection: ConnectionHandler, modalEvents: Registry<PermissionModalEvents> }) => { const ServerGroupsSideBar = React.memo(() => {
const events = useContext(ModalEventContext);
const [active, setActive] = useState(true); const [active, setActive] = useState(true);
const [clientList, setClientList] = useState(false); const [clientList, setClientList] = useState(false);
props.modalEvents.reactUse("action_activate_tab", event => setActive(event.tab === "groups-server")); events.reactUse("action_activate_tab", event => setActive(event.tab === "groups-server"));
props.modalEvents.reactUse("notify_client_list_toggled", event => setClientList(event.visible)); events.reactUse("notify_client_list_toggled", event => setClientList(event.visible));
return ( return (
<div <div
className={cssStyle.sideContainer + " " + cssStyle.containerServerGroups + " " + (active ? "" : cssStyle.hidden)}> className={cssStyle.sideContainer + " " + cssStyle.containerServerGroups + " " + (active ? "" : cssStyle.hidden)}>
<div className={cssStyle.containerGroupList + " " + (!clientList ? "" : cssStyle.hidden)}> <div className={cssStyle.containerGroupList + " " + (!clientList ? "" : cssStyle.hidden)}>
<GroupsList connection={props.connection} events={props.modalEvents} target={"server"}/> <GroupsList events={events} target={"server"}/>
</div> </div>
<div className={cssStyle.containerClientList + " " + (clientList ? "" : cssStyle.hidden)}> <div className={cssStyle.containerClientList + " " + (clientList ? "" : cssStyle.hidden)}>
<ServerClientList connection={props.connection} events={props.modalEvents}/> <ServerClientList events={events} />
</div> </div>
</div> </div>
); );
}; });
const ChannelGroupsSideBar = (props: { connection: ConnectionHandler, modalEvents: Registry<PermissionModalEvents> }) => { const ChannelGroupsSideBar = React.memo(() => {
const events = useContext(ModalEventContext);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
props.modalEvents.reactUse("action_activate_tab", event => setActive(event.tab === "groups-channel")); events.reactUse("action_activate_tab", event => setActive(event.tab === "groups-channel"));
return ( return (
<div <div
className={cssStyle.sideContainer + " " + cssStyle.containerChannelGroups + " " + (active ? "" : cssStyle.hidden)}> className={cssStyle.sideContainer + " " + cssStyle.containerChannelGroups + " " + (active ? "" : cssStyle.hidden)}>
<GroupsList connection={props.connection} events={props.modalEvents} target={"channel"}/> <GroupsList events={events} target={"channel"}/>
</div> </div>
); );
}; });
@ReactEventHandler<ChannelList>(e => e.props.events) @ReactEventHandler<ChannelList>(e => e.props.events)
class ChannelList extends React.Component<{ connection: ConnectionHandler, events: Registry<PermissionModalEvents>, tabTarget: "channel" | "client-channel" }, { selectedChanelId: number }> { class ChannelList extends React.Component<{ serverInfo: PermissionEditorServerInfo, events: Registry<PermissionModalEvents>, tabTarget: "channel" | "client-channel" }, { selectedChanelId: number }> {
private channels: ChannelInfo[] = []; private channels: ChannelInfo[] = [];
private isActiveTab = false; private isActiveTab = false;
@ -849,7 +861,7 @@ class ChannelList extends React.Component<{ connection: ConnectionHandler, event
id: e.id id: e.id
})} })}
> >
<RemoteIconRenderer icon={getIconManager().resolveIcon(e.iconId, this.props.connection.getCurrentServerUniqueId(), this.props.connection.handlerId)} /> <RemoteIconRenderer icon={getIconManager().resolveIcon(e.iconId, this.props.serverInfo.serverUniqueId, this.props.serverInfo.handlerId)} />
<a className={cssStyle.name}>{e.name + " (" + e.id + ")"}</a> <a className={cssStyle.name}>{e.name + " (" + e.id + ")"}</a>
</div> </div>
))} ))}
@ -925,20 +937,24 @@ class ChannelList extends React.Component<{ connection: ConnectionHandler, event
} }
} }
const ChannelSideBar = (props: { connection: ConnectionHandler, modalEvents: Registry<PermissionModalEvents> }) => { const ChannelSideBar = React.memo(() => {
const serverInfo = useContext(ServerInfoContext);
const events = useContext(ModalEventContext);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
props.modalEvents.reactUse("action_activate_tab", event => setActive(event.tab === "channel")); events.reactUse("action_activate_tab", event => setActive(event.tab === "channel"));
return ( return (
<div <div
className={cssStyle.sideContainer + " " + cssStyle.containerChannels + " " + (active ? "" : cssStyle.hidden)}> className={cssStyle.sideContainer + " " + cssStyle.containerChannels + " " + (active ? "" : cssStyle.hidden)}>
<ChannelList connection={props.connection} events={props.modalEvents} tabTarget={"channel"}/> <ChannelList serverInfo={serverInfo} events={events} tabTarget={"channel"}/>
</div> </div>
); );
}; });
const ClientSelect = React.memo((props: { tabTarget: "client" | "client-channel" }) => {
const events = React.useContext(ModalEventContext);
const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarget: "client" | "client-channel" }) => {
const [clientIdentifier, setClientIdentifier] = useState<number | string | undefined>(undefined); const [clientIdentifier, setClientIdentifier] = useState<number | string | undefined>(undefined);
const [clientInfo, setClientInfo] = useState<{ name: string, uniqueId: string, databaseId: number }>(undefined); const [clientInfo, setClientInfo] = useState<{ name: string, uniqueId: string, databaseId: number }>(undefined);
@ -947,24 +963,24 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
const refUniqueIdentifier = useRef<FlatInputField>(); const refUniqueIdentifier = useRef<FlatInputField>();
const refDatabaseId = useRef<FlatInputField>(); const refDatabaseId = useRef<FlatInputField>();
props.events.reactUse("action_activate_tab", event => { events.reactUse("action_activate_tab", event => {
if (event.tab !== props.tabTarget) { if (event.tab !== props.tabTarget) {
return; return;
} }
if (typeof event.activeClientDatabaseId !== "undefined") { if (typeof event.activeClientDatabaseId !== "undefined") {
props.events.fire("action_select_client", { events.fire("action_select_client", {
target: props.tabTarget, target: props.tabTarget,
id: event.activeClientDatabaseId === 0 ? "undefined" : event.activeClientDatabaseId id: event.activeClientDatabaseId === 0 ? "undefined" : event.activeClientDatabaseId
}); });
} else { } else {
if (clientInfo && clientInfo.databaseId) { if (clientInfo && clientInfo.databaseId) {
props.events.fire("action_set_permission_editor_subject", { events.fire("action_set_permission_editor_subject", {
mode: props.tabTarget, mode: props.tabTarget,
clientDatabaseId: clientInfo.databaseId clientDatabaseId: clientInfo.databaseId
}); });
} else { } else {
props.events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0}); events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0});
} }
} }
}); });
@ -979,16 +995,16 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
refDatabaseId.current?.setState({placeholder: placeholder}); refDatabaseId.current?.setState({placeholder: placeholder});
}; };
props.events.reactUse("query_client_info", event => { events.reactUse("query_client_info", event => {
if (event.client !== clientIdentifier) if (event.client !== clientIdentifier)
return; return;
refInput.current?.setState({disabled: true}); refInput.current?.setState({disabled: true});
resetInfoFields(tr("loading...")); resetInfoFields(tr("loading..."));
props.events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0}); events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0});
}); });
props.events.reactUse("notify_client_info", event => { events.reactUse("notify_client_info", event => {
if (event.client !== clientIdentifier) if (event.client !== clientIdentifier)
return; return;
@ -999,7 +1015,7 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
refNickname.current?.setValue(event.info.name); refNickname.current?.setValue(event.info.name);
refUniqueIdentifier.current?.setValue(event.info.uniqueId); refUniqueIdentifier.current?.setValue(event.info.uniqueId);
refDatabaseId.current?.setValue(event.info.databaseId + ""); refDatabaseId.current?.setValue(event.info.databaseId + "");
props.events.fire("action_set_permission_editor_subject", { events.fire("action_set_permission_editor_subject", {
mode: props.tabTarget, mode: props.tabTarget,
clientDatabaseId: event.info.databaseId clientDatabaseId: event.info.databaseId
}); });
@ -1022,7 +1038,7 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
refDatabaseId.current?.setState({placeholder: undefined}); refDatabaseId.current?.setState({placeholder: undefined});
}); });
props.events.reactUse("action_select_client", event => { events.reactUse("action_select_client", event => {
if (event.target !== props.tabTarget) if (event.target !== props.tabTarget)
return; return;
@ -1030,11 +1046,11 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
refInput.current.setValue(typeof event.id === "undefined" ? "" : event.id.toString()); refInput.current.setValue(typeof event.id === "undefined" ? "" : event.id.toString());
if (typeof event.id === "number" || typeof event.id === "string") { if (typeof event.id === "number" || typeof event.id === "string") {
/* first do the state update */ /* first do the state update */
props.events.fire_react("query_client_info", {client: event.id}); events.fire_react("query_client_info", {client: event.id});
} else { } else {
refInput.current?.setValue(undefined); refInput.current?.setValue(undefined);
resetInfoFields(undefined); resetInfoFields(undefined);
props.events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0}); events.fire("action_set_permission_editor_subject", {mode: props.tabTarget, clientDatabaseId: 0});
} }
}); });
@ -1086,7 +1102,7 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
} }
} }
refInput.current?.setState({isInvalid: false}); refInput.current?.setState({isInvalid: false});
props.events.fire("action_select_client", {id: client, target: props.tabTarget}); events.fire("action_select_client", {id: client, target: props.tabTarget});
}} }}
/> />
<hr/> <hr/>
@ -1097,31 +1113,147 @@ const ClientSelect = (props: { events: Registry<PermissionModalEvents>, tabTarge
disabled={true}/> disabled={true}/>
</div> </div>
); );
}; });
const ClientSideBar = (props: { connection: ConnectionHandler, modalEvents: Registry<PermissionModalEvents> }) => { const ClientSideBar = React.memo(() => {
const events = useContext(ModalEventContext);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
props.modalEvents.reactUse("action_activate_tab", event => setActive(event.tab === "client")); events.reactUse("action_activate_tab", event => setActive(event.tab === "client"));
return ( return (
<div <div
className={cssStyle.sideContainer + " " + cssStyle.containerClient + " " + (active ? "" : cssStyle.hidden)}> className={cssStyle.sideContainer + " " + cssStyle.containerClient + " " + (active ? "" : cssStyle.hidden)}>
<ClientSelect events={props.modalEvents} tabTarget={"client"}/> <ClientSelect tabTarget={"client"}/>
</div> </div>
); );
}; });
const ClientChannelSideBar = (props: { connection: ConnectionHandler, modalEvents: Registry<PermissionModalEvents> }) => { const ClientChannelSideBar = React.memo(() => {
const serverInfo = useContext(ServerInfoContext);
const events = useContext(ModalEventContext);
const [active, setActive] = useState(false); const [active, setActive] = useState(false);
props.modalEvents.reactUse("action_activate_tab", event => setActive(event.tab === "client-channel")); events.reactUse("action_activate_tab", event => setActive(event.tab === "client-channel"));
return ( return (
<div <div
className={cssStyle.sideContainer + " " + cssStyle.containerChannelClient + " " + (active ? "" : cssStyle.hidden)}> className={cssStyle.sideContainer + " " + cssStyle.containerChannelClient + " " + (active ? "" : cssStyle.hidden)}>
<ClientSelect events={props.modalEvents} tabTarget={"client-channel"}/> <ClientSelect tabTarget={"client-channel"}/>
<ChannelList connection={props.connection} events={props.modalEvents} tabTarget={"client-channel"}/> <ChannelList serverInfo={serverInfo} events={events} tabTarget={"client-channel"}/>
</div> </div>
); );
});
export const PermissionTabName: { [T in PermissionEditorTab]: { name: string, useTranslate: () => string, renderTranslate: () => React.ReactNode } } = {
"groups-server": {name: "Server Groups", useTranslate: () => useTr("Server Groups"), renderTranslate: () => <Translatable>Server Groups</Translatable>},
"groups-channel": {name: "Channel Groups", useTranslate: () => useTr("Channel Groups"), renderTranslate: () => <Translatable>Channel Groups</Translatable>},
"channel": {name: "Channel Permissions", useTranslate: () => useTr("Channel Permissions"), renderTranslate: () => <Translatable>Channel Permissions</Translatable>},
"client": {name: "Client Permissions", useTranslate: () => useTr("Client Permissions"), renderTranslate: () => <Translatable>Client Permissions</Translatable>},
"client-channel": {name: "Client Channel Permissions", useTranslate: () => useTr("Client Channel Permissions"), renderTranslate: () => <Translatable>Client Channel Permissions</Translatable>},
}; };
const ActiveTabInfo = React.memo(() => {
const events = useContext(ModalEventContext);
const [activeTab, setActiveTab] = useState<PermissionEditorTab>("groups-server");
events.reactUse("action_activate_tab", event => setActiveTab(event.tab));
return (
<div className={cssStyle.header + " " + cssStyle.activeTabInfo}>
<div className={cssStyle.entry}>
<a title={PermissionTabName[activeTab].useTranslate()} key={"tab-" + activeTab}>
{PermissionTabName[activeTab].renderTranslate()}
</a>
</div>
</div>
);
});
const TabSelectorEntry = React.memo((props: { entry: PermissionEditorTab }) => {
const events = useContext(ModalEventContext);
const [active, setActive] = useState(props.entry === "groups-server");
events.reactUse("action_activate_tab", event => setActive(event.tab === props.entry));
return (
<div className={cssStyle.entry + " " + (active ? cssStyle.selected : "")}
onClick={() => !active && events.fire("action_activate_tab", {tab: props.entry})}>
<a title={PermissionTabName[props.entry].useTranslate()}>
{PermissionTabName[props.entry].renderTranslate()}
</a>
</div>
);
});
const TabSelector = React.memo(() => {
return (
<div className={cssStyle.header + " " + cssStyle.tabSelector}>
<TabSelectorEntry entry={"groups-server"}/>
<TabSelectorEntry entry={"groups-channel"}/>
<TabSelectorEntry entry={"channel"}/>
<TabSelectorEntry entry={"client"}/>
<TabSelectorEntry entry={"client-channel"}/>
</div>
);
});
const InitialRendererTrigger = React.memo(() => {
const events = useContext(ModalEventContext);
useEffect(() => events.fire("notify_initial_rendered"), []);
return null;
})
export type DefaultTabValues = { groupId?: number, channelId?: number, clientDatabaseId?: number };
export class PermissionEditorModal extends AbstractModal {
readonly serverInfo: PermissionEditorServerInfo;
readonly modalEvents: Registry<PermissionModalEvents>;
readonly editorEvents: Registry<PermissionEditorEvents>;
constructor(serverInfo: PermissionEditorServerInfo, modalEvents: IpcRegistryDescription<PermissionModalEvents>, editorEvents: IpcRegistryDescription<PermissionEditorEvents>) {
super();
this.serverInfo = serverInfo;
this.modalEvents = Registry.fromIpcDescription(modalEvents);
this.editorEvents = Registry.fromIpcDescription(editorEvents);
}
protected onDestroy() {
super.onDestroy();
this.modalEvents.destroy();
this.editorEvents.destroy();
}
renderBody() {
return (
<ModalEventContext.Provider value={this.modalEvents}>
<EditorEventContext.Provider value={this.editorEvents}>
<ServerInfoContext.Provider value={this.serverInfo}>
<div className={cssStyle.container}>
<div className={cssStyle.contextContainer + " " + cssStyle.left}>
<ActiveTabInfo />
<ServerGroupsSideBar key={"server-groups"} />
<ChannelGroupsSideBar key={"channel-groups"} />
<ChannelSideBar key={"channel"} />
<ClientSideBar key={"client"} />
<ClientChannelSideBar key={"client-channel"} />
</div>
<ContextDivider id={"permission-editor"} defaultValue={25} direction={"horizontal"} />
<div className={cssStyle.contextContainer + " " + cssStyle.right}>
<TabSelector />
<EditorRenderer events={this.editorEvents} handlerId={this.serverInfo.handlerId} serverUniqueId={this.serverInfo.serverUniqueId} />
</div>
</div>
<InitialRendererTrigger />
</ServerInfoContext.Provider>
</EditorEventContext.Provider>
</ModalEventContext.Provider>
);
}
renderTitle(): React.ReactElement<Translatable> {
return <Translatable>Server permission editor</Translatable>;
}
}
export default PermissionEditorModal;

View File

@ -0,0 +1,32 @@
.arrow {
display: inline-block;
border: solid black;
//border-width: 0 3px 3px 0;
//padding: 3px;
//height: 10px;
border-width: 0 .2em .2em 0;
padding: .21em;
height: .5em;
width: .5em;
&.right {
transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
}
&.left {
transform: rotate(135deg);
-webkit-transform: rotate(135deg);
}
&.up {
transform: rotate(-135deg);
-webkit-transform: rotate(-135deg);
}
&.down {
transform: rotate(45deg);
-webkit-transform: rotate(45deg);
}
}

View File

@ -0,0 +1,19 @@
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
import React from "react";
const cssStyle = require("./Arrow.scss");
export const Arrow = (props: {
direction: "up" | "down" | "left" | "right",
className?: string,
onClick?: () => void
}) => (
<div
className={joinClassList(
cssStyle.arrow,
cssStyle[props.direction],
props.className
)}
onClick={props.onClick}
/>
)

View File

@ -16,6 +16,9 @@ import {
ModalClientGroupAssignmentVariables ModalClientGroupAssignmentVariables
} from "tc-shared/ui/modal/group-assignment/Definitions"; } from "tc-shared/ui/modal/group-assignment/Definitions";
import {VideoViewerEvents} from "tc-shared/ui/modal/video-viewer/Definitions"; import {VideoViewerEvents} from "tc-shared/ui/modal/video-viewer/Definitions";
import {PermissionModalEvents} from "tc-shared/ui/modal/permission/ModalDefinitions";
import {PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
import {PermissionEditorServerInfo} from "tc-shared/ui/modal/permission/ModalRenderer";
export type ModalType = "error" | "warning" | "info" | "none"; export type ModalType = "error" | "warning" | "info" | "none";
export type ModalRenderType = "page" | "dialog"; export type ModalRenderType = "page" | "dialog";
@ -133,6 +136,7 @@ export abstract class AbstractModal {
color() : "none" | "blue" { return "none"; } color() : "none" | "blue" { return "none"; }
verticalAlignment() : "top" | "center" | "bottom" { return "center"; } verticalAlignment() : "top" | "center" | "bottom" { return "center"; }
/** @deprecated */
protected onInitialize() {} protected onInitialize() {}
protected onDestroy() {} protected onDestroy() {}
@ -196,4 +200,9 @@ export interface ModalConstructorArguments {
/* events */ IpcRegistryDescription<ModalClientGroupAssignmentEvents>, /* events */ IpcRegistryDescription<ModalClientGroupAssignmentEvents>,
/* variables */ IpcVariableDescriptor<ModalClientGroupAssignmentVariables>, /* variables */ IpcVariableDescriptor<ModalClientGroupAssignmentVariables>,
], ],
"modal-permission-edit": [
/* serverInfo */ PermissionEditorServerInfo,
/* modalEvents */ IpcRegistryDescription<PermissionModalEvents>,
/* editorEvents */ IpcRegistryDescription<PermissionEditorEvents>
]
} }

View File

@ -97,4 +97,10 @@ registerModal({
popoutSupported: true popoutSupported: true
}); });
registerModal({
modalId: "modal-permission-edit",
classLoader: async () => await import("tc-shared/ui/modal/permission/ModalRenderer"),
popoutSupported: true
});

View File

@ -85,7 +85,9 @@ async function initializeModalRenderer(taskId) {
loader.setCurrentTaskName(taskId, tr("initializing modal class")); loader.setCurrentTaskName(taskId, tr("initializing modal class"));
try { try {
mainModalInstance = constructAbstractModalClass(modalClass, { windowed: true }, result.constructorArguments); mainModalInstance = constructAbstractModalClass(modalClass, { windowed: true }, result.constructorArguments);
mainModalInstance["onInitialize"]();
mainModalRenderer.renderModal(mainModalInstance); mainModalRenderer.renderModal(mainModalInstance);
mainModalInstance["onOpen"]();
} catch(error) { } catch(error) {
loader.critical_error("Failed to invoker modal", "Lookup the console for more detail"); loader.critical_error("Failed to invoker modal", "Lookup the console for more detail");
logError(LogCategory.GENERAL,tr("Failed to load modal: %o"), error); logError(LogCategory.GENERAL,tr("Failed to load modal: %o"), error);

View File

@ -55,6 +55,7 @@ export class InternalModalInstance implements ModalInstanceController {
try { try {
this.modalInstance = constructAbstractModalClass(modalClass.default, { windowed: false }, this.constructorArguments); this.modalInstance = constructAbstractModalClass(modalClass.default, { windowed: false }, this.constructorArguments);
this.modalInstance["onInitialize"]();
} catch (error) { } catch (error) {
logError(LogCategory.GENERAL, tr("Failed to create new modal of instance type %s: %o"), this.modalKlass.modalId, error); logError(LogCategory.GENERAL, tr("Failed to create new modal of instance type %s: %o"), this.modalKlass.modalId, error);
throw tr("failed to create new modal instance"); throw tr("failed to create new modal instance");
@ -153,11 +154,11 @@ export class InternalModalInstance implements ModalInstanceController {
this.events.destroy(); this.events.destroy();
} }
private getCloseCallback() { protected getCloseCallback() {
return () => this.events.fire("action_close"); return () => this.events.fire("action_close");
} }
private getPopoutCallback() { protected getPopoutCallback() {
if(!this.modalKlass.popoutSupported) { if(!this.modalKlass.popoutSupported) {
return undefined; return undefined;
} }
@ -169,7 +170,7 @@ export class InternalModalInstance implements ModalInstanceController {
return () => this.events.fire("action_popout"); return () => this.events.fire("action_popout");
} }
private getMinimizeCallback() { protected getMinimizeCallback() {
/* We can't minimize any windows */ /* We can't minimize any windows */
return undefined; return undefined;
} }

View File

@ -15,8 +15,15 @@ export const ServerTag = React.memo((props: {
serverName: string, serverName: string,
handlerId: string, handlerId: string,
serverUniqueId?: string, serverUniqueId?: string,
className?: string className?: string,
style?: EntryTagStyle
}) => { }) => {
let style = props.style || "normal";
if(style === "text-only") {
return <React.Fragment key={"text-only"}>{props.serverName}</React.Fragment>;
}
return ( return (
<div <div
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)} className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}

View File

@ -62,22 +62,22 @@ class ManifestGenerator {
} }
for(const module of compilation.chunkGraph.getChunkModules(chunk)) { for(const module of compilation.chunkGraph.getChunkModules(chunk)) {
const moduleId = compilation.chunkGraph.getModuleId(module); const identifier = module.identifier();
if(typeof moduleId === "string" && moduleId.startsWith("svg-sprites/")) { if(typeof identifier === "string" && identifier.startsWith("svg-sprites/")) {
/* custom svg sprite handler */ /* custom svg sprite handler */
modules.push({ modules.push({
id: module.id, id: module.id,
context: "svg-sprites", context: "svg-sprites",
resource: moduleId.substring("svg-sprites/".length) resource: identifier.substring("svg-sprites/".length)
}); });
continue; continue;
} }
if(!module.type.startsWith("javascript/")) { if(!module.context) {
continue; continue;
} }
if(!module.context) { if(!module.type.startsWith("javascript/")) {
continue; continue;
} }
@ -94,6 +94,7 @@ class ManifestGenerator {
throw "invalid context/resource relation (" + module.context + " <-> " + path.dirname(module.resource) + ")"; throw "invalid context/resource relation (" + module.context + " <-> " + path.dirname(module.resource) + ")";
} }
const moduleId = compilation.chunkGraph.getModuleId(module);
modules.push({ modules.push({
id: moduleId, id: moduleId,
context: path.relative(this.options.context, module.context).replace(/\\/g, "/"), context: path.relative(this.options.context, module.context).replace(/\\/g, "/"),