Reworked the server group assignment modal
parent
f3f5fd0618
commit
582b93852b
|
@ -1,6 +1,9 @@
|
||||||
# Changelog:
|
# 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**
|
* **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**
|
* **17.03.21**
|
||||||
- Updated from webpack 4 to webpack 5
|
- 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 {CommandResult} from "../connection/ServerConnectionDeclaration";
|
||||||
import {ChannelEntry} from "./Channel";
|
import {ChannelEntry} from "./Channel";
|
||||||
import {ConnectionHandler, ViewReasonId} from "../ConnectionHandler";
|
import {ConnectionHandler, ViewReasonId} from "../ConnectionHandler";
|
||||||
import {createServerGroupAssignmentModal} from "../ui/modal/ModalGroupAssignment";
|
|
||||||
import {openClientInfo} from "../ui/modal/ModalClientInfo";
|
import {openClientInfo} from "../ui/modal/ModalClientInfo";
|
||||||
import {spawnBanClient} from "../ui/modal/ModalBanClient";
|
import {spawnBanClient} from "../ui/modal/ModalBanClient";
|
||||||
import {spawnChangeLatency} from "../ui/modal/ModalChangeLatency";
|
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 { tr } from "tc-shared/i18n/localize";
|
||||||
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
import {EventClient} from "tc-shared/connectionlog/Definitions";
|
||||||
import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
|
||||||
|
import {spawnServerGroupAssignments} from "tc-shared/ui/modal/group-assignment/Controller";
|
||||||
|
|
||||||
export enum ClientType {
|
export enum ClientType {
|
||||||
CLIENT_VOICE,
|
CLIENT_VOICE,
|
||||||
|
@ -472,30 +472,7 @@ export class ClientEntry<Events extends ClientEvents = ClientEvents> extends Cha
|
||||||
}
|
}
|
||||||
|
|
||||||
open_assignment_modal() {
|
open_assignment_modal() {
|
||||||
createServerGroupAssignmentModal(this as any, (groups, flag) => {
|
spawnServerGroupAssignments(this.channelTree.client, this.properties.client_database_id);
|
||||||
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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
open_text_chat() {
|
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;
|
value?: boolean;
|
||||||
initialValue?: boolean;
|
initialValue?: boolean;
|
||||||
|
|
||||||
|
className?: string;
|
||||||
children?: never;
|
children?: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ export class Checkbox extends React.Component<CheckboxProperties, CheckboxState>
|
||||||
const disabledClass = disabled ? cssStyle.disabled : "";
|
const disabledClass = disabled ? cssStyle.disabled : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label className={cssStyle.labelCheckbox + " " + disabledClass}>
|
<label className={cssStyle.labelCheckbox + " " + disabledClass + " " + this.props.className}>
|
||||||
<div className={cssStyle.checkbox + " " + disabledClass}>
|
<div className={cssStyle.checkbox + " " + disabledClass}>
|
||||||
<input type={"checkbox"} checked={checked} disabled={disabled} onChange={event => this.onStateChange(event)} />
|
<input type={"checkbox"} checked={checked} disabled={disabled} onChange={event => this.onStateChange(event)} />
|
||||||
<div className={cssStyle.mark} />
|
<div className={cssStyle.mark} />
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useState} 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 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: {
|
export const IconRenderer = (props: {
|
||||||
icon: string;
|
icon: string;
|
||||||
title?: 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);
|
const [ revision, setRevision ] = useState(0);
|
||||||
|
|
||||||
props.icon.events.reactUse("notify_state_changed", () => setRevision(revision + 1));
|
props.icon.events.reactUse("notify_state_changed", () => setRevision(revision + 1));
|
||||||
|
@ -51,4 +53,12 @@ export const RemoteIconRenderer = (props: { icon: RemoteIcon, className?: string
|
||||||
default:
|
default:
|
||||||
throw "invalid icon state";
|
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 {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 {ChannelEditEvents} from "tc-shared/ui/modal/channel-edit/Definitions";
|
||||||
import {EchoTestEvents} from "tc-shared/ui/modal/echo-test/Definitions";
|
import {EchoTestEvents} from "tc-shared/ui/modal/echo-test/Definitions";
|
||||||
import {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
import {ModalGlobalSettingsEditorEvents} from "tc-shared/ui/modal/global-settings-editor/Definitions";
|
||||||
|
@ -12,6 +11,11 @@ import {
|
||||||
ModalBookmarksAddServerVariables
|
ModalBookmarksAddServerVariables
|
||||||
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
} from "tc-shared/ui/modal/bookmarks-add-server/Definitions";
|
||||||
import {ModalPokeEvents, ModalPokeVariables} from "tc-shared/ui/modal/poke/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 ModalType = "error" | "warning" | "info" | "none";
|
||||||
export type ModalRenderType = "page" | "dialog";
|
export type ModalRenderType = "page" | "dialog";
|
||||||
|
@ -188,4 +192,8 @@ export interface ModalConstructorArguments {
|
||||||
/* events */ IpcRegistryDescription<ModalPokeEvents>,
|
/* events */ IpcRegistryDescription<ModalPokeEvents>,
|
||||||
/* variables */ IpcVariableDescriptor<ModalPokeVariables>,
|
/* variables */ IpcVariableDescriptor<ModalPokeVariables>,
|
||||||
],
|
],
|
||||||
|
"modal-assign-server-groups": [
|
||||||
|
/* events */ IpcRegistryDescription<ModalClientGroupAssignmentEvents>,
|
||||||
|
/* variables */ IpcVariableDescriptor<ModalClientGroupAssignmentVariables>,
|
||||||
|
],
|
||||||
}
|
}
|
|
@ -91,3 +91,10 @@ registerModal({
|
||||||
popoutSupported: true
|
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 * as loader from "tc-loader";
|
||||||
import {Stage} from "tc-loader";
|
import {Stage} from "tc-loader";
|
||||||
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
import {getIpcInstance, IPCChannel} from "tc-shared/ipc/BrowserIPC";
|
||||||
import {AppParameters} from "tc-shared/settings";
|
|
||||||
import {generateDragElement, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
import {generateDragElement, setupDragData} from "tc-shared/ui/tree/DragHelper";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
|
||||||
|
@ -11,6 +10,7 @@ const cssStyle = require("./EntryTags.scss");
|
||||||
|
|
||||||
let ipcChannel: IPCChannel;
|
let ipcChannel: IPCChannel;
|
||||||
|
|
||||||
|
type EntryTagStyle = "text-only" | "normal";
|
||||||
export const ServerTag = React.memo((props: {
|
export const ServerTag = React.memo((props: {
|
||||||
serverName: string,
|
serverName: string,
|
||||||
handlerId: string,
|
handlerId: string,
|
||||||
|
@ -44,42 +44,56 @@ export const ClientTag = React.memo((props: {
|
||||||
handlerId: string,
|
handlerId: string,
|
||||||
clientId?: number,
|
clientId?: number,
|
||||||
clientDatabaseId?: number,
|
clientDatabaseId?: number,
|
||||||
className?: string
|
className?: string,
|
||||||
}) => (
|
|
||||||
<div className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
|
||||||
onContextMenu={event => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
ipcChannel.sendMessage("contextmenu-client", {
|
style?: EntryTagStyle
|
||||||
clientUniqueId: props.clientUniqueId,
|
}) => {
|
||||||
handlerId: props.handlerId,
|
let style = props.style || "normal";
|
||||||
clientId: props.clientId,
|
if(style === "text-only") {
|
||||||
clientDatabaseId: props.clientDatabaseId,
|
return <React.Fragment key={"text-only"}>{props.clientName}</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
pageX: event.pageX,
|
return (
|
||||||
pageY: event.pageY
|
<div
|
||||||
});
|
key={"normal"}
|
||||||
}}
|
className={cssStyle.tag + (props.className ? ` ${props.className}` : ``)}
|
||||||
draggable={true}
|
onContextMenu={event => {
|
||||||
onDragStart={event => {
|
event.preventDefault();
|
||||||
/* clients only => move */
|
|
||||||
event.dataTransfer.effectAllowed = "move"; /* prohibit copying */
|
ipcChannel.sendMessage("contextmenu-client", {
|
||||||
event.dataTransfer.dropEffect = "move";
|
clientUniqueId: props.clientUniqueId,
|
||||||
event.dataTransfer.setDragImage(generateDragElement([{ icon: ClientIcon.PlayerOn, name: props.clientName }]), 0, 6);
|
handlerId: props.handlerId,
|
||||||
setupDragData(event.dataTransfer, props.handlerId, [
|
clientId: props.clientId,
|
||||||
{
|
clientDatabaseId: props.clientDatabaseId,
|
||||||
type: "client",
|
|
||||||
clientUniqueId: props.clientUniqueId,
|
pageX: event.pageX,
|
||||||
clientId: props.clientId,
|
pageY: event.pageY
|
||||||
clientDatabaseId: props.clientDatabaseId
|
});
|
||||||
}
|
}}
|
||||||
], "client");
|
draggable={true}
|
||||||
event.dataTransfer.setData("text/plain", props.clientName);
|
onDragStart={event => {
|
||||||
}}
|
/* clients only => move */
|
||||||
>
|
event.dataTransfer.effectAllowed = "move"; /* prohibit copying */
|
||||||
{props.clientName}
|
event.dataTransfer.dropEffect = "move";
|
||||||
</div>
|
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: {
|
export const ChannelTag = React.memo((props: {
|
||||||
channelName: string,
|
channelName: string,
|
||||||
|
|
|
@ -46,6 +46,10 @@ export abstract class UiVariableProvider<Variables extends UiVariableMap> {
|
||||||
this.variableProvider[variable as any] = provider;
|
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 variable
|
||||||
* @param editor If the editor returns `false` or a new variable, such variable will be used
|
* @param editor If the editor returns `false` or a new variable, such variable will be used
|
||||||
|
|
Loading…
Reference in New Issue