Reworked the server group assignment modal
parent
f3f5fd0618
commit
582b93852b
|
@ -1,6 +1,9 @@
|
|||
# Changelog:
|
||||
* **21.03.21**
|
||||
- Reworked the server group assignment modal. It now better reacts to the user input as well is now popoutable
|
||||
|
||||
* **18.03.21**
|
||||
- Finally got fully rid of the initial backend glue and changes all systems to provider
|
||||
- Finally, got fully rid of the initial backend glue and changes all systems to provider
|
||||
|
||||
* **17.03.21**
|
||||
- Updated from webpack 4 to webpack 5
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
export class Mutex<T> {
|
||||
private value: T;
|
||||
private taskExecuting = false;
|
||||
private taskQueue = [];
|
||||
|
||||
constructor(value: T) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
execute<R>(callback: (value: T, setValue: (newValue: T) => void) => R | Promise<R>) : Promise<R> {
|
||||
return new Promise<R>((resolve, reject) => {
|
||||
this.taskQueue.push(() => new Promise(taskResolve => {
|
||||
try {
|
||||
const result = callback(this.value, newValue => this.value = newValue);
|
||||
if(result instanceof Promise) {
|
||||
result.then(result => {
|
||||
taskResolve();
|
||||
resolve(result);
|
||||
}).catch(error => {
|
||||
taskResolve();
|
||||
reject(error);
|
||||
});
|
||||
} else {
|
||||
taskResolve();
|
||||
resolve(result);
|
||||
}
|
||||
} catch (error) {
|
||||
taskResolve();
|
||||
reject(error);
|
||||
}
|
||||
}));
|
||||
|
||||
if(!this.taskExecuting) {
|
||||
this.executeNextTask();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async tryExecute<R>(callback: (value: T, setValue: (newValue: T) => void) => R | Promise<R>) : Promise<{ status: "success", result: R } | { status: "would-block" }> {
|
||||
if(!this.taskExecuting) {
|
||||
return {
|
||||
status: "success",
|
||||
result: await this.execute(callback)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
status: "would-block"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private executeNextTask() {
|
||||
const task = this.taskQueue.pop_front();
|
||||
if(typeof task === "undefined") {
|
||||
this.taskExecuting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
this.taskExecuting = true;
|
||||
task().then(() => this.executeNextTask());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
|
||||
export type ClientInfoResult = {
|
||||
status: "success",
|
||||
|
||||
clientName: string,
|
||||
clientUniqueId: string,
|
||||
clientDatabaseId: number,
|
||||
} | {
|
||||
status: "not-found"
|
||||
} | {
|
||||
status: "error",
|
||||
error: string
|
||||
};
|
||||
|
||||
type PendingInfoRequest = {
|
||||
promise: Promise<ClientInfoResult>,
|
||||
resolve: (result: ClientInfoResult) => void,
|
||||
fullFilled: boolean,
|
||||
};
|
||||
|
||||
type ClientInfo = {
|
||||
clientName: string,
|
||||
clientUniqueId: string,
|
||||
clientDatabaseId: number,
|
||||
};
|
||||
|
||||
export class ClientInfoResolver {
|
||||
private readonly handler: ConnectionHandler;
|
||||
private readonly requestDatabaseIds: { [key: number]: PendingInfoRequest };
|
||||
private readonly requestUniqueIds: { [key: string]: PendingInfoRequest };
|
||||
|
||||
private executed: boolean;
|
||||
|
||||
constructor(handler: ConnectionHandler) {
|
||||
this.handler = handler;
|
||||
this.executed = false;
|
||||
|
||||
this.requestDatabaseIds = {};
|
||||
this.requestUniqueIds = {};
|
||||
}
|
||||
|
||||
private registerRequest(type: "database-id", key: number) : Promise<ClientInfoResult>;
|
||||
private registerRequest(type: "unique-id", key: string) : Promise<ClientInfoResult>;
|
||||
private registerRequest(type, key) : Promise<ClientInfoResult> {
|
||||
let targetObject;
|
||||
switch (type) {
|
||||
case "database-id":
|
||||
targetObject = this.requestDatabaseIds;
|
||||
break;
|
||||
|
||||
case "unique-id":
|
||||
targetObject = this.requestUniqueIds;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if(this.executed) {
|
||||
throw tr("request already executed");
|
||||
}
|
||||
|
||||
if(typeof targetObject[key] === "undefined") {
|
||||
const handle: PendingInfoRequest = {
|
||||
fullFilled: false,
|
||||
|
||||
resolve: undefined,
|
||||
promise: undefined,
|
||||
};
|
||||
|
||||
handle.promise = new Promise<ClientInfoResult>(resolve => handle.resolve = resolve);
|
||||
targetObject[key] = handle;
|
||||
}
|
||||
|
||||
return targetObject[key].promise;
|
||||
}
|
||||
|
||||
private fullFullAllRequests(type: "database-id" | "unique-id", result: ClientInfoResult) {
|
||||
let targetObject;
|
||||
switch (type) {
|
||||
case "database-id":
|
||||
targetObject = this.requestDatabaseIds;
|
||||
break;
|
||||
|
||||
case "unique-id":
|
||||
targetObject = this.requestUniqueIds;
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
Object.keys(targetObject).forEach(key => {
|
||||
if(targetObject[key].fullFilled) {
|
||||
return;
|
||||
}
|
||||
|
||||
targetObject[key].fullFilled = true;
|
||||
targetObject[key].resolve(result);
|
||||
});
|
||||
}
|
||||
|
||||
private static parseClientInfo(json: any[]) : ClientInfo[] {
|
||||
const result: ClientInfo[] = [];
|
||||
|
||||
let index = 0;
|
||||
for(const entry of json) {
|
||||
index++;
|
||||
|
||||
if(typeof entry["cluid"] === "undefined") {
|
||||
logWarn(LogCategory.NETWORKING, tr("Missing client unique id in client info result bulk %d."), index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(typeof entry["clname"] === "undefined") {
|
||||
logWarn(LogCategory.NETWORKING, tr("Missing client name in client info result bulk %d."), index);
|
||||
continue;
|
||||
}
|
||||
|
||||
if(typeof entry["cldbid"] === "undefined") {
|
||||
logWarn(LogCategory.NETWORKING, tr("Missing client database id in client info result bulk %d."), index);
|
||||
continue;
|
||||
}
|
||||
|
||||
const databaseId = parseInt(entry["cldbid"]);
|
||||
if(isNaN(databaseId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Client database id (%s) in client info isn't parse able as integer in bulk %d."), entry["cldbid"], index);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({ clientName: entry["clname"], clientUniqueId: entry["cluid"], clientDatabaseId: databaseId });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
getInfoByDatabaseId(databaseId: number) : Promise<ClientInfoResult> {
|
||||
return this.registerRequest("database-id", databaseId);
|
||||
}
|
||||
|
||||
getInfoByUniqueId(uniqueId: string) : Promise<ClientInfoResult> {
|
||||
return this.registerRequest("unique-id", uniqueId);
|
||||
}
|
||||
|
||||
async executeQueries() {
|
||||
this.executed = true;
|
||||
let promises = [];
|
||||
let handlers = [];
|
||||
|
||||
try {
|
||||
const requestDatabaseIds = Object.keys(this.requestDatabaseIds);
|
||||
if(requestDatabaseIds.length > 0) {
|
||||
handlers.push(this.handler.serverConnection.command_handler_boss().register_explicit_handler("notifyclientgetnamefromdbid", command => {
|
||||
ClientInfoResolver.parseClientInfo(command.arguments).forEach(info => {
|
||||
if(this.requestDatabaseIds[info.clientDatabaseId].fullFilled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestDatabaseIds[info.clientDatabaseId].fullFilled = true;
|
||||
this.requestDatabaseIds[info.clientDatabaseId].resolve({
|
||||
status: "success",
|
||||
|
||||
clientName: info.clientName,
|
||||
clientDatabaseId: info.clientDatabaseId,
|
||||
clientUniqueId: info.clientUniqueId
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
promises.push(this.handler.serverConnection.send_command("clientgetnamefromdbid",
|
||||
requestDatabaseIds.map(entry => ({ cldbid: entry })),
|
||||
{
|
||||
process_result: false,
|
||||
}
|
||||
).catch(error => {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to resolve client info from database id: %o"), error);
|
||||
error = tr("lookup the console");
|
||||
}
|
||||
|
||||
this.fullFullAllRequests("database-id", {
|
||||
status: "error",
|
||||
error: error
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const requestUniqueIds = Object.keys(this.requestUniqueIds);
|
||||
if(requestUniqueIds.length > 0) {
|
||||
handlers.push(this.handler.serverConnection.command_handler_boss().register_explicit_handler("notifyclientnamefromuid", command => {
|
||||
ClientInfoResolver.parseClientInfo(command.arguments).forEach(info => {
|
||||
if(this.requestUniqueIds[info.clientUniqueId].fullFilled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.requestUniqueIds[info.clientUniqueId].fullFilled = true;
|
||||
this.requestUniqueIds[info.clientUniqueId].resolve({
|
||||
status: "success",
|
||||
|
||||
clientName: info.clientName,
|
||||
clientDatabaseId: info.clientDatabaseId,
|
||||
clientUniqueId: info.clientUniqueId
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
promises.push(this.handler.serverConnection.send_command("clientgetnamefromuid",
|
||||
requestUniqueIds.map(entry => ({ cluid: entry })),
|
||||
{
|
||||
process_result: false,
|
||||
}
|
||||
).catch(error => {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
return;
|
||||
}
|
||||
|
||||
error = error.formattedMessage();
|
||||
} else if(typeof error !== "string") {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to resolve client info from unique id: %o"), error);
|
||||
error = tr("lookup the console");
|
||||
}
|
||||
|
||||
this.fullFullAllRequests("unique-id", {
|
||||
status: "error",
|
||||
error: error
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
this.fullFullAllRequests("unique-id", { status: "not-found" });
|
||||
this.fullFullAllRequests("database-id", { status: "not-found" });
|
||||
} finally {
|
||||
handlers.forEach(callback => callback());
|
||||
|
||||
this.fullFullAllRequests("unique-id", {
|
||||
status: "error",
|
||||
error: tr("request failed")
|
||||
});
|
||||
|
||||
this.fullFullAllRequests("database-id", {
|
||||
status: "error",
|
||||
error: tr("request failed")
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ import * as htmltags from "../ui/htmltags";
|
|||
import {CommandResult} from "../connection/ServerConnectionDeclaration";
|
||||
import {ChannelEntry} from "./Channel";
|
||||
import {ConnectionHandler, ViewReasonId} from "../ConnectionHandler";
|
||||
import {createServerGroupAssignmentModal} from "../ui/modal/ModalGroupAssignment";
|
||||
import {openClientInfo} from "../ui/modal/ModalClientInfo";
|
||||
import {spawnBanClient} from "../ui/modal/ModalBanClient";
|
||||
import {spawnChangeLatency} from "../ui/modal/ModalChangeLatency";
|
||||
|
@ -31,6 +30,7 @@ import {VideoClient} from "tc-shared/connection/VideoConnection";
|
|||
import { tr } from "tc-shared/i18n/localize";
|
||||
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
||||
import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
||||
import {spawnServerGroupAssignments} from "tc-shared/ui/modal/group-assignment/Controller";
|
||||
|
||||
export enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
|
@ -472,30 +472,7 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
|
|||
}
|
||||
|
||||
open_assignment_modal() {
|
||||
createServerGroupAssignmentModal(this as any, (groups, flag) => {
|
||||
if(groups.length == 0) return Promise.resolve(true);
|
||||
|
||||
if(groups.length == 1) {
|
||||
if(flag) {
|
||||
return this.channelTree.client.serverConnection.send_command("servergroupaddclient", {
|
||||
sgid: groups[0],
|
||||
cldbid: this.properties.client_database_id
|
||||
}).then(() => true);
|
||||
} else
|
||||
return this.channelTree.client.serverConnection.send_command("servergroupdelclient", {
|
||||
sgid: groups[0],
|
||||
cldbid: this.properties.client_database_id
|
||||
}).then(() => true);
|
||||
} else {
|
||||
const data = groups.map(e => { return {sgid: e}; });
|
||||
data[0]["cldbid"] = this.properties.client_database_id;
|
||||
|
||||
if(flag) {
|
||||
return this.channelTree.client.serverConnection.send_command("clientaddservergroup", data, {flagset: ["continueonerror"]}).then(() => true);
|
||||
} else
|
||||
return this.channelTree.client.serverConnection.send_command("clientdelservergroup", data, {flagset: ["continueonerror"]}).then(() => true);
|
||||
}
|
||||
});
|
||||
spawnServerGroupAssignments(this.channelTree.client, this.properties.client_database_id);
|
||||
}
|
||||
|
||||
open_text_chat() {
|
||||
|
|
|
@ -0,0 +1,409 @@
|
|||
import {Registry} from "tc-events";
|
||||
import {
|
||||
ModalClientGroupAssignmentEvents,
|
||||
ModalClientGroupAssignmentVariables
|
||||
} from "tc-shared/ui/modal/group-assignment/Definitions";
|
||||
import {createIpcUiVariableProvider, IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {ClientInfoResolver, ClientInfoResult} from "tc-shared/connection/ClientInfo";
|
||||
import {GroupManager, GroupType} from "tc-shared/permission/GroupManager";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {spawnModal} from "tc-shared/ui/react-elements/modal";
|
||||
import {Mutex} from "tc-shared/Mutex";
|
||||
import {LogCategory, logError, logWarn} from "tc-shared/log";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ErrorCode} from "tc-shared/connection/ErrorCode";
|
||||
import {ModalController} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
|
||||
type AssignedGroups = {
|
||||
status: "unloaded"
|
||||
} | {
|
||||
status: "error",
|
||||
error: string
|
||||
} | {
|
||||
status: "success",
|
||||
groups: number[]
|
||||
};
|
||||
|
||||
const generateUniqueId = (handler: ConnectionHandler, clientDatabaseId: number) => "server-group-assignments-" + handler.handlerId + " - " + clientDatabaseId;
|
||||
|
||||
class Controller {
|
||||
readonly events: Registry<ModalClientGroupAssignmentEvents>;
|
||||
readonly variables: IpcUiVariableProvider<ModalClientGroupAssignmentVariables>;
|
||||
|
||||
readonly handler: ConnectionHandler;
|
||||
readonly clientDatabaseId: number;
|
||||
|
||||
private clientInfoTimeout: number;
|
||||
private clientInfoPromise: Promise<ClientInfoResult>;
|
||||
|
||||
private registeredListener: (() => void)[];
|
||||
private resendAvailableGroups = false;
|
||||
|
||||
private assignedGroups: Mutex<AssignedGroups>;
|
||||
private assignmentLoadEnqueued = false;
|
||||
|
||||
constructor(handler: ConnectionHandler, clientDatabaseId: number) {
|
||||
this.handler = handler;
|
||||
this.clientDatabaseId = clientDatabaseId;
|
||||
|
||||
this.events = new Registry<ModalClientGroupAssignmentEvents>();
|
||||
this.variables = createIpcUiVariableProvider();
|
||||
|
||||
this.assignedGroups = new Mutex<AssignedGroups>({ status: "unloaded" });
|
||||
this.registeredListener = [];
|
||||
|
||||
this.variables.setVariableProvider("handlerId", () => this.handler.handlerId);
|
||||
this.variables.setVariableProvider("availableGroups", () => {
|
||||
this.resendAvailableGroups = false;
|
||||
|
||||
const addPermissions = this.handler.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_ADD_POWER);
|
||||
const removePermissions = this.handler.permissions.neededPermission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER);
|
||||
|
||||
const serverGroups = this.handler.groups.serverGroups.slice(0).sort(GroupManager.sorter());
|
||||
return {
|
||||
defaultGroup: this.handler.channelTree.server.properties.virtualserver_default_server_group,
|
||||
groups: serverGroups.filter(entry => entry.type === GroupType.NORMAL).map(entry => ({
|
||||
name: entry.name,
|
||||
groupId: entry.id,
|
||||
saveDB: entry.properties.savedb,
|
||||
|
||||
icon: {
|
||||
iconId: entry.properties.iconid,
|
||||
serverUniqueId: this.handler.getCurrentServerUniqueId(),
|
||||
handlerId: this.handler.handlerId
|
||||
},
|
||||
|
||||
addAble: addPermissions.granted(entry.requiredMemberAddPower),
|
||||
removeAble: removePermissions.granted(entry.requiredMemberRemovePower)
|
||||
}))
|
||||
}
|
||||
});
|
||||
this.variables.setVariableProviderAsync("targetClient", async () => {
|
||||
const result = await this.getClientInfo();
|
||||
switch (result.status) {
|
||||
case "error":
|
||||
return { status: "error", message: result.error };
|
||||
|
||||
case "not-found":
|
||||
return { status: "error", message: tr("not found") };
|
||||
|
||||
case "success":
|
||||
return {
|
||||
status: "success",
|
||||
|
||||
clientName: result.clientName,
|
||||
clientUniqueId: result.clientUniqueId,
|
||||
clientDatabaseId: result.clientDatabaseId
|
||||
};
|
||||
|
||||
default:
|
||||
return { status: "error", message: tr("unknown status") };
|
||||
}
|
||||
});
|
||||
|
||||
this.variables.setVariableProviderAsync("assignedGroupStatus", async () => {
|
||||
const result = await this.assignedGroups.tryExecute<ModalClientGroupAssignmentVariables["assignedGroupStatus"]>(value => {
|
||||
switch (value.status) {
|
||||
case "success":
|
||||
return { status: "loaded", assignedGroups: value.groups.length };
|
||||
|
||||
case "unloaded":
|
||||
return { status: "loading" };
|
||||
|
||||
case "error":
|
||||
default:
|
||||
return { status: "error", message: value.error || tr("invalid status") };
|
||||
}
|
||||
});
|
||||
|
||||
if(result.status === "would-block") {
|
||||
return { status: "loading" };
|
||||
} else {
|
||||
return result.result;
|
||||
}
|
||||
});
|
||||
|
||||
this.variables.setVariableProviderAsync("groupAssigned", groupId => this.assignedGroups.execute(assignedGroups => {
|
||||
switch (assignedGroups.status) {
|
||||
case "success":
|
||||
return assignedGroups.groups.indexOf(groupId) !== -1;
|
||||
|
||||
case "error":
|
||||
case "unloaded":
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}));
|
||||
|
||||
this.variables.setVariableEditorAsync("groupAssigned", async (newValue, groupId) => this.assignedGroups.execute(async assignedGroups => {
|
||||
if(assignedGroups.status !== "success") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if((assignedGroups.groups.indexOf(groupId) === -1) !== newValue) {
|
||||
/* No change to propagate but update the local value */
|
||||
return true;
|
||||
}
|
||||
|
||||
let command, action: "add" | "remove";
|
||||
if(newValue) {
|
||||
action = "add";
|
||||
command = "servergroupaddclient";
|
||||
} else {
|
||||
action = "remove";
|
||||
command = "servergroupdelclient";
|
||||
}
|
||||
|
||||
try {
|
||||
await this.handler.serverConnection.send_command(command, { sgid: groupId, cldbid: this.clientDatabaseId });
|
||||
assignedGroups.groups.toggle(groupId, newValue);
|
||||
this.variables.sendVariable("assignedGroupStatus");
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.SERVER_INSUFFICIENT_PERMISSIONS) {
|
||||
this.events.fire("notify_toggle_result", {
|
||||
action: action,
|
||||
groupId: groupId,
|
||||
groupName: "",
|
||||
result: {
|
||||
status: "no-permissions",
|
||||
permission: this.handler.permissions.getFailedPermission(error)
|
||||
}
|
||||
});
|
||||
return false;
|
||||
} else {
|
||||
this.events.fire("notify_toggle_result", {
|
||||
action: action,
|
||||
groupId: groupId,
|
||||
groupName: "",
|
||||
result: {
|
||||
status: "error",
|
||||
reason: error.formattedMessage()
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
logError(LogCategory.NETWORKING, tr("Failed to toggle client server group %d: %o"), groupId, error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}));
|
||||
|
||||
this.events.on("action_remove_all", () => {
|
||||
this.assignedGroups.execute(async value => {
|
||||
let args = [];
|
||||
|
||||
switch (value.status) {
|
||||
case "success":
|
||||
if(value.groups.length === 0) {
|
||||
return;
|
||||
}
|
||||
args = value.groups.map(entry => ({ sgid: entry }));
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
args[0].cldbid = this.clientDatabaseId;
|
||||
let result: CommandResult;
|
||||
try {
|
||||
result = await this.handler.serverConnection.send_command("servergroupdelclient", args);
|
||||
} catch (error) {
|
||||
if(error instanceof CommandResult) {
|
||||
result = error;
|
||||
} else {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to remote all server groups from target client: %o"), error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const bulks = result.getBulks();
|
||||
if(bulks.length !== args.length) {
|
||||
if(!result.success) {
|
||||
/* result.bulks.length must be one then */
|
||||
logError(LogCategory.NETWORKING, tr("Failed to remote all server groups from target client: %o"), result.formattedMessage());
|
||||
return;
|
||||
} else {
|
||||
/* Server does not send a bulked response. We've to do a full refresh */
|
||||
this.events.fire("action_refresh", { slowMode: false });
|
||||
}
|
||||
} else {
|
||||
let statusUpdated = false;
|
||||
for(let index = 0; index < args.length; index++) {
|
||||
if(!bulks[index].success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
statusUpdated = true;
|
||||
value.groups.remove(args[index].sgid);
|
||||
}
|
||||
|
||||
if(statusUpdated) {
|
||||
this.variables.sendVariable("assignedGroupStatus");
|
||||
this.sendAllGroupStatus();
|
||||
}
|
||||
}
|
||||
}).then(undefined);
|
||||
});
|
||||
|
||||
this.events.on("action_refresh", event => {
|
||||
this.loadAssignedGroups(event.slowMode);
|
||||
this.variables.sendVariable("assignedGroupStatus");
|
||||
});
|
||||
|
||||
this.registeredListener.push(
|
||||
handler.groups.events.on(["notify_groups_created", "notify_groups_deleted", "notify_groups_received", "notify_groups_updated", "notify_reset"], () => this.enqueueGroupResend())
|
||||
);
|
||||
this.registeredListener.push(handler.permissions.register_needed_permission(PermissionType.I_SERVER_GROUP_MEMBER_ADD_POWER, () => this.enqueueGroupResend()));
|
||||
this.registeredListener.push(handler.permissions.register_needed_permission(PermissionType.I_SERVER_GROUP_MEMBER_REMOVE_POWER, () => this.enqueueGroupResend()));
|
||||
this.loadAssignedGroups(false);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.registeredListener.forEach(callback => callback());
|
||||
this.registeredListener = [];
|
||||
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
}
|
||||
|
||||
private sendAllGroupStatus() {
|
||||
this.variables.getVariable("availableGroups", undefined).then(groups => {
|
||||
groups.groups.forEach(group => this.variables.sendVariable("groupAssigned", group.groupId));
|
||||
});
|
||||
}
|
||||
|
||||
private loadAssignedGroups(slowMode: boolean) {
|
||||
if(this.assignmentLoadEnqueued) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.assignmentLoadEnqueued = true;
|
||||
this.assignedGroups.execute(async (assignedGroups, setAssignedGroups) => {
|
||||
if(slowMode) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
}
|
||||
this.assignmentLoadEnqueued = false;
|
||||
|
||||
let resultSet = false;
|
||||
const unregisterCallback = this.handler.serverConnection.command_handler_boss().register_explicit_handler("notifyservergroupsbyclientid", command => {
|
||||
const payload = command.arguments;
|
||||
const clientId = parseInt(payload[0].cldbid);
|
||||
if(isNaN(clientId) || clientId !== this.clientDatabaseId) {
|
||||
return;
|
||||
}
|
||||
|
||||
let groups = [];
|
||||
for(const entry of payload) {
|
||||
const groupId = parseInt(entry.sgid);
|
||||
if(isNaN(groupId)) {
|
||||
logWarn(LogCategory.NETWORKING, tr("Failed to parse sgid as integer of server groups by client id response (%o)."), entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
groups.push(groupId);
|
||||
}
|
||||
|
||||
resultSet = true;
|
||||
setAssignedGroups({ status: "success", groups: groups });
|
||||
});
|
||||
|
||||
try {
|
||||
await this.handler.serverConnection.send_command("servergroupsbyclientid", { cldbid: this.clientDatabaseId });
|
||||
if(!resultSet) {
|
||||
setAssignedGroups({ status: "error", error: tr("missing result") });
|
||||
}
|
||||
} catch (error) {
|
||||
if(!resultSet) {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorCode.DATABASE_EMPTY_RESULT) {
|
||||
setAssignedGroups({ status: "success", groups: [] });
|
||||
} else if(error.id === ErrorCode.CLIENT_INVALID_ID) {
|
||||
setAssignedGroups({ status: "error", error: tr("invalid client id") });
|
||||
} else {
|
||||
setAssignedGroups({ status: "error", error: error.formattedMessage() });
|
||||
}
|
||||
} else {
|
||||
if(typeof error !== "string") {
|
||||
logError(LogCategory.NETWORKING, tr("Failed to query client server groups: %o"), error);
|
||||
error = tr("lookup the console");
|
||||
}
|
||||
|
||||
setAssignedGroups({ status: "error", error: error });
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
unregisterCallback();
|
||||
}
|
||||
}).then(() => {
|
||||
this.variables.sendVariable("assignedGroupStatus");
|
||||
this.sendAllGroupStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private enqueueGroupResend() {
|
||||
if(this.resendAvailableGroups) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resendAvailableGroups = true;
|
||||
this.variables.sendVariable("availableGroups");
|
||||
}
|
||||
|
||||
private getClientInfo() : Promise<ClientInfoResult> {
|
||||
if(typeof this.clientInfoTimeout === "number" && Date.now() < this.clientInfoTimeout) {
|
||||
return this.clientInfoPromise;
|
||||
}
|
||||
|
||||
this.clientInfoTimeout = Date.now() + 5000;
|
||||
return (this.clientInfoPromise = new Promise<ClientInfoResult>(resolve => {
|
||||
const resolver = new ClientInfoResolver(this.handler);
|
||||
resolver.getInfoByDatabaseId(this.clientDatabaseId).then(result => {
|
||||
resolve(result);
|
||||
switch (result.status) {
|
||||
case "success":
|
||||
case "not-found":
|
||||
this.clientInfoTimeout = Date.now() + 60 * 1000;
|
||||
break;
|
||||
|
||||
case "error":
|
||||
default:
|
||||
this.clientInfoTimeout = Date.now() + 5 * 1000;
|
||||
break;
|
||||
}
|
||||
});
|
||||
resolver.executeQueries().then(undefined);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let controllerInstances: {[key: string]: ModalController} = {};
|
||||
export function spawnServerGroupAssignments(handler: ConnectionHandler, targetClientDatabaseId: number) {
|
||||
const uniqueId = generateUniqueId(handler, targetClientDatabaseId);
|
||||
if(typeof controllerInstances[uniqueId] !== "undefined") {
|
||||
controllerInstances[uniqueId].show().then(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new Controller(handler, targetClientDatabaseId);
|
||||
const modal = spawnModal("modal-assign-server-groups", [
|
||||
controller.events.generateIpcDescription(),
|
||||
controller.variables.generateConsumerDescription()
|
||||
], {
|
||||
popoutable: true,
|
||||
uniqueId: uniqueId
|
||||
});
|
||||
controller.events.on("action_close", () => modal.destroy());
|
||||
modal.getEvents().on("destroy", () => {
|
||||
delete controllerInstances[uniqueId];
|
||||
controller.destroy();
|
||||
});
|
||||
|
||||
modal.show().then(undefined);
|
||||
controllerInstances[uniqueId] = modal;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import {RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
|
||||
export type AvailableGroup = {
|
||||
groupId: number,
|
||||
saveDB: boolean,
|
||||
|
||||
name: string,
|
||||
icon: RemoteIconInfo | undefined,
|
||||
|
||||
addAble: boolean,
|
||||
removeAble: boolean,
|
||||
}
|
||||
|
||||
export type ClientInfo = {
|
||||
status: "success",
|
||||
|
||||
clientDatabaseId: number,
|
||||
clientUniqueId: string,
|
||||
clientName: string
|
||||
} | {
|
||||
status: "error",
|
||||
message: string
|
||||
};
|
||||
|
||||
export interface ModalClientGroupAssignmentVariables {
|
||||
readonly handlerId: string,
|
||||
readonly targetClient: ClientInfo,
|
||||
readonly availableGroups: {
|
||||
groups: AvailableGroup[],
|
||||
defaultGroup: number
|
||||
},
|
||||
readonly assignedGroupStatus: { status: "loaded", assignedGroups: number } | { status: "loading" } | { status: "error", message: string };
|
||||
groupAssigned: boolean,
|
||||
}
|
||||
|
||||
export interface ModalClientGroupAssignmentEvents {
|
||||
action_close: {},
|
||||
action_remove_all: {},
|
||||
action_refresh: { slowMode: boolean },
|
||||
|
||||
notify_toggle_result: {
|
||||
action: "add" | "remove",
|
||||
|
||||
groupId: number,
|
||||
groupName: string,
|
||||
|
||||
result: {
|
||||
status: "success"
|
||||
} | {
|
||||
status: "error",
|
||||
reason: string
|
||||
} | {
|
||||
status: "no-permissions",
|
||||
permission: string
|
||||
},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.container {
|
||||
user-select: none;
|
||||
|
||||
min-width: 20em;
|
||||
width: 30em;
|
||||
|
||||
min-height: 10em;
|
||||
max-height: calc(100vh - 10rem);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
background-color: #2f2f35;
|
||||
padding: .5em;
|
||||
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.clientInfo {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 4em;
|
||||
|
||||
margin-bottom: .25em;
|
||||
|
||||
.textError {
|
||||
color: #cc0000;
|
||||
}
|
||||
}
|
||||
|
||||
.refreshButton {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.containerButtons {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
padding-top: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&.windowed {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.assignmentList {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-height: 4em;
|
||||
height: 20em;
|
||||
|
||||
padding: 3px;
|
||||
overflow-y: auto;
|
||||
|
||||
border: 1px #161616 solid;
|
||||
border-radius: $border_radius_middle;
|
||||
background-color: #28292b;
|
||||
|
||||
position: relative;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
|
||||
.entry {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
height: 1.75em;
|
||||
|
||||
> * {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
min-width: 6em;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.text {
|
||||
align-self: center;
|
||||
text-align: center;
|
||||
|
||||
color: #666;
|
||||
font-size: 1.2em;
|
||||
|
||||
&.error {
|
||||
color: #a32929;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
align-self: center;
|
||||
margin-right: 4px;
|
||||
margin-left: 2px;
|
||||
margin-top: -2px;
|
||||
}
|
||||
|
||||
a {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
align-self: center;
|
||||
cursor: pointer;
|
||||
|
||||
padding: .2em;
|
||||
border-radius: .2em;
|
||||
|
||||
transition: all ease-in-out $button_hover_animation_time;
|
||||
|
||||
&:hover {
|
||||
background-color: #0000004f;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,222 @@
|
|||
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
|
||||
import React, {useContext} from "react";
|
||||
import {IpcRegistryDescription, Registry} from "tc-events";
|
||||
import {
|
||||
AvailableGroup,
|
||||
ModalClientGroupAssignmentEvents,
|
||||
ModalClientGroupAssignmentVariables
|
||||
} from "tc-shared/ui/modal/group-assignment/Definitions";
|
||||
import {createIpcUiVariableConsumer, IpcVariableDescriptor} from "tc-shared/ui/utils/IpcVariable";
|
||||
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
|
||||
import {Translatable, VariadicTranslatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {ClientTag} from "tc-shared/ui/tree/EntryTags";
|
||||
import {Checkbox} from "tc-shared/ui/react-elements/Checkbox";
|
||||
import {RemoteIconInfoRenderer} from "tc-shared/ui/react-elements/Icon";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots";
|
||||
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
|
||||
const cssStyle = require("./Renderer.scss");
|
||||
const VariablesContext = React.createContext<UiVariableConsumer<ModalClientGroupAssignmentVariables>>(undefined);
|
||||
const EventsContext = React.createContext<Registry<ModalClientGroupAssignmentEvents>>(undefined);
|
||||
|
||||
const GroupEntryRenderer = React.memo((props: { entry : AvailableGroup, defaultGroup: boolean, defaultGroupActive: boolean }) => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const assigned = variables.useVariable("groupAssigned", props.entry.groupId, false);
|
||||
|
||||
let disabled = false;
|
||||
if(assigned.status === "applying") {
|
||||
disabled = true;
|
||||
} else if(assigned.localValue ? !props.entry.removeAble : !props.entry.addAble) {
|
||||
disabled = true;
|
||||
} else if(props.defaultGroup && !assigned.localValue) {
|
||||
/* Only disable it if we haven't the group assigned in order to remove the assignment even though the groups is the default group */
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
let nameSuffix;
|
||||
if(props.defaultGroup) {
|
||||
nameSuffix = " [default]";
|
||||
} else if(!props.entry.saveDB) {
|
||||
nameSuffix = " [temp]";
|
||||
}
|
||||
return (
|
||||
<div className={cssStyle.entry}>
|
||||
<Checkbox
|
||||
value={(props.defaultGroup && props.defaultGroupActive) || assigned.localValue}
|
||||
onChange={value => assigned.setValue(value)}
|
||||
disabled={disabled}
|
||||
className={cssStyle.checkbox}
|
||||
/>
|
||||
<RemoteIconInfoRenderer icon={props.entry.icon} className={cssStyle.icon} />
|
||||
<div className={cssStyle.name}>{props.entry.name}{nameSuffix}</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const GroupListRenderer = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const serverGroups = variables.useReadOnly("availableGroups", undefined, { defaultGroup: -1, groups: [] });
|
||||
const assignmentStatus = variables.useReadOnly("assignedGroupStatus", undefined, { status: "loading" });
|
||||
|
||||
let body;
|
||||
if(assignmentStatus.status === "loaded") {
|
||||
body = serverGroups.groups.map(entry => (
|
||||
<GroupEntryRenderer
|
||||
entry={entry} key={"group-" + entry.groupId}
|
||||
defaultGroup={serverGroups.defaultGroup === entry.groupId}
|
||||
defaultGroupActive={assignmentStatus.assignedGroups === 0}
|
||||
/>
|
||||
));
|
||||
} else if(assignmentStatus.status === "loading") {
|
||||
body = (
|
||||
<div className={cssStyle.overlay} key={"loading"}>
|
||||
<div className={cssStyle.text}>
|
||||
<Translatable>loading</Translatable> <LoadingDots />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if(assignmentStatus.status === "error") {
|
||||
body = (
|
||||
<div className={cssStyle.overlay} key={"error"}>
|
||||
<div className={cssStyle.text + " " + cssStyle.error}>
|
||||
<Translatable>An error occurred:</Translatable><br />
|
||||
{assignmentStatus.message}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className={cssStyle.assignmentList}>
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Buttons = React.memo(() => {
|
||||
const events = useContext(EventsContext);
|
||||
const variables = useContext(VariablesContext);
|
||||
const assignmentStatus = variables.useReadOnly("assignedGroupStatus", undefined, { status: "loading" });
|
||||
|
||||
return (
|
||||
<div className={cssStyle.containerButtons}>
|
||||
<Button
|
||||
color={"red"}
|
||||
onClick={() => events.fire("action_remove_all")}
|
||||
disabled={assignmentStatus.status === "loaded" ? assignmentStatus.assignedGroups === 0 : true}
|
||||
>
|
||||
<Translatable>Remove all groups</Translatable>
|
||||
</Button>
|
||||
<Button color={"green"} onClick={() => events.fire("action_close")}>
|
||||
<Translatable>Close</Translatable>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
|
||||
const ClientInfoRenderer = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const handlerId = variables.useReadOnly("handlerId", undefined, undefined);
|
||||
const user = variables.useReadOnly("targetClient");
|
||||
|
||||
let inner;
|
||||
if(handlerId && user.status === "loaded") {
|
||||
let clientTag;
|
||||
if(user.value.status === "success") {
|
||||
clientTag = <ClientTag key={"client-tag"} clientName={user.value.clientName} clientUniqueId={user.value.clientUniqueId} handlerId={handlerId} />;
|
||||
} else {
|
||||
clientTag = <div key={"error"} className={cssStyle.textError}>{user.value.message}</div>;
|
||||
}
|
||||
|
||||
inner = (
|
||||
<VariadicTranslatable text={"Changing groups of {}"} key={"user-specific"}>
|
||||
{clientTag}
|
||||
</VariadicTranslatable>
|
||||
);
|
||||
} else {
|
||||
inner = <Translatable key={"generic"}>Change server groups</Translatable>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cssStyle.clientInfo}>
|
||||
{inner}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const RefreshButton = React.memo(() => {
|
||||
const events = useContext(EventsContext);
|
||||
|
||||
return (
|
||||
<div className={cssStyle.refreshButton + " " + cssStyle.button} onClick={() => events.fire("action_refresh", { slowMode: true })}>
|
||||
<ClientIconRenderer icon={ClientIcon.Refresh} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
const TitleRenderer = React.memo(() => {
|
||||
const variables = useContext(VariablesContext);
|
||||
const client = variables.useReadOnly("targetClient");
|
||||
const handlerId = variables.useReadOnly("handlerId", undefined, undefined);
|
||||
|
||||
if(client.status === "loaded" && client.value.status === "success" && handlerId) {
|
||||
return (
|
||||
<VariadicTranslatable text={"Server group assignments for {}"} key={"client-known"}>
|
||||
<ClientTag
|
||||
handlerId={handlerId}
|
||||
clientName={client.value.clientName}
|
||||
clientUniqueId={client.value.clientUniqueId}
|
||||
clientDatabaseId={client.value.clientDatabaseId}
|
||||
style={"text-only"}
|
||||
/>
|
||||
</VariadicTranslatable>
|
||||
);
|
||||
} else {
|
||||
return <Translatable key={"unknown"}>Server group assignments</Translatable>;
|
||||
}
|
||||
});
|
||||
|
||||
export default class ModalServerGroups extends AbstractModal {
|
||||
private readonly events: Registry<ModalClientGroupAssignmentEvents>;
|
||||
private readonly variables: UiVariableConsumer<ModalClientGroupAssignmentVariables>;
|
||||
|
||||
constructor(events: IpcRegistryDescription<ModalClientGroupAssignmentEvents>, variables: IpcVariableDescriptor<ModalClientGroupAssignmentVariables>) {
|
||||
super();
|
||||
|
||||
this.events = Registry.fromIpcDescription(events);
|
||||
this.variables = createIpcUiVariableConsumer(variables);
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
this.events.destroy();
|
||||
this.variables.destroy();
|
||||
}
|
||||
|
||||
renderBody(): React.ReactElement {
|
||||
return (
|
||||
<EventsContext.Provider value={this.events}>
|
||||
<VariablesContext.Provider value={this.variables}>
|
||||
<div className={cssStyle.container + " " + (this.properties.windowed ? cssStyle.windowed : "")}>
|
||||
<div className={cssStyle.title}>
|
||||
<ClientInfoRenderer />
|
||||
<RefreshButton />
|
||||
</div>
|
||||
<GroupListRenderer />
|
||||
<Buttons />
|
||||
</div>
|
||||
</VariablesContext.Provider>
|
||||
</EventsContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
renderTitle(): string | React.ReactElement {
|
||||
return (
|
||||
<VariablesContext.Provider value={this.variables}>
|
||||
<TitleRenderer />
|
||||
</VariablesContext.Provider>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ export interface CheckboxProperties {
|
|||
value?: boolean;
|
||||
initialValue?: boolean;
|
||||
|
||||
className?: string;
|
||||
children?: never;
|
||||
}
|
||||
|
||||
|
@ -32,7 +33,7 @@ export class Checkbox extends React.Component<CheckboxProperties, CheckboxState>
|
|||
const disabledClass = disabled ? cssStyle.disabled : "";
|
||||
|
||||
return (
|
||||
<label className={cssStyle.labelCheckbox + " " + disabledClass}>
|
||||
<label className={cssStyle.labelCheckbox + " " + disabledClass + " " + this.props.className}>
|
||||
<div className={cssStyle.checkbox + " " + disabledClass}>
|
||||
<input type={"checkbox"} checked={checked} disabled={disabled} onChange={event => this.onStateChange(event)} />
|
||||
<div className={cssStyle.mark} />
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import {RemoteIcon} from "tc-shared/file/Icons";
|
||||
import {getIconManager, RemoteIcon, RemoteIconInfo} from "tc-shared/file/Icons";
|
||||
|
||||
const cssStyle = require("./Icon.scss");
|
||||
|
||||
const EmptyIcon = (props: { className?: string, title?: string }) => <div className={cssStyle.container + " icon-container icon-empty " + props.className} title={props.title} />;
|
||||
|
||||
export const IconRenderer = (props: {
|
||||
icon: string;
|
||||
title?: string;
|
||||
|
@ -18,7 +20,7 @@ export const IconRenderer = (props: {
|
|||
}
|
||||
}
|
||||
|
||||
export const RemoteIconRenderer = (props: { icon: RemoteIcon, className?: string, title?: string }) => {
|
||||
export const RemoteIconRenderer = (props: { icon: RemoteIcon | undefined, className?: string, title?: string }) => {
|
||||
const [ revision, setRevision ] = useState(0);
|
||||
|
||||
props.icon.events.reactUse("notify_state_changed", () => setRevision(revision + 1));
|
||||
|
@ -51,4 +53,12 @@ export const RemoteIconRenderer = (props: { icon: RemoteIcon, className?: string
|
|||
default:
|
||||
throw "invalid icon state";
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
export const RemoteIconInfoRenderer = React.memo((props: { icon: RemoteIconInfo | undefined, className?: string, title?: string }) => {
|
||||
if(!props.icon || props.icon.iconId === 0) {
|
||||
return <EmptyIcon title={props.title} className={props.className} key={"empty-icon"} />;
|
||||
} else {
|
||||
return <RemoteIconRenderer icon={getIconManager().resolveIconInfo(props.icon)} className={props.className} title={props.title} key={"icon"} />;
|
||||
}
|
||||
});
|
|
@ -1,5 +1,4 @@
|
|||
import {IpcRegistryDescription, Registry} from "tc-shared/events";
|
||||
import {VideoViewerEvents} from "tc-shared/video-viewer/Definitions";
|
||||
import {ChannelEditEvents} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||
import {EchoTestEvents} from "tc-shared/ui/modal/echo-test/Definitions";
|
||||
import {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
||||
|
@ -12,6 +11,11 @@ import {
|
|||
ModalBookmarksAddServerVariables
|
||||
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
||||
import {ModalPokeEvents, ModalPokeVariables} from "tc-shared/ui/modal/poke/Definitions";
|
||||
import {
|
||||
ModalClientGroupAssignmentEvents,
|
||||
ModalClientGroupAssignmentVariables
|
||||
} from "tc-shared/ui/modal/group-assignment/Definitions";
|
||||
import {VideoViewerEvents} from "tc-shared/ui/modal/video-viewer/Definitions";
|
||||
|
||||
export type ModalType = "error" | "warning" | "info" | "none";
|
||||
export type ModalRenderType = "page" | "dialog";
|
||||
|
@ -188,4 +192,8 @@ export interface ModalConstructorArguments {
|
|||
/* events */ IpcRegistryDescription<ModalPokeEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalPokeVariables>,
|
||||
],
|
||||
"modal-assign-server-groups": [
|
||||
/* events */ IpcRegistryDescription<ModalClientGroupAssignmentEvents>,
|
||||
/* variables */ IpcVariableDescriptor<ModalClientGroupAssignmentVariables>,
|
||||
],
|
||||
}
|
|
@ -91,3 +91,10 @@ registerModal({
|
|||
popoutSupported: true
|
||||
});
|
||||
|
||||
registerModal({
|
||||
modalId: "modal-assign-server-groups",
|
||||
classLoader: async () => await import("tc-shared/ui/modal/group-assignment/Renderer"),
|
||||
popoutSupported: true
|
||||
});
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import * as React from "react";
|
|||
import * as loader from "tc-loader";
|
||||
import {Stage} from "tc-loader";
|
||||
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||
import {AppParameters} from "tc-shared/settings";
|
||||
import {generateDragElement, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
||||
import {ClientIcon} from "svg-sprites/client-icons";
|
||||
|
||||
|
@ -11,6 +10,7 @@ const cssStyle = require("./EntryTags.scss");
|
|||
|
||||
let ipcChannel: IPCChannel;
|
||||
|
||||
type EntryTagStyle = "text-only" | "normal";
|
||||
export const ServerTag = React.memo((props: {
|
||||
serverName: string,
|
||||
handlerId: string,
|
||||
|
@ -44,42 +44,56 @@ export const ClientTag = React.memo((props: {
|
|||
handlerId: string,
|
||||
clientId?: number,
|
||||
clientDatabaseId?: number,
|
||||
className?: string
|
||||
}) => (
|
||||
<div className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
className?: string,
|
||||
|
||||
ipcChannel.sendMessage("contextmenu-client", {
|
||||
clientUniqueId: props.clientUniqueId,
|
||||
handlerId: props.handlerId,
|
||||
clientId: props.clientId,
|
||||
clientDatabaseId: props.clientDatabaseId,
|
||||
style?: EntryTagStyle
|
||||
}) => {
|
||||
let style = props.style || "normal";
|
||||
if(style === "text-only") {
|
||||
return <React.Fragment key={"text-only"}>{props.clientName}</React.Fragment>;
|
||||
}
|
||||
|
||||
pageX: event.pageX,
|
||||
pageY: event.pageY
|
||||
});
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => {
|
||||
/* clients only => move */
|
||||
event.dataTransfer.effectAllowed = "move"; /* prohibit copying */
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.PlayerOn, name: props.clientName }]), 0, 6);
|
||||
setupDragData(event.dataTransfer, props.handlerId, [
|
||||
{
|
||||
type: "client",
|
||||
clientUniqueId: props.clientUniqueId,
|
||||
clientId: props.clientId,
|
||||
clientDatabaseId: props.clientDatabaseId
|
||||
}
|
||||
], "client");
|
||||
event.dataTransfer.setData("text/plain", props.clientName);
|
||||
}}
|
||||
>
|
||||
{props.clientName}
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div
|
||||
key={"normal"}
|
||||
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
|
||||
ipcChannel.sendMessage("contextmenu-client", {
|
||||
clientUniqueId: props.clientUniqueId,
|
||||
handlerId: props.handlerId,
|
||||
clientId: props.clientId,
|
||||
clientDatabaseId: props.clientDatabaseId,
|
||||
|
||||
pageX: event.pageX,
|
||||
pageY: event.pageY
|
||||
});
|
||||
}}
|
||||
draggable={true}
|
||||
onDragStart={event => {
|
||||
/* clients only => move */
|
||||
event.dataTransfer.effectAllowed = "move"; /* prohibit copying */
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
event.dataTransfer.setDragImage(generateDragElement([{
|
||||
icon: ClientIcon.PlayerOn,
|
||||
name: props.clientName
|
||||
}]), 0, 6);
|
||||
setupDragData(event.dataTransfer, props.handlerId, [
|
||||
{
|
||||
type: "client",
|
||||
clientUniqueId: props.clientUniqueId,
|
||||
clientId: props.clientId,
|
||||
clientDatabaseId: props.clientDatabaseId
|
||||
}
|
||||
], "client");
|
||||
event.dataTransfer.setData("text/plain", props.clientName);
|
||||
}}
|
||||
>
|
||||
{props.clientName}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const ChannelTag = React.memo((props: {
|
||||
channelName: string,
|
||||
|
|
|
@ -46,6 +46,10 @@ export abstract class UiVariableProvider<Variables extends UiVariableMap> {
|
|||
this.variableProvider[variable as any] = provider;
|
||||
}
|
||||
|
||||
setVariableProviderAsync<T extends keyof Variables>(variable: T, provider: (customData: any) => Promise<Variables[T]>) {
|
||||
this.variableProvider[variable as any] = provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param variable
|
||||
* @param editor If the editor returns `false` or a new variable, such variable will be used
|
||||
|
|
Loading…
Reference in New Issue