349 lines
No EOL
15 KiB
TypeScript
349 lines
No EOL
15 KiB
TypeScript
import {Modal, spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
|
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
|
import {Registry} from "tc-shared/events";
|
|
import {FlatInputField, FlatSelect} from "tc-shared/ui/react-elements/InputField";
|
|
import * as React from "react";
|
|
import {useEffect, useRef, useState} from "react";
|
|
import {GroupType} from "tc-shared/permission/GroupManager";
|
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
|
import PermissionType from "tc-shared/permission/PermissionType";
|
|
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
|
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
|
import {tra} from "tc-shared/i18n/localize";
|
|
|
|
const cssStyle = require("./ModalGroupCreate.scss");
|
|
|
|
export type GroupInfo = {
|
|
id: number,
|
|
name: string,
|
|
type: "query" | "template" | "normal"
|
|
};
|
|
|
|
export interface GroupCreateModalEvents {
|
|
action_set_name: { name: string | undefined },
|
|
action_set_type: { target: "query" | "template" | "normal" },
|
|
action_set_source: { group: number },
|
|
|
|
action_cancel: {},
|
|
action_create: {
|
|
name: string,
|
|
target: "query" | "template" | "normal",
|
|
source: number /* if zero than no template */
|
|
}
|
|
|
|
query_available_groups: { },
|
|
query_available_groups_result: {
|
|
groups: GroupInfo[]
|
|
},
|
|
|
|
query_client_permissions: {},
|
|
notify_client_permissions: {
|
|
createTemplateGroup: boolean,
|
|
createQueryGroup: boolean
|
|
},
|
|
|
|
notify_destroy: {}
|
|
}
|
|
|
|
const GroupNameInput = (props: { events: Registry<GroupCreateModalEvents>, defaultSource: number }) => {
|
|
const [ initialLoad, setInitialLoad ] = useState(true);
|
|
const [ existingGroups, setExistingGroups ] = useState<"loading" | GroupInfo[]>("loading");
|
|
const [ selectedType, setSelectedType ] = useState<"query" | "template" | "normal" | "loading">("loading");
|
|
|
|
const refInput = useRef<FlatInputField>();
|
|
|
|
useEffect(() => {
|
|
if(!initialLoad || !refInput.current)
|
|
return;
|
|
|
|
if(selectedType === "loading" || existingGroups === "loading")
|
|
return;
|
|
|
|
setInitialLoad(false);
|
|
refInput.current.focus();
|
|
|
|
const defaultGroup = existingGroups.find(e => e.id === props.defaultSource);
|
|
if(defaultGroup) {
|
|
let name = defaultGroup.name + " (" + tr("Copy") + ")";
|
|
let index = 1;
|
|
while(existingGroups.findIndex(e => e.name === name) !== -1)
|
|
name = defaultGroup.name + " (" + tr("Copy")+ " " + index++ + ")";
|
|
|
|
refInput.current.setValue(name);
|
|
props.events.fire("action_set_name", { name: updateGroupNameState(name) ? name : undefined });
|
|
}
|
|
});
|
|
|
|
const updateGroupNameState = (input: string) => {
|
|
if(!refInput.current)
|
|
return false;
|
|
|
|
if(input.length === 0 || input.length > 30) {
|
|
refInput.current.setState({ isInvalid: true, invalidMessage: tr("Invalid group name length") });
|
|
return false;
|
|
}
|
|
|
|
if(existingGroups === "loading")
|
|
return false;
|
|
|
|
if(existingGroups.findIndex(e => e.name === input && e.type === selectedType) !== -1) {
|
|
refInput.current.setState({ isInvalid: true, invalidMessage: tr("A group with this name already exists") });
|
|
return false;
|
|
}
|
|
|
|
refInput.current.setState({ isInvalid: false });
|
|
return true;
|
|
};
|
|
|
|
props.events.reactUse("query_available_groups_result", event => setExistingGroups(event.groups));
|
|
props.events.reactUse("action_set_type", event => setSelectedType(event.target));
|
|
|
|
return (
|
|
<FlatInputField
|
|
ref={refInput}
|
|
label={<Translatable>Group name</Translatable>}
|
|
finishOnEnter={true}
|
|
|
|
disabled={existingGroups === "loading" || selectedType === "loading"}
|
|
placeholder={existingGroups === "loading" || selectedType === "loading" ? tr("loading data...") : undefined}
|
|
onInput={() => props.events.fire("action_set_name", { name: updateGroupNameState(refInput.current.value()) ? refInput.current.value() : undefined })}
|
|
onBlur={() => props.events.fire("action_set_name", { name: updateGroupNameState(refInput.current.value()) ? refInput.current.value() : undefined })}
|
|
/>
|
|
)
|
|
};
|
|
|
|
const GroupTypeSelector = (props: { events: Registry<GroupCreateModalEvents> }) => {
|
|
const [ selectedType, setSelectedType ] = useState<"query" | "template" | "normal" | "loading">("loading");
|
|
const [ permissions, setPermissions ] = useState<"loading" | { createTemplate, createQuery }>("loading");
|
|
const refSelect = useRef<FlatSelect>();
|
|
|
|
props.events.reactUse("notify_client_permissions", event => {
|
|
setPermissions({
|
|
createQuery: event.createQueryGroup,
|
|
createTemplate: event.createTemplateGroup
|
|
});
|
|
|
|
/* the default type */
|
|
props.events.fire("action_set_type", { target: "normal" });
|
|
});
|
|
|
|
props.events.reactUse("action_set_type", event => setSelectedType(event.target));
|
|
|
|
return (
|
|
<FlatSelect
|
|
ref={refSelect}
|
|
label={<Translatable>Target group type</Translatable>}
|
|
className={cssStyle.groupType}
|
|
disabled={permissions === "loading"}
|
|
value={selectedType}
|
|
onChange={event => event.target.value !== "loading" && props.events.fire("action_set_type", { target: event.target.value as any })}
|
|
>
|
|
<option className={cssStyle.hiddenOption} value={"loading"}>{tr("loading...")}</option>
|
|
<option
|
|
value={"query"}
|
|
disabled={permissions === "loading" || !permissions.createQuery}
|
|
>{tr("Query group")}</option>
|
|
<option
|
|
value={"template"}
|
|
disabled={permissions === "loading" || !permissions.createTemplate}
|
|
>{tr("Template group")}</option>
|
|
<option
|
|
value={"normal"}
|
|
>{tr("Regular group")}</option>
|
|
</FlatSelect>
|
|
)
|
|
};
|
|
|
|
const SourceGroupSelector = (props: { events: Registry<GroupCreateModalEvents>, defaultSource: number }) => {
|
|
const [ selectedGroup, setSelectedGroup ] = useState(undefined);
|
|
const [ permissions, setPermissions ] = useState<"loading" | { createTemplate, createQuery }>("loading");
|
|
const [ exitingGroups, setExitingGroups ] = useState<"loading" | GroupInfo[]>("loading");
|
|
|
|
const refSelect = useRef<FlatSelect>();
|
|
|
|
props.events.reactUse("notify_client_permissions", event => setPermissions({
|
|
createQuery: event.createQueryGroup,
|
|
createTemplate: event.createTemplateGroup
|
|
}));
|
|
props.events.reactUse("query_available_groups_result", event => setExitingGroups(event.groups));
|
|
props.events.reactUse("action_set_source", event => setSelectedGroup(event.group));
|
|
|
|
const groupName = (group: GroupInfo) => {
|
|
let prefix = group.type === "template" ? "[T] " : group.type === "query" ? "[Q] " : "";
|
|
return prefix + group.name + " (" + group.id + ")";
|
|
};
|
|
|
|
const isLoading = exitingGroups === "loading" || permissions === "loading";
|
|
if(!isLoading && selectedGroup === undefined)
|
|
props.events.fire_async("action_set_source", {
|
|
group: (exitingGroups as GroupInfo[]).findIndex(e => e.id === props.defaultSource) === -1 ? 0 : props.defaultSource
|
|
});
|
|
|
|
return (
|
|
<FlatSelect
|
|
ref={refSelect}
|
|
label={<Translatable>Create group using this template</Translatable>}
|
|
className={cssStyle.groupSource}
|
|
disabled={isLoading}
|
|
value={isLoading || selectedGroup === undefined ? "-1" : selectedGroup.toString()}
|
|
onChange={event => props.events.fire("action_set_source", { group: parseInt(event.target.value) })}
|
|
>
|
|
<option className={cssStyle.hiddenOption} value={"-1"}>{tr("loading...")}</option>
|
|
<option value={"0"} onSelect={() => props.events.fire("action_set_source", { group: 0 })}>{tr("No template")}</option>
|
|
<optgroup label={tr("Query groups")} className={permissions === "loading" || !permissions.createQuery ? cssStyle.hiddenOption : ""} >
|
|
{exitingGroups === "loading" ? undefined :
|
|
exitingGroups.filter(e => e.type === "query").map(e => (
|
|
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
|
))
|
|
}
|
|
</optgroup>
|
|
<optgroup label={tr("Template groups")} className={permissions === "loading" || !permissions.createTemplate ? cssStyle.hiddenOption : ""} >
|
|
{exitingGroups === "loading" ? undefined :
|
|
exitingGroups.filter(e => e.type === "template").map(e => (
|
|
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
|
))
|
|
}
|
|
</optgroup>
|
|
<optgroup label={tr("Regular Groups")} >
|
|
{exitingGroups === "loading" ? undefined :
|
|
exitingGroups.filter(e => e.type === "normal").map(e => (
|
|
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
|
))
|
|
}
|
|
</optgroup>
|
|
</FlatSelect>
|
|
)
|
|
};
|
|
|
|
const CreateButton = (props: { events: Registry<GroupCreateModalEvents> }) => {
|
|
const [ sourceGroup, setSourceGroup ] = useState<number | undefined>(undefined);
|
|
const [ groupType, setGroupType ] = useState<"query" | "template" | "normal" | undefined>(undefined);
|
|
const [ groupName, setGroupName ] = useState<string | undefined>(undefined);
|
|
|
|
props.events.reactUse("action_set_name", event => setGroupName(event.name));
|
|
props.events.reactUse("action_set_type", event => setGroupType(event.target));
|
|
props.events.reactUse("action_set_source", event => setSourceGroup(event.group));
|
|
|
|
return <Button color={"green"} disabled={sourceGroup === undefined || groupType === undefined || groupName === undefined} onClick={() => {
|
|
props.events.fire("action_create", { name: groupName, source: sourceGroup, target: groupType });
|
|
}}>
|
|
<Translatable>Create Group</Translatable>
|
|
</Button>
|
|
};
|
|
|
|
class ModalGroupCreate extends Modal {
|
|
readonly target: "server" | "channel";
|
|
readonly events = new Registry<GroupCreateModalEvents>();
|
|
readonly defaultSourceGroup: number;
|
|
|
|
constructor(connection: ConnectionHandler, target: "server" | "channel", defaultSourceGroup: number) {
|
|
super();
|
|
|
|
this.events.enable_debug("group-create");
|
|
this.defaultSourceGroup = defaultSourceGroup;
|
|
this.target = target;
|
|
initializeGroupCreateController(connection, this.events, this.target);
|
|
}
|
|
|
|
protected onInitialize() {
|
|
this.modalController().events.on("destroy", () => this.events.fire("notify_destroy"));
|
|
|
|
this.events.fire_async("query_available_groups");
|
|
this.events.fire_async("query_client_permissions");
|
|
|
|
this.events.on(["action_cancel", "action_create"], () => this.modalController().destroy());
|
|
}
|
|
|
|
renderBody() {
|
|
return <div className={cssStyle.container}>
|
|
<GroupNameInput events={this.events} defaultSource={this.defaultSourceGroup} />
|
|
<div className={cssStyle.row}>
|
|
<GroupTypeSelector events={this.events} />
|
|
<SourceGroupSelector events={this.events} defaultSource={this.defaultSourceGroup} />
|
|
</div>
|
|
<div className={cssStyle.buttons}>
|
|
<Button color={"red"} onClick={() => this.events.fire("action_cancel")}><Translatable>Cancel</Translatable></Button>
|
|
<CreateButton events={this.events} />
|
|
</div>
|
|
</div>;
|
|
}
|
|
|
|
title(): string {
|
|
return this.target === "server" ? tr("Create a new server group") : tr("Create a new channel group");
|
|
}
|
|
|
|
}
|
|
|
|
export function spawnGroupCreate(connection: ConnectionHandler, target: "server" | "channel", sourceGroup: number = 0) {
|
|
const modal = spawnReactModal(ModalGroupCreate, connection, target, sourceGroup);
|
|
modal.show();
|
|
}
|
|
|
|
const stringifyError = error => {
|
|
if(error instanceof CommandResult) {
|
|
if(error.id === ErrorID.PERMISSION_ERROR)
|
|
return tr("insufficient permissions");
|
|
else
|
|
return error.message + (error.extra_message ? " (" + error.extra_message + ")" : "");
|
|
} else if(error instanceof Error) {
|
|
return error.message;
|
|
} else if(typeof error !== "string") {
|
|
return tr("Lookup the console");
|
|
}
|
|
return error;
|
|
};
|
|
|
|
function initializeGroupCreateController(connection: ConnectionHandler, events: Registry<GroupCreateModalEvents>, target: "server" | "channel") {
|
|
events.on("query_available_groups", event => {
|
|
const groups = target === "server" ? connection.groups.serverGroups : connection.groups.channelGroups;
|
|
|
|
events.fire_async("query_available_groups_result", {
|
|
groups: groups.map(e => {
|
|
return {
|
|
name: e.name,
|
|
id: e.id,
|
|
type: e.type === GroupType.TEMPLATE ? "template" : e.type === GroupType.QUERY ? "query" : "normal"
|
|
}
|
|
})
|
|
});
|
|
});
|
|
|
|
const notifyClientPermissions = () => events.fire_async("notify_client_permissions", {
|
|
createQueryGroup: connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1),
|
|
createTemplateGroup: connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1)
|
|
});
|
|
events.on("query_client_permissions", notifyClientPermissions);
|
|
events.on("notify_destroy", connection.permissions.events.on("client_permissions_changed", notifyClientPermissions));
|
|
|
|
events.on("action_create", event => {
|
|
let promise: Promise<CommandResult>;
|
|
if(event.source <= 0) {
|
|
/* real group create */
|
|
promise = connection.serverConnection.send_command("servergroupadd", {
|
|
name: event.name,
|
|
type: event.target === "query" ? 2 : event.target === "template" ? 0 : 1
|
|
});
|
|
} else {
|
|
/* group copy */
|
|
promise = connection.serverConnection.send_command("servergroupcopy", {
|
|
ssgid: event.source,
|
|
name: event.name,
|
|
type: event.target === "query" ? 2 : event.target === "template" ? 0 : 1
|
|
});
|
|
}
|
|
promise.then(() => {
|
|
createInfoModal(tr("Group has been created"), tr("The group has been successfully created.")).open();
|
|
}).catch(error => {
|
|
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) {
|
|
createErrorModal(tr("Failed to create group"),
|
|
tra("Failed to create group.\nMissing permission {}", connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknwon"))).open();
|
|
return;
|
|
}
|
|
|
|
console.warn(tr("Failed to create group: %o"), error);
|
|
createErrorModal(tr("Failed to create group"),
|
|
tra("Failed to create group.\n{}", stringifyError(error))).open();
|
|
});
|
|
});
|
|
} |