TeaWeb/shared/js/ui/modal/ModalGroupCreate.tsx

357 lines
15 KiB
TypeScript
Raw Normal View History

2021-02-09 10:11:40 +01:00
import {spawnReactModal} from "tc-shared/ui/react-elements/modal";
2020-06-15 16:56:05 +02:00
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
import {FlatInputField, Select} from "tc-shared/ui/react-elements/InputField";
2020-06-15 16:56:05 +02:00
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} from "tc-shared/connection/ServerConnectionDeclaration";
2020-06-15 16:56:05 +02:00
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {tra} from "tc-shared/i18n/localize";
import {ErrorCode} from "tc-shared/connection/ErrorCode";
import {LogCategory, logError} from "tc-shared/log";
import {InternalModal} from "tc-shared/ui/react-elements/modal/Definitions";
2020-06-15 16:56:05 +02:00
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<Select>();
2020-06-15 16:56:05 +02:00
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 (
<Select
2020-06-15 16:56:05 +02:00
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>
</Select>
2020-06-15 16:56:05 +02:00
)
};
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<Select>();
2020-06-15 16:56:05 +02:00
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_react("action_set_source", {
2020-06-15 16:56:05 +02:00
group: (exitingGroups as GroupInfo[]).findIndex(e => e.id === props.defaultSource) === -1 ? 0 : props.defaultSource
});
return (
<Select
2020-06-15 16:56:05 +02:00
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>
</Select>
2020-06-15 16:56:05 +02:00
)
};
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 InternalModal {
2020-06-15 16:56:05 +02:00
readonly target: "server" | "channel";
readonly events: Registry<GroupCreateModalEvents>;
2020-06-15 16:56:05 +02:00
readonly defaultSourceGroup: number;
constructor(connection: ConnectionHandler, events: Registry<GroupCreateModalEvents>, target: "server" | "channel", defaultSourceGroup: number) {
2020-06-15 16:56:05 +02:00
super();
this.events = events;
2020-06-15 16:56:05 +02:00
this.defaultSourceGroup = defaultSourceGroup;
this.target = target;
initializeGroupCreateController(connection, this.events, this.target);
}
protected onInitialize() {
this.events.fire_react("query_available_groups");
this.events.fire_react("query_client_permissions");
}
2020-06-15 16:56:05 +02:00
protected onDestroy() {
this.events.fire("notify_destroy");
2020-06-15 16:56:05 +02:00
}
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>;
}
2021-01-17 23:11:21 +01:00
renderTitle() {
2020-07-17 23:56:20 +02:00
return this.target === "server" ? <Translatable>Create a new server group</Translatable> : <Translatable>Create a new channel group</Translatable>;
2020-06-15 16:56:05 +02:00
}
}
export function spawnGroupCreate(connection: ConnectionHandler, target: "server" | "channel", sourceGroup: number = 0) {
const events = new Registry<GroupCreateModalEvents>();
events.enableDebug("group-create");
const modal = spawnReactModal(ModalGroupCreate, connection, events, target, sourceGroup);
2020-06-15 16:56:05 +02:00
modal.show();
events.on(["action_cancel", "action_create"], () => modal.destroy());
2020-06-15 16:56:05 +02:00
}
const stringifyError = error => {
if(error instanceof CommandResult) {
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS)
2020-06-15 16:56:05 +02:00
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_react("query_available_groups_result", {
2020-06-15 16:56:05 +02:00
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_react("notify_client_permissions", {
2020-06-15 16:56:05 +02:00
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(target + "groupadd", {
2020-06-15 16:56:05 +02:00
name: event.name,
type: event.target === "query" ? 2 : event.target === "template" ? 0 : 1
});
} else {
/* group copy */
promise = connection.serverConnection.send_command(target + "groupcopy", {
2020-06-15 16:56:05 +02:00
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 === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
2020-06-15 16:56:05 +02:00
createErrorModal(tr("Failed to create group"),
tra("Failed to create group.\nMissing permission {}", connection.permissions.getFailedPermission(error))).open();
2020-06-15 16:56:05 +02:00
return;
}
logError(LogCategory.GENERAL, tr("Failed to create group: %o"), error);
2020-06-15 16:56:05 +02:00
createErrorModal(tr("Failed to create group"),
tra("Failed to create group.\n{}", stringifyError(error))).open();
});
});
}