Permission editor reworking
parent
c936a56aee
commit
c38b2c8d83
11
ChangeLog.md
11
ChangeLog.md
|
@ -1,4 +1,15 @@
|
|||
# Changelog:
|
||||
* **15.06.20**
|
||||
- Recoded the permission editor with react
|
||||
- Fixed sever permission editor display bugs
|
||||
- Recoded the group add editor with react
|
||||
- Added the ability to duplicate groups and copy their permissions
|
||||
- The permission editor now uses by default the highest permission value instead of 1
|
||||
- Added options to enable/disable a whole permission group
|
||||
|
||||
* **14.06.20**
|
||||
- Fixed local icon display element not updating when the icon has been loaded
|
||||
|
||||
* **13.06.20**
|
||||
- Started to extract all color values and put them into css variables
|
||||
- Fixed a minor issue related to server/channel groups
|
||||
|
|
|
@ -70,8 +70,7 @@
|
|||
"webpack": "^4.42.1",
|
||||
"webpack-bundle-analyzer": "^3.6.1",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"worker-plugin": "^4.0.2",
|
||||
"tsd": "latest"
|
||||
"worker-plugin": "^4.0.2"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -1,47 +1,6 @@
|
|||
@import "mixin";
|
||||
@import "properties";
|
||||
|
||||
html:root {
|
||||
--modal-permissions-header-text: #e1e1e1;
|
||||
--modal-permissions-header-background: #19191b;
|
||||
--modal-permissions-header-hover: #4e4e4e;
|
||||
--modal-permissions-header-selected: #0073d4;
|
||||
|
||||
--modal-permission-right: #303036;
|
||||
--modal-permission-left: #222226;
|
||||
|
||||
--modal-permissions-entry-hover: #28282c;
|
||||
--modal-permissions-entry-selected: #111111;
|
||||
--modal-permissions-current-group: #101012;
|
||||
|
||||
--modal-permissions-buttons-background: #1b1b1b;
|
||||
--modal-permissions-buttons-hover: #262626;
|
||||
--modal-permissions-buttons-disabled: hsla(0, 0%, 9%, 1);
|
||||
|
||||
--modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */
|
||||
--modal-permissions-container-seperator: #222224; /* the seperator between left and right */
|
||||
|
||||
--modal-permissions-icon-select: #121213;
|
||||
--modal-permissions-icon-select-border: #0d0d0d;
|
||||
--modal-permissions-icon-select-hover: #17171a;
|
||||
--modal-permissions-icon-select-hover-border: #333333;
|
||||
|
||||
--modal-permissions-table-border: #1e2025;
|
||||
|
||||
--modal-permissions-table-header: #303036;
|
||||
--modal-permissions-table-entry-odd: #303036;
|
||||
--modal-permissions-table-entry-even: #25252a;
|
||||
--modal-permissions-table-entry-hover: #343a47;
|
||||
|
||||
--modal-permissions-table-header-text: #e1e1e1;
|
||||
--modal-permissions-table-entry-text: #535455;
|
||||
--modal-permissions-table-entry-active-text: #e1e1e1;
|
||||
--modal-permissions-table-entry-group-text: #e1e1e1;
|
||||
|
||||
--modal-permissions-table-input: #e1e1e1;
|
||||
--modal-permissions-table-input-focus: #3f7dbf;
|
||||
}
|
||||
|
||||
.modal-body.modal-permission-editor {
|
||||
padding: 0!important;
|
||||
|
||||
|
@ -69,8 +28,8 @@ html:root {
|
|||
|
||||
.header {
|
||||
height: 4em;
|
||||
background-color: var(--modal-permissions-header-text);
|
||||
color: var(--modal-permissions-header-background);
|
||||
background-color: var(--modal-permissions-header-background);
|
||||
color: var(--modal-permissions-header-text);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -116,6 +75,7 @@ html:root {
|
|||
|
||||
background-color: var(--modal-permission-right);
|
||||
|
||||
/* DONE! */
|
||||
.header {
|
||||
> .entry {
|
||||
position: relative;
|
||||
|
@ -736,9 +696,9 @@ html:root {
|
|||
height: 2em;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--modal-permissions-table-border);
|
||||
background-color: var(--modal-permissions-table-entry-odd);
|
||||
background-color: var(--modal-permissions-table-row-odd);
|
||||
|
||||
color: var(--modal-permissions-table-entry-text);
|
||||
color: var(--modal-permissions-table-row-text);
|
||||
|
||||
@mixin fixed-column($name, $width) {
|
||||
.column-#{$name} {
|
||||
|
@ -845,11 +805,11 @@ html:root {
|
|||
|
||||
.entry {
|
||||
&.even {
|
||||
background-color: var(--modal-permissions-table-entry-even);
|
||||
background-color: var(--modal-permissions-table-row-even);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--modal-permissions-table-entry-hover);
|
||||
background-color: var(--modal-permissions-table-row-hover);
|
||||
}
|
||||
/* We cant use this effect here because the odd/even effect would be a bit crazy then */
|
||||
//@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
|
|
Before Width: | Height: | Size: 678 B After Width: | Height: | Size: 678 B |
|
@ -375,12 +375,15 @@ export class CommandHelper extends AbstractCommandHandler {
|
|||
|
||||
try {
|
||||
const result: ServerGroupClient[] = [];
|
||||
for(const entry of command.arguments)
|
||||
for(const entry of command.arguments) {
|
||||
if(!('cldbid' in entry))
|
||||
continue;
|
||||
result.push({
|
||||
client_database_id: parseInt(entry["cldbid"]),
|
||||
client_nickname: entry["client_nickname"],
|
||||
client_unique_identifier: entry["client_unique_identifier"]
|
||||
});
|
||||
}
|
||||
resolve(result);
|
||||
} catch (error) {
|
||||
log.error(LogCategory.NETWORKING, tr("Failed to parse server group client list: %o"), error);
|
||||
|
|
|
@ -44,9 +44,9 @@ export class Registry<Events> {
|
|||
enable_warn_unhandled_events() { this.warn_unhandled_events = true; }
|
||||
disable_warn_unhandled_events() { this.warn_unhandled_events = false; }
|
||||
|
||||
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
|
||||
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
|
||||
on(events, handler) {
|
||||
on<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
||||
on(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
||||
on(events, handler) : () => void {
|
||||
if(!Array.isArray(events))
|
||||
events = [events];
|
||||
|
||||
|
@ -57,12 +57,13 @@ export class Registry<Events> {
|
|||
const handlers = this.handler[event] || (this.handler[event] = []);
|
||||
handlers.push(handler);
|
||||
}
|
||||
return () => this.off(events, handler);
|
||||
}
|
||||
|
||||
/* one */
|
||||
one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void);
|
||||
one(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void);
|
||||
one(events, handler) {
|
||||
one<T extends keyof Events>(event: T, handler: (event?: Events[T] & Event<Events, T>) => void) : () => void;
|
||||
one(events: (keyof Events)[], handler: (event?: Event<Events, keyof Events>) => void) : () => void;
|
||||
one(events, handler) : () => void {
|
||||
if(!Array.isArray(events))
|
||||
events = [events];
|
||||
|
||||
|
@ -72,6 +73,7 @@ export class Registry<Events> {
|
|||
handler[this.registry_uuid] = { singleshot: true };
|
||||
handlers.push(handler);
|
||||
}
|
||||
return () => this.off(events, handler);
|
||||
}
|
||||
|
||||
off<T extends keyof Events>(handler: (event?) => void);
|
||||
|
|
|
@ -1,21 +1,18 @@
|
|||
import {Registry} from "tc-shared/events";
|
||||
import {ClientGlobalControlEvents} from "tc-shared/events/GlobalEvents";
|
||||
import {control_bar_instance, ControlBarEvents} from "tc-shared/ui/frames/control-bar";
|
||||
import {manager, Sound} from "tc-shared/sound/Sounds";
|
||||
import {Sound} from "tc-shared/sound/Sounds";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {default_recorder} from "tc-shared/voice/RecorderProfile";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {add_server_to_bookmarks} from "tc-shared/bookmarks";
|
||||
import {settings} from "tc-shared/settings";
|
||||
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {spawnQueryCreate} from "tc-shared/ui/modal/ModalQuery";
|
||||
import {openBanList} from "tc-shared/ui/modal/ModalBanList";
|
||||
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {spawnSettingsModal} from "tc-shared/ui/modal/ModalSettings";
|
||||
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permissionv2/ModalPermissionEditor";
|
||||
|
||||
/*
|
||||
function initialize_sounds(event_registry: Registry<ClientGlobalControlEvents>) {
|
||||
|
@ -114,7 +111,7 @@ export function initialize(event_registry: Registry<ClientGlobalControlEvents>)
|
|||
}
|
||||
|
||||
if(connection_handler)
|
||||
spawnPermissionEdit(connection_handler).open();
|
||||
spawnPermissionEditorModal(connection_handler);
|
||||
else
|
||||
createErrorModal(tr("You have to be connected"), tr("You have to be connected!")).open();
|
||||
break;
|
||||
|
|
|
@ -34,6 +34,8 @@ import {spawnFileTransferModal} from "tc-shared/ui/modal/transfer/ModalFileTrans
|
|||
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import ContextMenuEvent = JQuery.ContextMenuEvent;
|
||||
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permissionv2/ModalPermissionEditor";
|
||||
import {spawnGroupCreate} from "tc-shared/ui/modal/ModalGroups";
|
||||
|
||||
/* required import for init */
|
||||
require("./proto").initialize();
|
||||
|
@ -496,7 +498,8 @@ function main() {
|
|||
modal.open();
|
||||
}
|
||||
*/
|
||||
|
||||
//setTimeout(() => spawnPermissionEditorModal(server_connections.active_connection()), 3000);
|
||||
//setTimeout(() => spawnGroupCreate(server_connections.active_connection(), "server"), 3000);
|
||||
|
||||
if(settings.static_global(Settings.KEY_USER_IS_NEW)) {
|
||||
const modal = openModalNewcomer();
|
||||
|
@ -547,7 +550,7 @@ const task_connect_handler: loader.Task = {
|
|||
}
|
||||
};
|
||||
|
||||
if(chandler) {
|
||||
if(chandler && !settings.static(Settings.KEY_CONNECT_NO_SINGLE_INSTANCE)) {
|
||||
try {
|
||||
await chandler.post_connect_request(connect_data, () => new Promise<boolean>((resolve, reject) => {
|
||||
spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.{:br:}Would you like to connect there?"), response => {
|
||||
|
|
|
@ -44,12 +44,12 @@ export interface GroupManagerEvents {
|
|||
}
|
||||
}
|
||||
|
||||
export type GroupProperty = "name" | "icon" | "sort-id" | "save-db" | "name-mode";
|
||||
export interface GroupEvents {
|
||||
notify_group_deleted: { },
|
||||
|
||||
notify_properties_updated: {
|
||||
updated_properties: {[Key in keyof GroupProperties]: GroupProperties[Key]};
|
||||
group_properties: GroupProperties
|
||||
updated_properties: GroupProperty[];
|
||||
},
|
||||
|
||||
notify_needed_powers_updated: { }
|
||||
|
@ -81,25 +81,41 @@ export class Group {
|
|||
this.name = name;
|
||||
}
|
||||
|
||||
updateProperties(properties: {key: string, value: string}[]) {
|
||||
let updates = {};
|
||||
for(const { key, value } of properties) {
|
||||
if(!JSON.map_field_to(this.properties, value, key))
|
||||
continue; /* no updates */
|
||||
updatePropertiesFromGroupList(data: any) {
|
||||
const updates: GroupProperty[] = [];
|
||||
|
||||
if(key === "iconid")
|
||||
this.properties.iconid = this.properties.iconid >>> 0;
|
||||
|
||||
updates[key] = this.properties[key];
|
||||
if(this.name !== data["name"]) {
|
||||
this.name = data["name"];
|
||||
updates.push("name");
|
||||
}
|
||||
|
||||
if(Object.keys(updates).length === 0)
|
||||
return;
|
||||
/* icon */
|
||||
let value = parseInt(data["iconid"]) >>> 0;
|
||||
if(value !== this.properties.iconid) {
|
||||
this.properties.iconid = value;
|
||||
updates.push("icon");
|
||||
}
|
||||
|
||||
this.events.fire("notify_properties_updated", {
|
||||
group_properties: this.properties,
|
||||
updated_properties: updates as any
|
||||
});
|
||||
value = parseInt(data["sortid"]);
|
||||
if(value !== this.properties.sortid) {
|
||||
this.properties.sortid = value;
|
||||
updates.push("sort-id");
|
||||
}
|
||||
|
||||
let flag = parseInt(data["savedb"]) >= 1;
|
||||
if(flag !== this.properties.savedb) {
|
||||
this.properties.savedb = flag;
|
||||
updates.push("save-db");
|
||||
}
|
||||
|
||||
value = parseInt(data["namemode"]);
|
||||
if(value !== this.properties.namemode) {
|
||||
this.properties.namemode = value;
|
||||
updates.push("name-mode");
|
||||
}
|
||||
|
||||
if(updates.length > 0)
|
||||
this.events.fire("notify_properties_updated", { updated_properties: updates });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,16 +256,10 @@ export class GroupManager extends AbstractCommandHandler {
|
|||
group = deleteGroups.splice(groupIndex, 1)[0];
|
||||
}
|
||||
|
||||
const property_blacklist = [
|
||||
"sgid", "cgid", "type", "name",
|
||||
|
||||
"n_member_removep", "n_member_addp", "n_modifyp"
|
||||
];
|
||||
|
||||
group.requiredMemberRemovePower = parseInt(groupData["n_member_removep"]);
|
||||
group.requiredMemberAddPower = parseInt(groupData["n_member_addp"]);
|
||||
group.requiredModifyPower = parseInt(groupData["n_modifyp"]);
|
||||
group.updateProperties(Object.keys(groupData).filter(e => property_blacklist.findIndex(a => a === e) === -1).map(e => { return { key: e, value: groupData[e] } }));
|
||||
group.updatePropertiesFromGroupList(groupData);
|
||||
group.events.fire("notify_needed_powers_updated");
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import {ServerCommand} from "tc-shared/connection/ConnectionBase";
|
|||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {AbstractCommandHandler} from "tc-shared/connection/AbstractCommandHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
|
||||
export class PermissionInfo {
|
||||
name: string;
|
||||
|
@ -34,7 +35,7 @@ export class GroupedPermissions {
|
|||
|
||||
export class PermissionValue {
|
||||
readonly type: PermissionInfo;
|
||||
value: number;
|
||||
value: number | undefined; /* undefined if no permission is given */
|
||||
flag_skip: boolean;
|
||||
flag_negate: boolean;
|
||||
granted_value: number;
|
||||
|
@ -61,9 +62,14 @@ export class PermissionValue {
|
|||
hasValue() : boolean {
|
||||
return typeof(this.value) !== "undefined" && this.value != -2;
|
||||
}
|
||||
|
||||
hasGrant() : boolean {
|
||||
return typeof(this.granted_value) !== "undefined" && this.granted_value != -2;
|
||||
}
|
||||
|
||||
valueOr(fallback: number) {
|
||||
return this.hasValue() ? this.value : fallback;
|
||||
}
|
||||
}
|
||||
|
||||
export class NeededPermissionValue extends PermissionValue {
|
||||
|
@ -126,6 +132,10 @@ export namespace find {
|
|||
}
|
||||
}
|
||||
|
||||
export interface PermissionManagerEvents {
|
||||
client_permissions_changed: {}
|
||||
}
|
||||
|
||||
export type RequestLists =
|
||||
"requests_channel_permissions" |
|
||||
"requests_client_permissions" |
|
||||
|
@ -134,6 +144,8 @@ export type RequestLists =
|
|||
"requests_playlist_client_permissions";
|
||||
|
||||
export class PermissionManager extends AbstractCommandHandler {
|
||||
readonly events = new Registry<PermissionManagerEvents>();
|
||||
|
||||
readonly handle: ConnectionHandler;
|
||||
|
||||
permissionList: PermissionInfo[] = [];
|
||||
|
@ -397,6 +409,8 @@ export class PermissionManager extends AbstractCommandHandler {
|
|||
for(const listener of this.needed_permission_change_listener[e.type.name] || [])
|
||||
listener();
|
||||
}
|
||||
|
||||
this.events.fire("client_permissions_changed");
|
||||
}
|
||||
|
||||
register_needed_permission(key: PermissionType, listener: () => any) {
|
||||
|
|
|
@ -289,6 +289,9 @@ if (!String.prototype.format) {
|
|||
};
|
||||
}
|
||||
|
||||
if(!Object.values)
|
||||
Object.values = object => Object.keys(object).map(e => object[e]);
|
||||
|
||||
function concatenate(resultConstructor, ...arrays) {
|
||||
let totalLength = 0;
|
||||
for (const arr of arrays) {
|
||||
|
|
|
@ -238,6 +238,10 @@ export class Settings extends StaticSettings {
|
|||
static readonly KEY_CONNECT_HISTORY: SettingsKey<string> = {
|
||||
key: 'connect_history'
|
||||
};
|
||||
static readonly KEY_CONNECT_NO_SINGLE_INSTANCE: SettingsKey<boolean> = {
|
||||
key: 'connect_no_single_instance',
|
||||
default_value: false
|
||||
};
|
||||
|
||||
static readonly KEY_CONNECT_NO_DNSPROXY: SettingsKey<boolean> = {
|
||||
key: 'connect_no_dnsproxy',
|
||||
|
|
|
@ -201,6 +201,14 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
if(typeof event.updated_properties.client_nickname !== "undefined" || typeof event.updated_properties.client_talk_power !== "undefined")
|
||||
this.reorderClientList(true);
|
||||
};
|
||||
|
||||
this.events.on("notify_properties_updated", event => {
|
||||
this.channelTree?.events.fire("notify_channel_updated", {
|
||||
channel: this,
|
||||
channelProperties: event.channel_properties,
|
||||
updatedProperties: event.updated_properties
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
destroy() {
|
||||
|
@ -223,6 +231,16 @@ export class ChannelEntry extends ChannelTreeEntry<ChannelEvents> {
|
|||
return this.properties.channel_name;
|
||||
}
|
||||
|
||||
channelDepth() {
|
||||
let depth = 0;
|
||||
let parent = this.parent;
|
||||
while(parent) {
|
||||
depth++;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return depth;
|
||||
}
|
||||
|
||||
formattedChannelName() {
|
||||
return this.parsed_channel_name.text;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ import {ChannelEntry} from "tc-shared/ui/channel";
|
|||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {voice} from "tc-shared/connection/ConnectionBase";
|
||||
import VoiceClient = voice.VoiceClient;
|
||||
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
|
||||
import {createServerGroupAssignmentModal} from "tc-shared/ui/modal/ModalGroupAssignment";
|
||||
import {openClientInfo} from "tc-shared/ui/modal/ModalClientInfo";
|
||||
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
|
||||
|
@ -28,6 +27,7 @@ import { ClientEntry as ClientEntryView } from "./tree/Client";
|
|||
import * as React from "react";
|
||||
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "tc-shared/ui/TreeEntry";
|
||||
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "tc-shared/ui/modal/ModalChangeVolumeNew";
|
||||
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permissionv2/ModalPermissionEditor";
|
||||
|
||||
export enum ClientType {
|
||||
CLIENT_VOICE,
|
||||
|
@ -455,13 +455,13 @@ export class ClientEntry extends ChannelTreeEntry<ClientEvents> {
|
|||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-permission_client",
|
||||
name: tr("Client permissions"),
|
||||
callback: () => spawnPermissionEdit(this.channelTree.client, "clp", {unique_id: this.clientUid()}).open()
|
||||
callback: () => spawnPermissionEditorModal(this.channelTree.client, "client", { clientDatabaseId: this.properties.client_database_id })
|
||||
},
|
||||
{
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-permission_client",
|
||||
name: tr("Client channel permissions"),
|
||||
callback: () => spawnPermissionEdit(this.channelTree.client, "clchp", {unique_id: this.clientUid(), channel_id: this._channel ? this._channel.channelId : undefined }).open()
|
||||
callback: () => spawnPermissionEditorModal(this.channelTree.client, "client-channel", { clientDatabaseId: this.properties.client_database_id })
|
||||
}
|
||||
]
|
||||
}];
|
||||
|
|
|
@ -7,10 +7,9 @@ import {
|
|||
boorkmak_connect,
|
||||
DirectoryBookmark
|
||||
} from "tc-shared/bookmarks";
|
||||
import {ConnectionHandler, DisconnectReason} from "tc-shared/ConnectionHandler";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {Sound} from "tc-shared/sound/Sounds";
|
||||
import {spawnConnectModal} from "tc-shared/ui/modal/ModalConnect";
|
||||
import {spawnPermissionEdit} from "tc-shared/ui/modal/permission/ModalPermissionEdit";
|
||||
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {PermissionType} from "tc-shared/permission/PermissionType";
|
||||
|
@ -24,6 +23,7 @@ import * as loader from "tc-loader";
|
|||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {control_bar_instance} from "tc-shared/ui/frames/control-bar";
|
||||
import {icon_cache_loader, IconManager, LocalIcon} from "tc-shared/file/Icons";
|
||||
import {spawnPermissionEditorModal} from "tc-shared/ui/modal/permissionv2/ModalPermissionEditor";
|
||||
|
||||
export interface HRItem { }
|
||||
|
||||
|
@ -390,35 +390,35 @@ export function initialize() {
|
|||
item = menu.append_item(tr("Server Groups"));
|
||||
item.icon("client-permission_server_groups");
|
||||
item.click(() => {
|
||||
spawnPermissionEdit(server_connections.active_connection(), "sg").open();
|
||||
spawnPermissionEditorModal(server_connections.active_connection(), "groups-server");
|
||||
});
|
||||
_state_updater["permission.sg"] = { item: item, conditions: [condition_connected]};
|
||||
|
||||
item = menu.append_item(tr("Client Permissions"));
|
||||
item.icon("client-permission_client");
|
||||
item.click(() => {
|
||||
spawnPermissionEdit(server_connections.active_connection(), "clp").open();
|
||||
spawnPermissionEditorModal(server_connections.active_connection(), "client");
|
||||
});
|
||||
_state_updater["permission.clp"] = { item: item, conditions: [condition_connected]};
|
||||
|
||||
item = menu.append_item(tr("Channel Client Permissions"));
|
||||
item.icon("client-permission_client");
|
||||
item.click(() => {
|
||||
spawnPermissionEdit(server_connections.active_connection(), "clchp").open();
|
||||
spawnPermissionEditorModal(server_connections.active_connection(), "client-channel");
|
||||
});
|
||||
_state_updater["permission.chclp"] = { item: item, conditions: [condition_connected]};
|
||||
|
||||
item = menu.append_item(tr("Channel Groups"));
|
||||
item.icon("client-permission_channel");
|
||||
item.click(() => {
|
||||
spawnPermissionEdit(server_connections.active_connection(), "cg").open();
|
||||
spawnPermissionEditorModal(server_connections.active_connection(), "groups-channel");
|
||||
});
|
||||
_state_updater["permission.cg"] = { item: item, conditions: [condition_connected]};
|
||||
|
||||
item = menu.append_item(tr("Channel Permissions"));
|
||||
item.icon("client-permission_channel");
|
||||
item.click(() => {
|
||||
spawnPermissionEdit(server_connections.active_connection(), "chp").open();
|
||||
spawnPermissionEditorModal(server_connections.active_connection(), "channel");
|
||||
});
|
||||
_state_updater["permission.cp"] = { item: item, conditions: [condition_connected]};
|
||||
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
padding: 0 1em 1em;
|
||||
|
||||
width: 35em;
|
||||
min-width: 10em;
|
||||
max-width: 100%;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
}
|
||||
|
||||
select {
|
||||
.hiddenOption {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.groupType, .groupSource {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
min-width: 6em;
|
||||
}
|
||||
|
||||
.groupType {
|
||||
margin-right: 1em;
|
||||
|
||||
flex-grow: 0;
|
||||
width: 12em;
|
||||
}
|
||||
.groupSource {}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 30em){
|
||||
.container {
|
||||
.row {
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.groupType, .groupSource {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,349 @@
|
|||
import {Modal, spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {FlatInputField, FlatSelect} from "tc-shared/ui/react-elements/InputField";
|
||||
import * as React from "react";
|
||||
import {useEffect, useRef, useState} from "react";
|
||||
import {GroupType} from "tc-shared/permission/GroupManager";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
|
||||
const cssStyle = require("./ModalGroupCreate.scss");
|
||||
|
||||
export type GroupInfo = {
|
||||
id: number,
|
||||
name: string,
|
||||
type: "query" | "template" | "normal"
|
||||
};
|
||||
|
||||
export interface GroupCreateModalEvents {
|
||||
action_set_name: { name: string | undefined },
|
||||
action_set_type: { target: "query" | "template" | "normal" },
|
||||
action_set_source: { group: number },
|
||||
|
||||
action_cancel: {},
|
||||
action_create: {
|
||||
name: string,
|
||||
target: "query" | "template" | "normal",
|
||||
source: number /* if zero than no template */
|
||||
}
|
||||
|
||||
query_available_groups: { },
|
||||
query_available_groups_result: {
|
||||
groups: GroupInfo[]
|
||||
},
|
||||
|
||||
query_client_permissions: {},
|
||||
notify_client_permissions: {
|
||||
createTemplateGroup: boolean,
|
||||
createQueryGroup: boolean
|
||||
},
|
||||
|
||||
notify_destroy: {}
|
||||
}
|
||||
|
||||
const GroupNameInput = (props: { events: Registry<GroupCreateModalEvents>, defaultSource: number }) => {
|
||||
const [ initialLoad, setInitialLoad ] = useState(true);
|
||||
const [ existingGroups, setExistingGroups ] = useState<"loading" | GroupInfo[]>("loading");
|
||||
const [ selectedType, setSelectedType ] = useState<"query" | "template" | "normal" | "loading">("loading");
|
||||
|
||||
const refInput = useRef<FlatInputField>();
|
||||
|
||||
useEffect(() => {
|
||||
if(!initialLoad || !refInput.current)
|
||||
return;
|
||||
|
||||
if(selectedType === "loading" || existingGroups === "loading")
|
||||
return;
|
||||
|
||||
setInitialLoad(false);
|
||||
refInput.current.focus();
|
||||
|
||||
const defaultGroup = existingGroups.find(e => e.id === props.defaultSource);
|
||||
if(defaultGroup) {
|
||||
let name = defaultGroup.name + " (" + tr("Copy") + ")";
|
||||
let index = 1;
|
||||
while(existingGroups.findIndex(e => e.name === name) !== -1)
|
||||
name = defaultGroup.name + " (" + tr("Copy")+ " " + index++ + ")";
|
||||
|
||||
refInput.current.setValue(name);
|
||||
props.events.fire("action_set_name", { name: updateGroupNameState(name) ? name : undefined });
|
||||
}
|
||||
});
|
||||
|
||||
const updateGroupNameState = (input: string) => {
|
||||
if(!refInput.current)
|
||||
return false;
|
||||
|
||||
if(input.length === 0 || input.length > 30) {
|
||||
refInput.current.setState({ isInvalid: true, invalidMessage: tr("Invalid group name length") });
|
||||
return false;
|
||||
}
|
||||
|
||||
if(existingGroups === "loading")
|
||||
return false;
|
||||
|
||||
if(existingGroups.findIndex(e => e.name === input && e.type === selectedType) !== -1) {
|
||||
refInput.current.setState({ isInvalid: true, invalidMessage: tr("A group with this name already exists") });
|
||||
return false;
|
||||
}
|
||||
|
||||
refInput.current.setState({ isInvalid: false });
|
||||
return true;
|
||||
};
|
||||
|
||||
props.events.reactUse("query_available_groups_result", event => setExistingGroups(event.groups));
|
||||
props.events.reactUse("action_set_type", event => setSelectedType(event.target));
|
||||
|
||||
return (
|
||||
<FlatInputField
|
||||
ref={refInput}
|
||||
label={<Translatable>Group name</Translatable>}
|
||||
finishOnEnter={true}
|
||||
|
||||
disabled={existingGroups === "loading" || selectedType === "loading"}
|
||||
placeholder={existingGroups === "loading" || selectedType === "loading" ? tr("loading data...") : undefined}
|
||||
onInput={() => props.events.fire("action_set_name", { name: updateGroupNameState(refInput.current.value()) ? refInput.current.value() : undefined })}
|
||||
onBlur={() => props.events.fire("action_set_name", { name: updateGroupNameState(refInput.current.value()) ? refInput.current.value() : undefined })}
|
||||
/>
|
||||
)
|
||||
};
|
||||
|
||||
const GroupTypeSelector = (props: { events: Registry<GroupCreateModalEvents> }) => {
|
||||
const [ selectedType, setSelectedType ] = useState<"query" | "template" | "normal" | "loading">("loading");
|
||||
const [ permissions, setPermissions ] = useState<"loading" | { createTemplate, createQuery }>("loading");
|
||||
const refSelect = useRef<FlatSelect>();
|
||||
|
||||
props.events.reactUse("notify_client_permissions", event => {
|
||||
setPermissions({
|
||||
createQuery: event.createQueryGroup,
|
||||
createTemplate: event.createTemplateGroup
|
||||
});
|
||||
|
||||
/* the default type */
|
||||
props.events.fire("action_set_type", { target: "normal" });
|
||||
});
|
||||
|
||||
props.events.reactUse("action_set_type", event => setSelectedType(event.target));
|
||||
|
||||
return (
|
||||
<FlatSelect
|
||||
ref={refSelect}
|
||||
label={<Translatable>Target group type</Translatable>}
|
||||
className={cssStyle.groupType}
|
||||
disabled={permissions === "loading"}
|
||||
value={selectedType}
|
||||
onChange={event => event.target.value !== "loading" && props.events.fire("action_set_type", { target: event.target.value as any })}
|
||||
>
|
||||
<option className={cssStyle.hiddenOption} value={"loading"}>{tr("loading...")}</option>
|
||||
<option
|
||||
value={"query"}
|
||||
disabled={permissions === "loading" || !permissions.createQuery}
|
||||
>{tr("Query group")}</option>
|
||||
<option
|
||||
value={"template"}
|
||||
disabled={permissions === "loading" || !permissions.createTemplate}
|
||||
>{tr("Template group")}</option>
|
||||
<option
|
||||
value={"normal"}
|
||||
>{tr("Regular group")}</option>
|
||||
</FlatSelect>
|
||||
)
|
||||
};
|
||||
|
||||
const SourceGroupSelector = (props: { events: Registry<GroupCreateModalEvents>, defaultSource: number }) => {
|
||||
const [ selectedGroup, setSelectedGroup ] = useState(undefined);
|
||||
const [ permissions, setPermissions ] = useState<"loading" | { createTemplate, createQuery }>("loading");
|
||||
const [ exitingGroups, setExitingGroups ] = useState<"loading" | GroupInfo[]>("loading");
|
||||
|
||||
const refSelect = useRef<FlatSelect>();
|
||||
|
||||
props.events.reactUse("notify_client_permissions", event => setPermissions({
|
||||
createQuery: event.createQueryGroup,
|
||||
createTemplate: event.createTemplateGroup
|
||||
}));
|
||||
props.events.reactUse("query_available_groups_result", event => setExitingGroups(event.groups));
|
||||
props.events.reactUse("action_set_source", event => setSelectedGroup(event.group));
|
||||
|
||||
const groupName = (group: GroupInfo) => {
|
||||
let prefix = group.type === "template" ? "[T] " : group.type === "query" ? "[Q] " : "";
|
||||
return prefix + group.name + " (" + group.id + ")";
|
||||
};
|
||||
|
||||
const isLoading = exitingGroups === "loading" || permissions === "loading";
|
||||
if(!isLoading && selectedGroup === undefined)
|
||||
props.events.fire_async("action_set_source", {
|
||||
group: (exitingGroups as GroupInfo[]).findIndex(e => e.id === props.defaultSource) === -1 ? 0 : props.defaultSource
|
||||
});
|
||||
|
||||
return (
|
||||
<FlatSelect
|
||||
ref={refSelect}
|
||||
label={<Translatable>Create group using this template</Translatable>}
|
||||
className={cssStyle.groupSource}
|
||||
disabled={isLoading}
|
||||
value={isLoading || selectedGroup === undefined ? "-1" : selectedGroup.toString()}
|
||||
onChange={event => props.events.fire("action_set_source", { group: parseInt(event.target.value) })}
|
||||
>
|
||||
<option className={cssStyle.hiddenOption} value={"-1"}>{tr("loading...")}</option>
|
||||
<option value={"0"} onSelect={() => props.events.fire("action_set_source", { group: 0 })}>{tr("No template")}</option>
|
||||
<optgroup label={tr("Query groups")} className={permissions === "loading" || !permissions.createQuery ? cssStyle.hiddenOption : ""} >
|
||||
{exitingGroups === "loading" ? undefined :
|
||||
exitingGroups.filter(e => e.type === "query").map(e => (
|
||||
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
<optgroup label={tr("Template groups")} className={permissions === "loading" || !permissions.createTemplate ? cssStyle.hiddenOption : ""} >
|
||||
{exitingGroups === "loading" ? undefined :
|
||||
exitingGroups.filter(e => e.type === "template").map(e => (
|
||||
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
<optgroup label={tr("Regular Groups")} >
|
||||
{exitingGroups === "loading" ? undefined :
|
||||
exitingGroups.filter(e => e.type === "normal").map(e => (
|
||||
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
</FlatSelect>
|
||||
)
|
||||
};
|
||||
|
||||
const CreateButton = (props: { events: Registry<GroupCreateModalEvents> }) => {
|
||||
const [ sourceGroup, setSourceGroup ] = useState<number | undefined>(undefined);
|
||||
const [ groupType, setGroupType ] = useState<"query" | "template" | "normal" | undefined>(undefined);
|
||||
const [ groupName, setGroupName ] = useState<string | undefined>(undefined);
|
||||
|
||||
props.events.reactUse("action_set_name", event => setGroupName(event.name));
|
||||
props.events.reactUse("action_set_type", event => setGroupType(event.target));
|
||||
props.events.reactUse("action_set_source", event => setSourceGroup(event.group));
|
||||
|
||||
return <Button color={"green"} disabled={sourceGroup === undefined || groupType === undefined || groupName === undefined} onClick={() => {
|
||||
props.events.fire("action_create", { name: groupName, source: sourceGroup, target: groupType });
|
||||
}}>
|
||||
<Translatable>Create Group</Translatable>
|
||||
</Button>
|
||||
};
|
||||
|
||||
class ModalGroupCreate extends Modal {
|
||||
readonly target: "server" | "channel";
|
||||
readonly events = new Registry<GroupCreateModalEvents>();
|
||||
readonly defaultSourceGroup: number;
|
||||
|
||||
constructor(connection: ConnectionHandler, target: "server" | "channel", defaultSourceGroup: number) {
|
||||
super();
|
||||
|
||||
this.events.enable_debug("group-create");
|
||||
this.defaultSourceGroup = defaultSourceGroup;
|
||||
this.target = target;
|
||||
initializeGroupCreateController(connection, this.events, this.target);
|
||||
}
|
||||
|
||||
protected onInitialize() {
|
||||
this.modalController().events.on("destroy", () => this.events.fire("notify_destroy"));
|
||||
|
||||
this.events.fire_async("query_available_groups");
|
||||
this.events.fire_async("query_client_permissions");
|
||||
|
||||
this.events.on(["action_cancel", "action_create"], () => this.modalController().destroy());
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
return <div className={cssStyle.container}>
|
||||
<GroupNameInput events={this.events} defaultSource={this.defaultSourceGroup} />
|
||||
<div className={cssStyle.row}>
|
||||
<GroupTypeSelector events={this.events} />
|
||||
<SourceGroupSelector events={this.events} defaultSource={this.defaultSourceGroup} />
|
||||
</div>
|
||||
<div className={cssStyle.buttons}>
|
||||
<Button color={"red"} onClick={() => this.events.fire("action_cancel")}><Translatable>Cancel</Translatable></Button>
|
||||
<CreateButton events={this.events} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
title(): string {
|
||||
return this.target === "server" ? tr("Create a new server group") : tr("Create a new channel group");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function spawnGroupCreate(connection: ConnectionHandler, target: "server" | "channel", sourceGroup: number = 0) {
|
||||
const modal = spawnReactModal(ModalGroupCreate, connection, target, sourceGroup);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
const stringifyError = error => {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorID.PERMISSION_ERROR)
|
||||
return tr("insufficient permissions");
|
||||
else
|
||||
return error.message + (error.extra_message ? " (" + error.extra_message + ")" : "");
|
||||
} else if(error instanceof Error) {
|
||||
return error.message;
|
||||
} else if(typeof error !== "string") {
|
||||
return tr("Lookup the console");
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
function initializeGroupCreateController(connection: ConnectionHandler, events: Registry<GroupCreateModalEvents>, target: "server" | "channel") {
|
||||
events.on("query_available_groups", event => {
|
||||
const groups = target === "server" ? connection.groups.serverGroups : connection.groups.channelGroups;
|
||||
|
||||
events.fire_async("query_available_groups_result", {
|
||||
groups: groups.map(e => {
|
||||
return {
|
||||
name: e.name,
|
||||
id: e.id,
|
||||
type: e.type === GroupType.TEMPLATE ? "template" : e.type === GroupType.QUERY ? "query" : "normal"
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
const notifyClientPermissions = () => events.fire_async("notify_client_permissions", {
|
||||
createQueryGroup: connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1),
|
||||
createTemplateGroup: connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1)
|
||||
});
|
||||
events.on("query_client_permissions", notifyClientPermissions);
|
||||
events.on("notify_destroy", connection.permissions.events.on("client_permissions_changed", notifyClientPermissions));
|
||||
|
||||
events.on("action_create", event => {
|
||||
let promise: Promise<CommandResult>;
|
||||
if(event.source <= 0) {
|
||||
/* real group create */
|
||||
promise = connection.serverConnection.send_command("servergroupadd", {
|
||||
name: event.name,
|
||||
type: event.target === "query" ? 2 : event.target === "template" ? 0 : 1
|
||||
});
|
||||
} else {
|
||||
/* group copy */
|
||||
promise = connection.serverConnection.send_command("servergroupcopy", {
|
||||
ssgid: event.source,
|
||||
name: event.name,
|
||||
type: event.target === "query" ? 2 : event.target === "template" ? 0 : 1
|
||||
});
|
||||
}
|
||||
promise.then(() => {
|
||||
createInfoModal(tr("Group has been created"), tr("The group has been successfully created.")).open();
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) {
|
||||
createErrorModal(tr("Failed to create group"),
|
||||
tra("Failed to create group.\nMissing permission {}", connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknwon"))).open();
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(tr("Failed to create group: %o"), error);
|
||||
createErrorModal(tr("Failed to create group"),
|
||||
tra("Failed to create group.\n{}", stringifyError(error))).open();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
padding: 0 1em 1em;
|
||||
|
||||
width: 40em;
|
||||
min-width: 10em;
|
||||
max-width: 100%;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
select {
|
||||
.hiddenOption {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sourceGroup, .targetGroup {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 5em;
|
||||
width: 10em;
|
||||
}
|
||||
|
||||
.sourceGroup {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,223 @@
|
|||
import {Modal, spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {Registry} from "tc-shared/events";
|
||||
import {useRef, useState} from "react";
|
||||
import {FlatSelect} from "tc-shared/ui/react-elements/InputField";
|
||||
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||
import * as React from "react";
|
||||
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||
import {GroupType} from "tc-shared/permission/GroupManager";
|
||||
import PermissionType from "tc-shared/permission/PermissionType";
|
||||
import {CommandResult, ErrorID} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
|
||||
const cssStyle = require("./ModalGroupPermissionCopy.scss");
|
||||
|
||||
export type GroupInfo = {
|
||||
id: number,
|
||||
name: string,
|
||||
type: "query" | "template" | "normal"
|
||||
};
|
||||
|
||||
export interface GroupPermissionCopyModalEvents {
|
||||
action_set_source: { group: number },
|
||||
action_set_target: { group: number }
|
||||
|
||||
action_cancel: {},
|
||||
action_copy: {
|
||||
source: number;
|
||||
target: number;
|
||||
}
|
||||
|
||||
query_available_groups: { },
|
||||
query_available_groups_result: {
|
||||
groups: GroupInfo[]
|
||||
},
|
||||
|
||||
query_client_permissions: {},
|
||||
notify_client_permissions: {
|
||||
createTemplateGroup: boolean,
|
||||
createQueryGroup: boolean
|
||||
},
|
||||
|
||||
notify_destroy: {}
|
||||
}
|
||||
|
||||
const GroupSelector = (props: { events: Registry<GroupPermissionCopyModalEvents>, defaultGroup: number, updateEvent: "action_set_source" | "action_set_target", label: string, className: string}) => {
|
||||
const [ selectedGroup, setSelectedGroup ] = useState(undefined);
|
||||
const [ permissions, setPermissions ] = useState<"loading" | { createTemplate, createQuery }>("loading");
|
||||
const [ exitingGroups, setExitingGroups ] = useState<"loading" | GroupInfo[]>("loading");
|
||||
|
||||
const refSelect = useRef<FlatSelect>();
|
||||
|
||||
props.events.reactUse("notify_client_permissions", event => setPermissions({
|
||||
createQuery: event.createQueryGroup,
|
||||
createTemplate: event.createTemplateGroup
|
||||
}));
|
||||
props.events.reactUse("query_available_groups_result", event => setExitingGroups(event.groups));
|
||||
props.events.reactUse(props.updateEvent, event => setSelectedGroup(event.group));
|
||||
|
||||
const groupName = (group: GroupInfo) => {
|
||||
let prefix = group.type === "template" ? "[T] " : group.type === "query" ? "[Q] " : "";
|
||||
return prefix + group.name + " (" + group.id + ")";
|
||||
};
|
||||
|
||||
const isLoading = exitingGroups === "loading" || permissions === "loading";
|
||||
if(!isLoading && selectedGroup === undefined)
|
||||
props.events.fire_async(props.updateEvent, {
|
||||
group: (exitingGroups as GroupInfo[]).findIndex(e => e.id === props.defaultGroup) === -1 ? 0 : props.defaultGroup
|
||||
});
|
||||
|
||||
return (
|
||||
<FlatSelect
|
||||
ref={refSelect}
|
||||
label={<Translatable>{props.label}</Translatable>}
|
||||
className={props.className}
|
||||
disabled={isLoading}
|
||||
value={isLoading || selectedGroup === undefined ? "-1" : selectedGroup.toString()}
|
||||
onChange={event => props.events.fire(props.updateEvent, { group: parseInt(event.target.value) })}
|
||||
>
|
||||
<option className={cssStyle.hiddenOption} value={"-1"}>{tr("loading...")}</option>
|
||||
<option className={cssStyle.hiddenOption} value={"0"}>{tr("Select a group")}</option>
|
||||
<optgroup label={tr("Query groups")} className={permissions === "loading" || !permissions.createQuery ? cssStyle.hiddenOption : ""} >
|
||||
{exitingGroups === "loading" ? undefined :
|
||||
exitingGroups.filter(e => e.type === "query").map(e => (
|
||||
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
<optgroup label={tr("Template groups")} className={permissions === "loading" || !permissions.createTemplate ? cssStyle.hiddenOption : ""} >
|
||||
{exitingGroups === "loading" ? undefined :
|
||||
exitingGroups.filter(e => e.type === "template").map(e => (
|
||||
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
<optgroup label={tr("Regular Groups")} >
|
||||
{exitingGroups === "loading" ? undefined :
|
||||
exitingGroups.filter(e => e.type === "normal").map(e => (
|
||||
<option key={"group-" + e.id} value={e.id.toString()}>{groupName(e)}</option>
|
||||
))
|
||||
}
|
||||
</optgroup>
|
||||
</FlatSelect>
|
||||
)
|
||||
};
|
||||
|
||||
const CopyButton = (props: { events: Registry<GroupPermissionCopyModalEvents> }) => {
|
||||
const [ sourceGroup, setSourceGroup ] = useState<number>(0);
|
||||
const [ targetGroup, setTargetGroup ] = useState<number>(0);
|
||||
|
||||
props.events.reactUse("action_set_source", event => setSourceGroup(event.group));
|
||||
props.events.reactUse("action_set_target", event => setTargetGroup(event.group));
|
||||
|
||||
return <Button color={"green"} disabled={sourceGroup === 0 || targetGroup === 0 || targetGroup === sourceGroup} onClick={() => {
|
||||
props.events.fire("action_copy", { source: sourceGroup, target: targetGroup });
|
||||
}}>
|
||||
<Translatable>Copy group permissions</Translatable>
|
||||
</Button>
|
||||
};
|
||||
|
||||
class ModalGroupPermissionCopy extends Modal {
|
||||
readonly events = new Registry<GroupPermissionCopyModalEvents>();
|
||||
|
||||
readonly defaultSource: number;
|
||||
readonly defaultTarget: number;
|
||||
|
||||
constructor(connection: ConnectionHandler, target: "server" | "channel", sourceGroup?: number, targetGroup?: number) {
|
||||
super();
|
||||
|
||||
this.defaultSource = sourceGroup;
|
||||
this.defaultTarget = targetGroup;
|
||||
|
||||
initializeGroupPermissionCopyController(connection, this.events, target);
|
||||
}
|
||||
|
||||
protected onInitialize() {
|
||||
this.modalController().events.on("destroy", () => this.events.fire("notify_destroy"));
|
||||
|
||||
this.events.fire_async("query_available_groups");
|
||||
this.events.fire_async("query_client_permissions");
|
||||
|
||||
this.events.on(["action_cancel", "action_copy"], () => this.modalController().destroy());
|
||||
}
|
||||
|
||||
renderBody() {
|
||||
return <div className={cssStyle.container}>
|
||||
<div className={cssStyle.row}>
|
||||
<GroupSelector events={this.events} defaultGroup={this.defaultSource} updateEvent={"action_set_source"} label={"Source group"} className={cssStyle.sourceGroup} />
|
||||
<GroupSelector events={this.events} defaultGroup={this.defaultTarget} updateEvent={"action_set_target"} label={"Target group"} className={cssStyle.targetGroup} />
|
||||
</div>
|
||||
<div className={cssStyle.buttons}>
|
||||
<Button color={"red"} onClick={() => this.events.fire("action_cancel")}><Translatable>Cancel</Translatable></Button>
|
||||
<CopyButton events={this.events} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
title(): string {
|
||||
return tr("Copy group permissions");
|
||||
}
|
||||
}
|
||||
|
||||
export function spawnModalGroupPermissionCopy(connection: ConnectionHandler, target: "channel" | "server", sourceGroup?: number, targetGroup?: number) {
|
||||
const modal = spawnReactModal(ModalGroupPermissionCopy, connection, target, sourceGroup, targetGroup);
|
||||
modal.show();
|
||||
}
|
||||
|
||||
const stringifyError = error => {
|
||||
if(error instanceof CommandResult) {
|
||||
if(error.id === ErrorID.PERMISSION_ERROR)
|
||||
return tr("insufficient permissions");
|
||||
else
|
||||
return error.message + (error.extra_message ? " (" + error.extra_message + ")" : "");
|
||||
} else if(error instanceof Error) {
|
||||
return error.message;
|
||||
} else if(typeof error !== "string") {
|
||||
return tr("Lookup the console");
|
||||
}
|
||||
return error;
|
||||
};
|
||||
|
||||
function initializeGroupPermissionCopyController(connection: ConnectionHandler, events: Registry<GroupPermissionCopyModalEvents>, target: "server" | "channel") {
|
||||
events.on("query_available_groups", event => {
|
||||
const groups = target === "server" ? connection.groups.serverGroups : connection.groups.channelGroups;
|
||||
|
||||
events.fire_async("query_available_groups_result", {
|
||||
groups: groups.map(e => {
|
||||
return {
|
||||
name: e.name,
|
||||
id: e.id,
|
||||
type: e.type === GroupType.TEMPLATE ? "template" : e.type === GroupType.QUERY ? "query" : "normal"
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
const notifyClientPermissions = () => events.fire_async("notify_client_permissions", {
|
||||
createQueryGroup: connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_QUERYGROUP).granted(1),
|
||||
createTemplateGroup: connection.permissions.neededPermission(PermissionType.B_SERVERINSTANCE_MODIFY_TEMPLATES).granted(1)
|
||||
});
|
||||
events.on("query_client_permissions", notifyClientPermissions);
|
||||
events.on("notify_destroy", connection.permissions.events.on("client_permissions_changed", notifyClientPermissions));
|
||||
|
||||
events.on("action_copy", event => {
|
||||
connection.serverConnection.send_command("servergroupcopy", {
|
||||
ssgid: event.source,
|
||||
tsgid: event.target
|
||||
}).then(() => {
|
||||
createInfoModal(tr("Group permissions have been copied"), tr("The group permissions have been successfully copied.")).open();
|
||||
}).catch(error => {
|
||||
if(error instanceof CommandResult && error.id === ErrorID.PERMISSION_ERROR) {
|
||||
createErrorModal(tr("Failed to copy group permissions"),
|
||||
tra("Failed to copy group permissions.\nMissing permission {}", connection.permissions.resolveInfo(parseInt(error.json["failed_permid"]))?.name || tr("unknwon"))).open();
|
||||
return;
|
||||
}
|
||||
|
||||
console.warn(tr("Failed to copy group permissions: %o"), error);
|
||||
createErrorModal(tr("Failed to copy group permissions"),
|
||||
tra("Failed to copy group permissions.\n{}", stringifyError(error))).open();
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1319,7 +1319,7 @@ function apply_server_groups(connection: ConnectionHandler, editor: AbstractPerm
|
|||
if(!!text.match(/^[0-9]+$/))
|
||||
return true;
|
||||
try {
|
||||
return atob(text).length >= 20;
|
||||
return atob(text).length == 20;
|
||||
} catch(error) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1349,7 +1349,6 @@ function apply_server_groups(connection: ConnectionHandler, editor: AbstractPerm
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
connection.serverConnection.send_command("servergroupaddclient", {
|
||||
sgid: current_group.id,
|
||||
cldbid: dbid
|
||||
|
@ -1411,7 +1410,8 @@ function apply_server_groups(connection: ConnectionHandler, editor: AbstractPerm
|
|||
}
|
||||
}
|
||||
|
||||
function spawnGroupAdd(server_group: boolean, permissions: PermissionManager, valid_name: (name: string, group_type: number) => boolean, callback: (group_name: string, group_type: number) => any) {
|
||||
/* Attention: This is used by the new permission editor! */
|
||||
export function spawnGroupAdd(server_group: boolean, permissions: PermissionManager, valid_name: (name: string, group_type: number) => boolean, callback: (group_name: string, group_type: number) => any) {
|
||||
let modal: Modal;
|
||||
modal = createModal({
|
||||
header: tr("Create a new group"),
|
||||
|
|
|
@ -4,6 +4,8 @@ export const senseless_server_group_permissions: PermissionType[] = [
|
|||
PermissionType.B_CHANNEL_GROUP_INHERITANCE_END
|
||||
];
|
||||
|
||||
/* TODO: All the needed permissions! */
|
||||
|
||||
const filter = (text, ignore_type) => Object.keys(PermissionType)
|
||||
.filter(e => e.toLowerCase().substr(ignore_type ? 1 : 0).startsWith(text)).map(e => PermissionType[e]);
|
||||
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
html:root {
|
||||
--modal-permissions-header-text: #e1e1e1;
|
||||
--modal-permissions-header-background: #19191b;
|
||||
--modal-permissions-header-hover: #4e4e4e;
|
||||
--modal-permissions-header-selected: #0073d4;
|
||||
|
||||
--modal-permission-right: #303036;
|
||||
--modal-permission-left: #222226;
|
||||
|
||||
--modal-permissions-entry-hover: #28282c;
|
||||
--modal-permissions-entry-selected: #111111;
|
||||
--modal-permissions-current-group: #101012;
|
||||
|
||||
--modal-permissions-buttons-background: #0f0f0f;
|
||||
--modal-permissions-buttons-hover: #262626;
|
||||
--modal-permissions-buttons-disabled: #1b1b1b;
|
||||
|
||||
--modal-permissions-seperator: #1e1e1e; /* the seperator for the "enter a unique id" and "client info" part */
|
||||
--modal-permissions-container-seperator: #222224; /* the seperator between left and right */
|
||||
|
||||
--modal-permissions-icon-select: #121213;
|
||||
--modal-permissions-icon-select-border: #0d0d0d;
|
||||
--modal-permissions-icon-select-hover: #17171a;
|
||||
--modal-permissions-icon-select-hover-border: #333333;
|
||||
|
||||
--modal-permission-no-permnissions:#18171c;
|
||||
--modal-permissions-table-border: #1e2025;
|
||||
|
||||
--modal-permissions-table-header: #303036;
|
||||
--modal-permissions-table-row-odd: #303036;
|
||||
--modal-permissions-table-row-even: #25252a;
|
||||
--modal-permissions-table-row-hover: #343a47;
|
||||
|
||||
--modal-permissions-table-header-text: #e1e1e1;
|
||||
--modal-permissions-table-row-text: #535455;
|
||||
--modal-permissions-table-entry-active-text: #e1e1e1;
|
||||
--modal-permissions-table-entry-group-text: #e1e1e1;
|
||||
|
||||
--modal-permissions-table-input: #e1e1e1;
|
||||
--modal-permissions-table-input-focus: #3f7dbf;
|
||||
}
|
||||
|
||||
.container {
|
||||
@include user-select(none);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
width: 1000em;
|
||||
min-width: 20em;
|
||||
max-width: 100%;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
|
||||
.contextContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
&.left {
|
||||
min-width: 10em;
|
||||
min-height: 10em;
|
||||
overflow: hidden;
|
||||
background-color: var(--modal-permission-left);
|
||||
}
|
||||
|
||||
&.right {
|
||||
min-width: 30em;
|
||||
background-color: var(--modal-permission-right);
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
height: 4em;
|
||||
background-color: var(--modal-permissions-header-background);
|
||||
color: var(--modal-permissions-header-text);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.entry {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
text-align: center;
|
||||
|
||||
height: 100%;
|
||||
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
&.tabSelector {
|
||||
min-width: 8em;
|
||||
|
||||
.entry {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
cursor: pointer;
|
||||
padding-bottom: 2px;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--modal-permissions-header-hover);
|
||||
|
||||
padding-bottom: 0;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
|
||||
margin-right: -10em;
|
||||
margin-left: -10em;
|
||||
margin-bottom: -.2em;
|
||||
bottom: 0;
|
||||
|
||||
height: 100%;
|
||||
width: calc(100% + 20em);
|
||||
|
||||
box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border: none;
|
||||
border-bottom: 2px solid var(--modal-permissions-header-selected);
|
||||
|
||||
padding-bottom: 0;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
|
||||
margin-right: -10em;
|
||||
margin-left: -10em;
|
||||
margin-bottom: -.2em;
|
||||
bottom: 0;
|
||||
|
||||
height: 100%;
|
||||
width: calc(100% + 20em);
|
||||
|
||||
box-shadow: inset 0px -1.2em 3em -20px var(--modal-permissions-header-selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.activeTabInfo {
|
||||
min-width: 6em;
|
||||
font-weight: bold;
|
||||
|
||||
.entry {
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
> * {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,458 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
html:root {
|
||||
--modal-permission-loading: #666;
|
||||
--modal-permission-error: #666161;
|
||||
}
|
||||
|
||||
.containerMenuBar {
|
||||
padding-top: .5em;
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
height: 3em;
|
||||
box-sizing: content-box;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
.clients {
|
||||
width: 12em;
|
||||
min-width: 3em;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 0;
|
||||
|
||||
margin-right: 1em;
|
||||
align-self: center;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filter {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-width: 4em;
|
||||
|
||||
align-self: center;
|
||||
|
||||
padding-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
.label {
|
||||
top: 0.5em;
|
||||
}
|
||||
|
||||
&:focus-within .label, .labelFloating {
|
||||
top: -0.4em!important;
|
||||
}
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
margin-left: .5em;
|
||||
font-size: .9em;
|
||||
padding-top: .8em; /* since we've only one switch currently */
|
||||
}
|
||||
|
||||
.containerIconSelect {
|
||||
position: relative;
|
||||
|
||||
height: 2.5em;
|
||||
|
||||
border-radius: .2em;
|
||||
margin-left: 1em;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
cursor: pointer;
|
||||
background-color: var(--modal-permissions-icon-select);
|
||||
border: 1px solid var(--modal-permissions-icon-select-border);
|
||||
|
||||
.preview {
|
||||
height: 100%;
|
||||
width: 3em;
|
||||
|
||||
border: none;
|
||||
border-right: 1px solid var(--modal-permissions-icon-select-border);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
> div {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
> img {
|
||||
align-self: center;
|
||||
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
@include transition(border-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
.containerDropdown {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
height: 100%;
|
||||
width: 1.5em;
|
||||
|
||||
.button {
|
||||
text-align: center;
|
||||
|
||||
:global(.arrow) {
|
||||
border-color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
display: none;
|
||||
position: absolute;
|
||||
width: max-content;
|
||||
|
||||
top: calc(2.5em - 2px);
|
||||
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
background-color: var(--modal-permissions-icon-select);
|
||||
border: 1px solid var(--modal-permissions-icon-select-border);
|
||||
border-radius: .2em 0 .2em .2em;
|
||||
|
||||
right: -1px;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
.entry {
|
||||
padding: .5em;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--modal-permissions-icon-select-border);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--modal-permissions-icon-select-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-bottom-right-radius: 0;
|
||||
.dropdown {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--modal-permissions-icon-select-hover);
|
||||
border-color: var(--modal-permissions-icon-select-hover-border);
|
||||
|
||||
.preview {
|
||||
border-color: var(--modal-permissions-icon-select-hover-border);
|
||||
}
|
||||
}
|
||||
|
||||
@include transition(border-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
.permissionTable {
|
||||
@include user-select(none);
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
min-height: 6em;
|
||||
height: 50em;
|
||||
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
padding-left: .5em;
|
||||
padding-right: .5em;
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 100%;
|
||||
height: 2em;
|
||||
|
||||
border: none;
|
||||
|
||||
border-bottom: 1px solid var(--modal-permissions-table-border);
|
||||
|
||||
@mixin fixed-column($name, $width) {
|
||||
.column#{$name} {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: $width;
|
||||
|
||||
align-items: center;
|
||||
|
||||
padding-left: 1em;
|
||||
|
||||
border: none;
|
||||
border-right: 1px solid var(--modal-permissions-table-border);
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
a {
|
||||
max-width: 100%;
|
||||
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include fixed-column(Name, 6em);
|
||||
@include fixed-column(Value, 6em);
|
||||
@include fixed-column(Skip, 5em);
|
||||
@include fixed-column(Negate, 5em);
|
||||
@include fixed-column(Granted, 6em);
|
||||
|
||||
.columnName {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.groupName {
|
||||
margin-left: .5em;
|
||||
}
|
||||
}
|
||||
|
||||
.columnGranted {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
|
||||
&.active {
|
||||
color: var(--modal-permissions-table-entry-active-text)!important;
|
||||
}
|
||||
|
||||
&.group {
|
||||
color: var(--modal-permissions-table-entry-group-text)!important;
|
||||
font-weight: bold;
|
||||
|
||||
:global(.arrow) {
|
||||
cursor: pointer;
|
||||
border-color: var(--modal-permissions-table-entry-active-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.permission {}
|
||||
}
|
||||
|
||||
.header {
|
||||
.row {
|
||||
background-color: var(--modal-permissions-table-header);
|
||||
color: var(--modal-permissions-table-header-text);
|
||||
|
||||
font-weight: bold;
|
||||
|
||||
.columnGranted {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
position: relative;
|
||||
min-height: 6em; /* TODO: Width */
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
overflow-y: scroll;
|
||||
overflow-x: auto;
|
||||
|
||||
@include chat-scrollbar-vertical();
|
||||
@include chat-scrollbar-horizontal();
|
||||
|
||||
.row {
|
||||
position: absolute;
|
||||
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
color: var(--modal-permissions-table-row-text);
|
||||
background-color: var(--modal-permissions-table-row-odd);
|
||||
|
||||
&.even {
|
||||
background-color: var(--modal-permissions-table-row-even);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--modal-permissions-table-row-hover);
|
||||
}
|
||||
|
||||
input[type="number"] {
|
||||
color: var(--modal-permissions-table-input);
|
||||
|
||||
outline: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
|
||||
height: 1.5em;
|
||||
width: 5em; /* the column width minus one */
|
||||
|
||||
/* fix the column padding */
|
||||
padding-left: 1em;
|
||||
margin-left: -.5em; /* have a bit of space on both sides */
|
||||
|
||||
border-bottom: 2px solid transparent;
|
||||
|
||||
@include transition(border-bottom-color $button_hover_animation_time ease-in-out);
|
||||
|
||||
&:not(.applying):focus {
|
||||
border-bottom-color: var(--modal-permissions-table-input-focus);
|
||||
}
|
||||
|
||||
|
||||
&.applying {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* We cant use this effect here because the odd/even effect would be a bit crazy then */
|
||||
//@include transition(background-color $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
.spaceAllocator {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
z-index: 1;
|
||||
background-color: var(--modal-permission-right);
|
||||
|
||||
padding-top: 2em;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
font-size: 1.6em;
|
||||
|
||||
color: var(--modal-permission-loading);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.error {
|
||||
a {
|
||||
color: var(--modal-permission-error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
z-index: 1;
|
||||
background: var(--modal-permission-right);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.unset {}
|
||||
&.noPermissions {
|
||||
justify-content: flex-start;
|
||||
padding-top: 2em;
|
||||
font-size: 1em;
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
color: var(--modal-permission-no-permnissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.containerFooter {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
|
||||
padding: .5em;
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
* {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
div {
|
||||
margin-right: .5em;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,312 @@
|
|||
@import "../../../../css/static/mixin";
|
||||
@import "../../../../css/static/properties";
|
||||
|
||||
.containerList {
|
||||
color: var(--text);
|
||||
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
overflow: auto;
|
||||
@include chat-scrollbar-vertical();
|
||||
@include chat-scrollbar-horizontal();
|
||||
|
||||
width: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
.entries {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
height: max-content;
|
||||
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
|
||||
.entry {
|
||||
padding-left: .25em;
|
||||
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--modal-permissions-entry-hover);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: var(--modal-permissions-entry-selected);
|
||||
}
|
||||
|
||||
> * {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
@include transition(background-color .25s ease-in-out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sideContainer {
|
||||
height: 0; /* will expend due to flex grow */
|
||||
width: 100%;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.containerServerGroups, .containerChannelGroups {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.list {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: 2em;
|
||||
|
||||
.name {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
|
||||
:global(.icon_em) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
position: relative;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
height: 2.5em;
|
||||
width: 100%;
|
||||
|
||||
.button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-around;
|
||||
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
background-color: var(--modal-permissions-buttons-background);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--modal-permissions-buttons-hover);
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--modal-permissions-buttons-disabled);
|
||||
}
|
||||
|
||||
@include transition(background-color .25s ease-in-out);
|
||||
|
||||
img {
|
||||
width: 2.2em;
|
||||
height: 2.2em;
|
||||
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$animation_length: .3s;
|
||||
.containerGroupList {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
&.hidden {
|
||||
@include transform(translateX(-100%));
|
||||
}
|
||||
@include transition($animation_length ease-in-out);
|
||||
}
|
||||
|
||||
.containerClientList {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
|
||||
.selectedGroup {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
z-index: 2;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
background-color: var(--modal-permissions-current-group);
|
||||
color: var(--text);
|
||||
padding-left: .25em;
|
||||
|
||||
height: 1.5em;
|
||||
font-size: 1.125em;
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
|
||||
height: 100%;
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
height: 1.5em;
|
||||
}
|
||||
}
|
||||
|
||||
.overlay {
|
||||
z-index: 1;
|
||||
|
||||
position: absolute;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
|
||||
padding-top: 2em;
|
||||
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
color: #666666;
|
||||
background-color: var(--modal-permission-left);
|
||||
|
||||
a {
|
||||
text-align: center;
|
||||
font-size: 1.2em;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
&.error {
|
||||
color: #6b6161;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
@include transform(translateX(100%));
|
||||
}
|
||||
@include transition($animation_length ease-in-out);
|
||||
}
|
||||
}
|
||||
|
||||
.containerChannels {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.containerClient {
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.containerChannelClient {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.containerChannels, .containerClient, .containerChannelClient {
|
||||
.listChannels {
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
|
||||
min-height: 2em;
|
||||
|
||||
.name {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.clientSelect {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
padding: .25em;
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 2px solid var(--modal-permissions-seperator);
|
||||
|
||||
margin-left: -.25em;
|
||||
margin-right: -.25em;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
margin-top: -.75em;
|
||||
}
|
||||
|
||||
.infoField {
|
||||
margin-bottom: .25em;
|
||||
margin-top: -.5em;
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -27,6 +27,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
|||
render() {
|
||||
if(this.props.hidden)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={this.classList(
|
||||
|
@ -35,7 +36,7 @@ export class Button extends ReactComponentBase<ButtonProperties, ButtonState> {
|
|||
cssStyle["type-" + this.props.type] || cssStyle["type-normal"],
|
||||
this.props.className
|
||||
)}
|
||||
disabled={this.state.disabled || this.props.disabled}
|
||||
disabled={typeof this.state.disabled === "boolean" ? this.state.disabled : this.props.disabled}
|
||||
onClick={this.props.onClick}
|
||||
>
|
||||
{this.props.children}
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
@import "../../../css/static/mixin";
|
||||
@import "../../../css/static/properties";
|
||||
|
||||
$separator_thickness: 5px;
|
||||
$animation_separator_length: .1s;
|
||||
.separator {
|
||||
@include transition(all $animation_separator_length ease-in-out);
|
||||
background: #1e1e1e;
|
||||
|
||||
display: block;
|
||||
|
||||
flex-grow: 0!important;
|
||||
flex-shrink: 0!important;
|
||||
|
||||
&.vertical {
|
||||
height: $separator_thickness;
|
||||
min-height: $separator_thickness!important;
|
||||
max-height: $separator_thickness!important;
|
||||
|
||||
//width: 100%;
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
width: $separator_thickness;
|
||||
min-width: $separator_thickness!important;
|
||||
max-width: $separator_thickness!important;
|
||||
|
||||
//height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.documentActiveClass {
|
||||
@include user-select(none);
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import * as React from "react";
|
||||
import {settings} from "tc-shared/settings";
|
||||
const cssStyle = require("./ContextDivider.scss");
|
||||
|
||||
export interface ContextDividerProperties {
|
||||
id: string;
|
||||
direction: "vertical" | "horizontal";
|
||||
|
||||
defaultValue: number; /* [0;100] */
|
||||
|
||||
separatorClassName?: string;
|
||||
separatorActiveClassName?: string;
|
||||
|
||||
children: [React.ReactElement, React.ReactElement];
|
||||
}
|
||||
|
||||
export interface ContextDividerState {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export class ContextDivider extends React.Component<ContextDividerProperties, ContextDividerState> {
|
||||
private readonly refSeparator = React.createRef<HTMLDivElement>();
|
||||
private readonly listenerMove;
|
||||
private readonly listenerUp;
|
||||
|
||||
private value;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
active: false
|
||||
};
|
||||
|
||||
this.value = this.props.defaultValue;
|
||||
try {
|
||||
const config = JSON.parse(settings.global("separator-settings-" + this.props.id));
|
||||
if(typeof config.value !== "number")
|
||||
throw "Invalid value";
|
||||
|
||||
this.value = config.value;
|
||||
} catch (e) { }
|
||||
|
||||
this.listenerMove = (event: MouseEvent | TouchEvent) => {
|
||||
const separator = this.refSeparator.current;
|
||||
if(!separator) {
|
||||
this.setState({ active: false });
|
||||
return;
|
||||
}
|
||||
const parentBounds = separator.parentElement.getBoundingClientRect();
|
||||
|
||||
const min = this.props.direction === "horizontal" ? parentBounds.left : parentBounds.top;
|
||||
const max = this.props.direction === "horizontal" ? parentBounds.left + parentBounds.width : parentBounds.top + parentBounds.height;
|
||||
const current = event instanceof MouseEvent ?
|
||||
(this.props.direction === "horizontal" ? event.pageX : event.pageY) :
|
||||
(this.props.direction === "horizontal" ? event.touches[event.touches.length - 1].clientX : event.touches[event.touches.length - 1].clientY);
|
||||
|
||||
/*
|
||||
const previous_offset = previous_element.offset();
|
||||
const next_offset = next_element.offset();
|
||||
|
||||
const min = vertical ? Math.min(previous_offset.left, next_offset.left) : Math.min(previous_offset.top, next_offset.top);
|
||||
const max = vertical ?
|
||||
Math.max(previous_offset.left + previous_element.width(), next_offset.left + next_element.width()) :
|
||||
Math.max(previous_offset.top + previous_element.height(), next_offset.top + next_element.height());
|
||||
*/
|
||||
|
||||
if(current < min) {
|
||||
this.value = 0;
|
||||
} else if(current < max) {
|
||||
const x_offset = current - min;
|
||||
const x_offset_max = max - min;
|
||||
|
||||
this.value = x_offset * 100 / x_offset_max;
|
||||
} else {
|
||||
this.value = 100;
|
||||
}
|
||||
|
||||
settings.changeGlobal("separator-settings-" + this.props.id, JSON.stringify({
|
||||
value: this.value
|
||||
}));
|
||||
this.applySeparator(separator.previousSibling as HTMLElement, separator.nextSibling as HTMLElement);
|
||||
};
|
||||
|
||||
this.listenerUp = () => this.stopMovement();
|
||||
}
|
||||
|
||||
render() {
|
||||
let separatorClassNames = cssStyle.separator + " " + (this.props.separatorClassName || "");
|
||||
|
||||
if(this.props.direction === "vertical")
|
||||
separatorClassNames += " " + cssStyle.vertical;
|
||||
else
|
||||
separatorClassNames += " " + cssStyle.horizontal;
|
||||
|
||||
if(this.state.active && this.props.separatorClassName)
|
||||
separatorClassNames += " " + this.props.separatorClassName;
|
||||
|
||||
return [
|
||||
this.props.children[0],
|
||||
<div key={"context-separator"} ref={this.refSeparator} className={separatorClassNames} onMouseDown={e => this.startMovement(e)} onTouchStart={e => this.startMovement(e)} />,
|
||||
this.props.children[1]
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
const separator = this.refSeparator.current;
|
||||
if(!separator) return;
|
||||
|
||||
this.applySeparator(separator.previousSibling as HTMLElement, separator.nextSibling as HTMLElement);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.stopMovement();
|
||||
}
|
||||
|
||||
private startMovement(event: React.MouseEvent | React.TouchEvent) {
|
||||
this.setState({ active: true });
|
||||
|
||||
document.addEventListener('mousemove', this.listenerMove);
|
||||
document.addEventListener('touchmove', this.listenerMove);
|
||||
|
||||
document.addEventListener('mouseup', this.listenerUp);
|
||||
document.addEventListener('touchend', this.listenerUp);
|
||||
document.addEventListener('touchcancel', this.listenerUp);
|
||||
document.documentElement.classList.add(cssStyle.documentActiveClass);
|
||||
|
||||
this.listenerMove(event.nativeEvent);
|
||||
}
|
||||
|
||||
private stopMovement() {
|
||||
this.setState({ active: false });
|
||||
document.removeEventListener('mousemove', this.listenerMove);
|
||||
document.removeEventListener('touchmove', this.listenerMove);
|
||||
|
||||
document.removeEventListener('mouseup', this.listenerUp);
|
||||
document.removeEventListener('touchend', this.listenerUp);
|
||||
document.removeEventListener('touchcancel', this.listenerUp);
|
||||
document.documentElement.classList.remove(cssStyle.documentActiveClass);
|
||||
}
|
||||
|
||||
private applySeparator(previousElement: HTMLElement, nextElement: HTMLElement) {
|
||||
if(!this.refSeparator.current || !previousElement || !nextElement)
|
||||
return;
|
||||
|
||||
if(this.props.direction === "horizontal") {
|
||||
const center = this.refSeparator.current.clientWidth;
|
||||
|
||||
previousElement.style.width = `calc(${this.value}% - ${center / 2}px)`;
|
||||
nextElement.style.width = `calc(${100 - this.value}% - ${center / 2}px)`;
|
||||
} else {
|
||||
const center = this.refSeparator.current.clientHeight;
|
||||
|
||||
previousElement.style.height = `calc(${this.value}% - ${center / 2}px)`;
|
||||
nextElement.style.height = `calc(${100 - this.value}% - ${center / 2}px)`;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -39,12 +39,12 @@ export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
|||
render() {
|
||||
const icon = this.props.icon;
|
||||
if(!icon || icon.status === "empty" || icon.status === "destroyed")
|
||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
return <div key={"empty"} className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
else if(icon.status === "loaded") {
|
||||
if(icon.icon_id >= 0 && icon.icon_id <= 1000) {
|
||||
if(icon.icon_id === 0)
|
||||
return <div className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
return <div className={"icon_em client-group_" + icon.icon_id} />;
|
||||
return <div key={"loaded-empty"} className={"icon-container icon-empty"} title={this.props.title} />;
|
||||
return <div key={"loaded"} className={"icon_em client-group_" + icon.icon_id} />;
|
||||
}
|
||||
return <div key={"icon"} className={"icon-container"}><img style={{ maxWidth: "100%", maxHeight: "100%" }} src={icon.loaded_url} alt={this.props.title || ("icon " + icon.icon_id)} /></div>;
|
||||
} else if(icon.status === "loading")
|
||||
|
@ -60,4 +60,9 @@ export class LocalIconRenderer extends React.Component<LoadedIconRenderer, {}> {
|
|||
componentWillUnmount(): void {
|
||||
this.props.icon?.status_change_callbacks.remove(this.callback_state_update);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<LoadedIconRenderer>, prevState: Readonly<{}>, snapshot?: any): void {
|
||||
prevProps.icon?.status_change_callbacks.remove(this.callback_state_update);
|
||||
this.props.icon?.status_change_callbacks.push(this.callback_state_update);
|
||||
}
|
||||
}
|
|
@ -2,33 +2,33 @@
|
|||
@import "../../../css/static/properties";
|
||||
|
||||
html:root {
|
||||
--input-field-border: #111112;
|
||||
--input-field-background: #121213;
|
||||
--input-field-text: #b3b3b3;
|
||||
--input-field-placeholder: #606060;
|
||||
--boxed-input-field-border: #111112;
|
||||
--boxed-input-field-background: #121213;
|
||||
--boxed-input-field-text: #b3b3b3;
|
||||
--boxed-input-field-placeholder: #606060;
|
||||
|
||||
--input-field-disabled-background: #1a1819;
|
||||
--boxed-input-field-disabled-background: #1a1819;
|
||||
|
||||
--input-field-focus-border: #111112;
|
||||
--input-field-focus-background: #121213;
|
||||
--input-field-focus-text: #b3b3b3;
|
||||
--boxed-input-field-focus-border: #111112;
|
||||
--boxed-input-field-focus-background: #121213;
|
||||
--boxed-input-field-focus-text: #b3b3b3;
|
||||
|
||||
--input-field-invalid-border: #721c1c;
|
||||
--input-field-invalid-background: #180d0d;
|
||||
--input-field-invalid-text: #b3b3b3;
|
||||
--boxed-input-field-invalid-border: #721c1c;
|
||||
--boxed-input-field-invalid-background: #180d0d;
|
||||
--boxed-input-field-invalid-text: #b3b3b3;
|
||||
}
|
||||
|
||||
.container {
|
||||
.containerBoxed {
|
||||
border-radius: .2em;
|
||||
border: 1px solid var(--input-field-border);
|
||||
border: 1px solid var(--boxed-input-field-border);
|
||||
|
||||
background-color: var(--input-field-background);
|
||||
background-color: var(--boxed-input-field-background);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: stretch;
|
||||
|
||||
color: var(--input-field-text);
|
||||
color: var(--boxed-input-field-text);
|
||||
|
||||
&.size-normal {
|
||||
height: 2em;
|
||||
|
@ -43,7 +43,7 @@ html:root {
|
|||
}
|
||||
|
||||
@include placeholder(&) {
|
||||
color: var(--input-field-placeholder);
|
||||
color: var(--boxed-input-field-placeholder);
|
||||
};
|
||||
|
||||
.prefix {
|
||||
|
@ -66,18 +66,18 @@ html:root {
|
|||
}
|
||||
|
||||
&.is-invalid {
|
||||
background-color: var(--input-field-invalid-background);
|
||||
border-color: var(--input-field-invalid-border);
|
||||
color: var(--input-field-invalid-text);
|
||||
background-color: var(--boxed-input-field-invalid-background);
|
||||
border-color: var(--boxed-input-field-invalid-border);
|
||||
color: var(--boxed-input-field-invalid-text);
|
||||
|
||||
background-image: unset!important;
|
||||
}
|
||||
|
||||
&:focus, &:focus-within {
|
||||
background-color: var(--input-field-focus-background);
|
||||
border-color: var(--input-field-focus-border);
|
||||
background-color: var(--boxed-input-field-focus-background);
|
||||
border-color: var(--boxed-input-field-focus-border);
|
||||
|
||||
color: var(--input-field-focus-text);
|
||||
color: var(--boxed-input-field-focus-text);
|
||||
|
||||
.prefix {
|
||||
width: 0;
|
||||
|
@ -123,7 +123,7 @@ html:root {
|
|||
}
|
||||
|
||||
&.disabled, &:disabled {
|
||||
background-color: var(--input-field-disabled-background);
|
||||
background-color: var(--boxed-input-field-disabled-background);
|
||||
}
|
||||
|
||||
&.noRightIcon {
|
||||
|
@ -139,4 +139,136 @@ html:root {
|
|||
}
|
||||
|
||||
@include transition($button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
.containerFlat {
|
||||
position: relative;
|
||||
|
||||
padding-top: 1.75rem; /* the label above (might be floating) */
|
||||
margin-bottom: 1rem; /* for invalid label/help label */
|
||||
|
||||
label {
|
||||
color: #999999;
|
||||
|
||||
top: 1rem;
|
||||
left: 0;
|
||||
font-size: .75rem;
|
||||
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
transition: all .3s ease;
|
||||
|
||||
line-height: 1;
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
max-width: 100%;
|
||||
|
||||
&.type-floating {
|
||||
will-change: left, top, contents;
|
||||
color: #999999;
|
||||
top: 2.42rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
&.type-static {
|
||||
top: 1rem;
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
@include transition(color $button_hover_animation_time ease-in-out, top $button_hover_animation_time ease-in-out, font-size $button_hover_animation_time ease-in-out);
|
||||
}
|
||||
|
||||
&:focus-within, &.isFilled {
|
||||
label {
|
||||
color: #3c74a2;
|
||||
|
||||
&.type-floating {
|
||||
font-size: .75rem;
|
||||
top: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input, select {
|
||||
display: block;
|
||||
|
||||
height: 2.25em;
|
||||
width: 100%;
|
||||
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
|
||||
color: #cdd1d0;
|
||||
|
||||
background: no-repeat bottom, 50% calc(100% - 1px);
|
||||
background-image: linear-gradient(0deg, #008aff 2px, rgba(0, 150, 136, 0) 0), linear-gradient(0deg, #393939 1px, transparent 0);
|
||||
background-clip: padding-box;
|
||||
background-size: 0 100%, 100% 100%;
|
||||
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
|
||||
transition: background 0s ease-out;
|
||||
padding: .4375rem 0;
|
||||
|
||||
@include transition(all .15s ease-in-out);
|
||||
|
||||
&:focus {
|
||||
background-size: 100% 100%, 100% 100%;
|
||||
transition-duration: .3s;
|
||||
|
||||
color: #ced3d3;
|
||||
background-color: transparent;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
option, optgroup {
|
||||
background: #121213;
|
||||
}
|
||||
}
|
||||
|
||||
.invalidFeedback {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
margin-top: .25rem;
|
||||
font-size: 80%;
|
||||
color: #f44336;
|
||||
|
||||
@include transition(opacity .25s ease-in-out);
|
||||
}
|
||||
|
||||
&.isInvalid {
|
||||
input, select {
|
||||
background-image: linear-gradient(0deg, #d50000 2px,rgba(213,0,0,0) 0),linear-gradient(0deg,rgba(241,1,1,.61) 1px,transparent 0);
|
||||
}
|
||||
|
||||
.invalidFeedback {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
label {
|
||||
color: #f44336!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.help {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 100%;
|
||||
margin-top: .25rem;
|
||||
|
||||
font-size: .75em;
|
||||
|
||||
@include transition(opacity .25s ease-in-out);
|
||||
}
|
||||
|
||||
input:focus-within ~ .help, select:focus-within ~ .help {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -51,7 +51,7 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
|||
<div
|
||||
draggable={false}
|
||||
className={
|
||||
cssStyle.container + " " +
|
||||
cssStyle.containerBoxed + " " +
|
||||
cssStyle["size-" + (this.props.size || "normal")] +
|
||||
(this.state.disabled || this.props.disabled ? cssStyle.disabled : "") + " " +
|
||||
(this.state.isInvalid || this.props.isInvalid ? cssStyle.isInvalid : "") + " " +
|
||||
|
@ -102,4 +102,217 @@ export class BoxedInputField extends React.Component<BoxedInputFieldProperties,
|
|||
if(this.props.onBlur)
|
||||
this.props.onBlur();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface FlatInputFieldProperties {
|
||||
defaultValue?: string;
|
||||
placeholder?: string;
|
||||
|
||||
className?: string;
|
||||
|
||||
label?: string | React.ReactElement;
|
||||
labelType?: "static" | "floating";
|
||||
labelClassName?: string;
|
||||
labelFloatingClassName?: string;
|
||||
|
||||
help?: string | React.ReactElement;
|
||||
helpClassName?: string;
|
||||
|
||||
invalidClassName?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
editable?: boolean;
|
||||
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
|
||||
onChange?: (newValue?: string) => void;
|
||||
onInput?: (newValue?: string) => void;
|
||||
|
||||
finishOnEnter?: boolean;
|
||||
}
|
||||
|
||||
export interface FlatInputFieldState {
|
||||
filled: boolean;
|
||||
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
editable?: boolean;
|
||||
|
||||
isInvalid: boolean;
|
||||
invalidMessage: string | React.ReactElement;
|
||||
}
|
||||
|
||||
export class FlatInputField extends React.Component<FlatInputFieldProperties, FlatInputFieldState> {
|
||||
private refInput = React.createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isInvalid: false,
|
||||
filled: !!this.props.defaultValue,
|
||||
invalidMessage: ""
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
|
||||
const readOnly = typeof this.state.editable === "boolean" ? !this.state.editable : typeof this.props.editable === "boolean" ? !this.props.editable : false;
|
||||
const placeholder = typeof this.state.placeholder === "string" ? this.state.placeholder : typeof this.props.placeholder === "string" ? this.props.placeholder : undefined;
|
||||
return (
|
||||
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.state.filled ? cssStyle.isFilled : "") + " " + (this.props.className || "")}>
|
||||
{this.props.label ?
|
||||
<label className={
|
||||
cssStyle["type-" + (this.props.labelType || "static")] + " " +
|
||||
(this.props.labelClassName || "") + " " +
|
||||
(this.props.labelFloatingClassName && this.state.filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
|
||||
<input
|
||||
defaultValue={this.props.defaultValue}
|
||||
type={"text"}
|
||||
ref={this.refInput}
|
||||
readOnly={readOnly}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
onChange={() => this.onChange()}
|
||||
onInput={e => this.props.onInput && this.props.onInput(e.currentTarget.value)}
|
||||
onKeyPress={e => this.props.finishOnEnter && e.key === "Enter" && this.refInput.current?.blur()}
|
||||
/>
|
||||
{this.state.invalidMessage ? <small className={cssStyle.invalidFeedback + " " + (this.props.invalidClassName || "")}>{this.state.invalidMessage}</small> : undefined}
|
||||
{this.props.help ? <small className={cssStyle.help + " " + (this.props.helpClassName || "")}>{this.props.help}</small> : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onChange() {
|
||||
const value = this.refInput.current?.value;
|
||||
this.setState({ filled: !!value});
|
||||
|
||||
this.props.onChange && this.props.onChange(value);
|
||||
}
|
||||
|
||||
value() {
|
||||
return this.refInput.current?.value;
|
||||
}
|
||||
|
||||
setValue(value: string | undefined) {
|
||||
this.refInput.current.value = typeof value === "undefined" ? "" : value;
|
||||
}
|
||||
|
||||
inputElement() : HTMLInputElement | undefined {
|
||||
return this.refInput.current;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refInput.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export interface FlatSelectProperties {
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
|
||||
className?: string;
|
||||
|
||||
label?: string | React.ReactElement;
|
||||
labelClassName?: string;
|
||||
|
||||
help?: string | React.ReactElement;
|
||||
helpClassName?: string;
|
||||
|
||||
invalidClassName?: string;
|
||||
|
||||
disabled?: boolean;
|
||||
editable?: boolean;
|
||||
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
|
||||
onChange?: (event?: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
}
|
||||
|
||||
export interface FlatSelectFieldState {
|
||||
disabled?: boolean;
|
||||
|
||||
isInvalid: boolean;
|
||||
invalidMessage: string | React.ReactElement;
|
||||
}
|
||||
|
||||
export class FlatSelect extends React.Component<FlatSelectProperties, FlatSelectFieldState> {
|
||||
private refSelect = React.createRef<HTMLSelectElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isInvalid: false,
|
||||
invalidMessage: ""
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
|
||||
return (
|
||||
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.props.className || "")}>
|
||||
{this.props.label ?
|
||||
<label className={cssStyle["type-static"] + " " + (this.props.labelClassName || "")}>{this.props.label}</label> : undefined}
|
||||
<select
|
||||
ref={this.refSelect}
|
||||
|
||||
value={this.props.value}
|
||||
defaultValue={this.props.defaultValue}
|
||||
disabled={disabled}
|
||||
|
||||
onFocus={this.props.onFocus}
|
||||
onBlur={this.props.onBlur}
|
||||
onChange={e => this.props.onChange && this.props.onChange(e)}
|
||||
>
|
||||
{this.props.children}
|
||||
</select>
|
||||
{this.state.invalidMessage ? <small className={cssStyle.invalidFeedback + " " + (this.props.invalidClassName || "")}>{this.state.invalidMessage}</small> : undefined}
|
||||
{this.props.help ? <small className={cssStyle.help + " " + (this.props.helpClassName || "")}>{this.props.help}</small> : undefined}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
selectElement() : HTMLSelectElement | undefined {
|
||||
return this.refSelect.current;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refSelect.current?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -170,6 +170,10 @@ class ModalImpl extends React.PureComponent<{ controller: ModalController }, {
|
|||
}
|
||||
}
|
||||
|
||||
export function spawnReactModal<ModalClass extends Modal, T>(modalClass: new (T) => ModalClass, properties?: T) : ModalController<ModalClass> {
|
||||
return new ModalController(new modalClass(properties));
|
||||
export function spawnReactModal<ModalClass extends Modal, A1>(modalClass: new (..._: [A1]) => ModalClass, arg1: A1) : ModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends Modal, A1, A2>(modalClass: new (..._: [A1, A2]) => ModalClass, arg1: A1, arg2: A2) : ModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends Modal, A1, A2, A3>(modalClass: new (..._: [A1, A2, A3]) => ModalClass, arg1: A1, arg2: A2, arg3: A3) : ModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends Modal, A1, A2, A3, A4>(modalClass: new (..._: [A1, A2, A3, A4]) => ModalClass, arg1: A1, arg2: A2, arg3: A3, arg4: A4) : ModalController<ModalClass>;
|
||||
export function spawnReactModal<ModalClass extends Modal>(modalClass: new (..._: any[]) => ModalClass, ...args: any[]) : ModalController<ModalClass> {
|
||||
return new ModalController(new modalClass(...args));
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
@import "../../../css/static/properties";
|
||||
@import "../../../css/static/mixin";
|
||||
|
||||
/* general switch look */
|
||||
.switch {
|
||||
$ball_outer_width: 1.5em; /* 1.5? */
|
||||
$ball_inner_width: .4em;
|
||||
|
||||
$slider_height: .8em;
|
||||
$slider_width: 2em;
|
||||
|
||||
$slider_border_size: .1em;
|
||||
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
outline: none;
|
||||
|
||||
width: $slider_width;
|
||||
height: $slider_height;
|
||||
|
||||
/* "allocate" space for the slider */
|
||||
margin-top: ($ball_outer_width - $slider_height) / 2;
|
||||
margin-bottom: ($ball_outer_width - $slider_height) / 2;
|
||||
margin-left: $ball_outer_width / 2;
|
||||
margin-right: $ball_outer_width / 2;
|
||||
|
||||
/* fix size */
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
|
||||
input {
|
||||
/* "hide" the actual input node */
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
pointer-events: all!important;
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
|
||||
top: -$slider_border_size;
|
||||
left: -$slider_border_size;
|
||||
right: -$slider_border_size;
|
||||
bottom: -$slider_border_size;
|
||||
|
||||
background-color: #1c1c1c;
|
||||
|
||||
border: $slider_border_size solid #262628;
|
||||
border-radius: 5px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
|
||||
height: $ball_outer_width;
|
||||
width: $ball_outer_width;
|
||||
|
||||
left: - $ball_outer_width / 2;
|
||||
bottom: -($ball_outer_width - $slider_height) / 2;
|
||||
|
||||
background-color: #3d3a3a;
|
||||
|
||||
@include transition(.4s);
|
||||
border-radius: 50%;
|
||||
|
||||
box-shadow: 0 0 .2em 1px rgba(0, 0, 0, 0.27);
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
|
||||
height: $ball_inner_width;
|
||||
width: $ball_inner_width;
|
||||
|
||||
left: -($ball_inner_width / 2);
|
||||
bottom: $slider_height / 2 - $ball_inner_width / 2;
|
||||
|
||||
background-color: #a5a5a5;
|
||||
box-shadow: 0 0 1em 1px rgba(165, 165, 165, 0.4);
|
||||
border-radius: 50%;
|
||||
|
||||
@include transition(.4s);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
input:focus + .slider {
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
&:before {
|
||||
@include transform(translateX($slider_width));
|
||||
}
|
||||
|
||||
.dot {
|
||||
@include transform(translateX($slider_width));
|
||||
background-color: #46c0ec;
|
||||
box-shadow: 0 0 1em 1px #46c0ec;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
|
||||
|
||||
.label {
|
||||
margin-left: .25em;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
.dot {
|
||||
background-color: #808080;
|
||||
box-shadow: 0 0 1em 1px rgba(102, 102, 102, 0.4);
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
.dot {
|
||||
background-color: #138db9;
|
||||
box-shadow: 0 0 1em 1px #138db9;
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
background-color: #252424;
|
||||
}
|
||||
|
||||
.slider:before {
|
||||
background-color: #2f2d2d;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react";
|
||||
const cssStyle = require("./Switch.scss");
|
||||
|
||||
export interface SwitchProperties {
|
||||
initialState: boolean;
|
||||
|
||||
className?: string;
|
||||
|
||||
label?: string | React.ReactElement;
|
||||
labelSide?: "right" | "left";
|
||||
|
||||
disabled?: boolean;
|
||||
|
||||
onChange?: (value: boolean) => void;
|
||||
onBlur?: () => void;
|
||||
}
|
||||
|
||||
export interface SwitchState {
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export class Switch extends React.Component<SwitchProperties, SwitchState> {
|
||||
private readonly ref = React.createRef<HTMLLabelElement>();
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
checked: this.props.initialState,
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : !!this.props.disabled;
|
||||
return (
|
||||
<label ref={this.ref} className={cssStyle.container + " " + (this.props.className || "") + " " + (disabled ? cssStyle.disabled : "")} onBlur={this.props.onBlur}>
|
||||
<div className={cssStyle.switch}>
|
||||
<input type="checkbox" onChange={e => {
|
||||
this.setState({ checked: e.currentTarget.checked });
|
||||
this.props.onChange && this.props.onChange(e.currentTarget.checked);
|
||||
}} disabled={disabled} checked={this.state.checked} />
|
||||
<span className={cssStyle.slider}>
|
||||
<span className={cssStyle.dot} />
|
||||
</span>
|
||||
</div>
|
||||
{this.props.label ? <a className={cssStyle.label}>{this.props.label}</a> : undefined}
|
||||
</label>
|
||||
)
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.ref.current?.focus();
|
||||
}
|
||||
}
|
|
@ -1,15 +1,11 @@
|
|||
import * as React from "react";
|
||||
|
||||
export class Translatable extends React.Component<{ message: string, children?: never } | { children: string }, { translated: string }> {
|
||||
export class Translatable extends React.Component<{ message: string, children?: never } | { children: string }, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
translated: /* @tr-ignore */ tr(props.message || props.children)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return this.state.translated || "";
|
||||
return /* @tr-ignore */ tr(typeof this.props.children === "string" ? this.props.children : (this.props as any).message);
|
||||
}
|
||||
}
|
|
@ -115,7 +115,7 @@ class ClientServerGroupIcons extends ReactComponentBase<ClientServerGroupIconsPr
|
|||
|
||||
protected initialize() {
|
||||
this.group_updated_callback = (event: GroupEvents["notify_properties_updated"]) => {
|
||||
if(typeof event.updated_properties.iconid !== "undefined" || typeof event.updated_properties.sortid !== "undefined")
|
||||
if(event.updated_properties.indexOf("sort-id") !== -1 || event.updated_properties.indexOf("icon") !== -1)
|
||||
this.forceUpdate();
|
||||
};
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ class ClientChannelGroupIcon extends ReactComponentBase<ClientChannelGroupIconPr
|
|||
|
||||
protected initialize() {
|
||||
this.group_updated_callback = (event: GroupEvents["notify_properties_updated"]) => {
|
||||
if(typeof event.updated_properties.iconid !== "undefined" || typeof event.updated_properties.sortid !== "undefined")
|
||||
if(event.updated_properties.indexOf("sort-id") !== -1 || event.updated_properties.indexOf("icon") !== -1)
|
||||
this.forceUpdate();
|
||||
};
|
||||
}
|
||||
|
@ -254,6 +254,7 @@ interface ClientNameState {
|
|||
@BatchUpdateAssignment(BatchUpdateType.CHANNEL_TREE)
|
||||
@ReactEventHandler<ClientName>(e => e.props.client.events)
|
||||
class ClientName extends ReactComponentBase<ClientNameProperties, ClientNameState> {
|
||||
/* FIXME: Update prefix/suffix if a server/channel group updates! */
|
||||
protected initialize() {
|
||||
this.state = {} as any;
|
||||
this.updateGroups(this.state);
|
||||
|
|
|
@ -9,7 +9,7 @@ import {Sound} from "tc-shared/sound/Sounds";
|
|||
import {Group} from "tc-shared/permission/GroupManager";
|
||||
import * as server_log from "tc-shared/ui/frames/server_log";
|
||||
import {ServerAddress, ServerEntry} from "tc-shared/ui/server";
|
||||
import {ChannelEntry, ChannelSubscribeMode} from "tc-shared/ui/channel";
|
||||
import {ChannelEntry, ChannelProperties, ChannelSubscribeMode} from "tc-shared/ui/channel";
|
||||
import {ClientEntry, LocalClientEntry, MusicClientEntry} from "tc-shared/ui/client";
|
||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {createChannelModal} from "tc-shared/ui/modal/ModalCreateChannel";
|
||||
|
@ -46,7 +46,13 @@ export interface ChannelTreeEvents {
|
|||
notify_query_view_state_changed: { queries_shown: boolean },
|
||||
|
||||
notify_entry_move_begin: {},
|
||||
notify_entry_move_end: {}
|
||||
notify_entry_move_end: {},
|
||||
|
||||
notify_channel_updated: {
|
||||
channel: ChannelEntry,
|
||||
channelProperties: ChannelProperties,
|
||||
updatedProperties: ChannelProperties
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelTreeEntrySelect {
|
||||
|
@ -260,6 +266,19 @@ export class ChannelTree {
|
|||
return this._tag_container;
|
||||
}
|
||||
|
||||
channelsOrdered() : ChannelEntry[] {
|
||||
const result = [];
|
||||
|
||||
const visit = (channel: ChannelEntry) => {
|
||||
result.push(channel);
|
||||
channel.child_channel_head && visit(channel.child_channel_head);
|
||||
channel.channel_next && visit(channel.channel_next);
|
||||
};
|
||||
this.channel_first && visit(this.channel_first);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
ReactDOM.unmountComponentAtNode(this._tag_container[0]);
|
||||
|
||||
|
|
Loading…
Reference in New Issue