Temp changed for connect modal
This commit is contained in:
parent
38d655eee4
commit
c3b64447db
10 changed files with 932 additions and 46 deletions
|
@ -337,7 +337,7 @@ export class ConnectionHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(user_action) {
|
if(user_action) {
|
||||||
this.currentConnectId = await connectionHistory.logConnectionAttempt(originalAddress.host, originalAddress.port);
|
this.currentConnectId = await connectionHistory.logConnectionAttempt(originalAddress.host + (originalAddress.port === 9987 ? "" : (":" + originalAddress.port)));
|
||||||
} else {
|
} else {
|
||||||
this.currentConnectId = -1;
|
this.currentConnectId = -1;
|
||||||
}
|
}
|
||||||
|
@ -921,7 +921,7 @@ export class ConnectionHandler {
|
||||||
errorMessage = tr("lookup the console");
|
errorMessage = tr("lookup the console");
|
||||||
}
|
}
|
||||||
|
|
||||||
logWarn(LogCategory.VOICE, tr("Failed to start microphone input (%s)."), error);
|
logWarn(LogCategory.VOICE, tr("Failed to start microphone input (%o)."), error);
|
||||||
if(notifyError) {
|
if(notifyError) {
|
||||||
this.lastRecordErrorPopup = Date.now();
|
this.lastRecordErrorPopup = Date.now();
|
||||||
createErrorModal(tr("Failed to start recording"), tra("Microphone start failed.\nError: {}", errorMessage)).open();
|
createErrorModal(tr("Failed to start recording"), tra("Microphone start failed.\nError: {}", errorMessage)).open();
|
||||||
|
|
|
@ -6,16 +6,15 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import {ServerProperties} from "tc-shared/tree/Server";
|
import {ServerProperties} from "tc-shared/tree/Server";
|
||||||
|
|
||||||
const kUnknownServerUniqueId = "unknown";
|
export const kUnknownHistoryServerUniqueId = "unknown";
|
||||||
|
|
||||||
export type ConnectionHistoryEntry = {
|
export type ConnectionHistoryEntry = {
|
||||||
id: number,
|
id: number,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
|
|
||||||
targetHost: string,
|
/* Target address how it has been given by the user */
|
||||||
targetPort: number,
|
targetAddress: string;
|
||||||
|
serverUniqueId: string | typeof kUnknownHistoryServerUniqueId
|
||||||
serverUniqueId: string | typeof kUnknownServerUniqueId
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ConnectionHistoryServerEntry = {
|
export type ConnectionHistoryServerEntry = {
|
||||||
|
@ -30,6 +29,8 @@ export type ConnectionHistoryServerInfo = {
|
||||||
name: string,
|
name: string,
|
||||||
iconId: number,
|
iconId: number,
|
||||||
|
|
||||||
|
country: string,
|
||||||
|
|
||||||
/* These properties are only available upon server variable retrieval */
|
/* These properties are only available upon server variable retrieval */
|
||||||
clientsOnline: number | -1,
|
clientsOnline: number | -1,
|
||||||
clientsMax: number | -1,
|
clientsMax: number | -1,
|
||||||
|
@ -49,28 +50,42 @@ export class ConnectionHistory {
|
||||||
switch (event.oldVersion) {
|
switch (event.oldVersion) {
|
||||||
case 0:
|
case 0:
|
||||||
if(!database.objectStoreNames.contains("attempt-history")) {
|
if(!database.objectStoreNames.contains("attempt-history")) {
|
||||||
|
/*
|
||||||
|
Schema:
|
||||||
|
{
|
||||||
|
timestamp: number,
|
||||||
|
targetAddress: string,
|
||||||
|
serverUniqueId: string | typeof kUnknownHistoryServerUniqueId
|
||||||
|
}
|
||||||
|
*/
|
||||||
const store = database.createObjectStore("attempt-history", { keyPath: "id", autoIncrement: true });
|
const store = database.createObjectStore("attempt-history", { keyPath: "id", autoIncrement: true });
|
||||||
store.createIndex("timestamp", "timestamp", { unique: false });
|
store.createIndex("timestamp", "timestamp", { unique: false });
|
||||||
store.createIndex("targetHost", "targetHost", { unique: false });
|
store.createIndex("targetAddress", "targetAddress", { unique: false });
|
||||||
store.createIndex("targetPort", "targetPort", { unique: false });
|
|
||||||
store.createIndex("serverUniqueId", "serverUniqueId", { unique: false });
|
store.createIndex("serverUniqueId", "serverUniqueId", { unique: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!database.objectStoreNames.contains("server-info")) {
|
if(!database.objectStoreNames.contains("server-info")) {
|
||||||
const store = database.createObjectStore("server-info", { keyPath: "uniqueId" });
|
database.createObjectStore("server-info", { keyPath: "uniqueId" });
|
||||||
store.createIndex("firstConnectTimestamp", "firstConnectTimestamp", { unique: false });
|
/*
|
||||||
store.createIndex("firstConnectId", "firstConnectId", { unique: false });
|
Schema:
|
||||||
|
{
|
||||||
|
firstConnectTimestamp: number,
|
||||||
|
firstConnectId: number,
|
||||||
|
|
||||||
store.createIndex("lastConnectTimestamp", "lastConnectTimestamp", { unique: false });
|
lastConnectTimestamp: number,
|
||||||
store.createIndex("lastConnectId", "lastConnectId", { unique: false });
|
lastConnectId: number,
|
||||||
|
|
||||||
store.createIndex("name", "name", { unique: false });
|
name: string,
|
||||||
store.createIndex("iconId", "iconId", { unique: false });
|
iconId: number,
|
||||||
|
|
||||||
store.createIndex("clientsOnline", "clientsOnline", { unique: false });
|
country: string,
|
||||||
store.createIndex("clientsMax", "clientsMax", { unique: false });
|
|
||||||
|
|
||||||
store.createIndex("passwordProtected", "passwordProtected", { unique: false });
|
clientsOnline: number | -1,
|
||||||
|
clientsMax: number | -1,
|
||||||
|
|
||||||
|
passwordProtected: boolean
|
||||||
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fall through wanted */
|
/* fall through wanted */
|
||||||
|
@ -96,11 +111,10 @@ export class ConnectionHistory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new connection attempt.
|
* Register a new connection attempt.
|
||||||
* @param targetHost
|
* @param targetAddress
|
||||||
* @param targetPort
|
|
||||||
* @return Returns a unique connect attempt identifier id which could be later used to set the unique server id.
|
* @return Returns a unique connect attempt identifier id which could be later used to set the unique server id.
|
||||||
*/
|
*/
|
||||||
async logConnectionAttempt(targetHost: string, targetPort: number) : Promise<number> {
|
async logConnectionAttempt(targetAddress: string) : Promise<number> {
|
||||||
if(!this.database) {
|
if(!this.database) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -111,9 +125,8 @@ export class ConnectionHistory {
|
||||||
const id = await new Promise<IDBValidKey>((resolve, reject) => {
|
const id = await new Promise<IDBValidKey>((resolve, reject) => {
|
||||||
const insert = store.put({
|
const insert = store.put({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
targetHost: targetHost,
|
targetAddress: targetAddress,
|
||||||
targetPort: targetPort,
|
serverUniqueId: kUnknownHistoryServerUniqueId
|
||||||
serverUniqueId: kUnknownServerUniqueId
|
|
||||||
});
|
});
|
||||||
|
|
||||||
insert.onsuccess = () => resolve(insert.result);
|
insert.onsuccess = () => resolve(insert.result);
|
||||||
|
@ -128,8 +141,8 @@ export class ConnectionHistory {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async resolveDatabaseServerInfo(serverUniqueId: string) : Promise<IDBCursorWithValue | null> {
|
private async resolveDatabaseServerInfo(serverUniqueId: string, mode: IDBTransactionMode) : Promise<IDBCursorWithValue | null> {
|
||||||
const transaction = this.database.transaction(["server-info"], "readwrite");
|
const transaction = this.database.transaction(["server-info"], mode);
|
||||||
const store = transaction.objectStore("server-info");
|
const store = transaction.objectStore("server-info");
|
||||||
|
|
||||||
return await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
|
return await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
|
||||||
|
@ -140,7 +153,7 @@ export class ConnectionHistory {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async updateDatabaseServerInfo(serverUniqueId: string, updateCallback: (databaseValue) => void) {
|
private async updateDatabaseServerInfo(serverUniqueId: string, updateCallback: (databaseValue) => void) {
|
||||||
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
|
let entry = await this.resolveDatabaseServerInfo(serverUniqueId, "readwrite");
|
||||||
|
|
||||||
if(entry) {
|
if(entry) {
|
||||||
const newValue = Object.assign({}, entry.value);
|
const newValue = Object.assign({}, entry.value);
|
||||||
|
@ -209,7 +222,7 @@ export class ConnectionHistory {
|
||||||
if(entry.value.serverUniqueId === serverUniqueId) {
|
if(entry.value.serverUniqueId === serverUniqueId) {
|
||||||
logWarn(LogCategory.GENERAL, tr("updateConnectionServerUniqueId(...) has been called twice"));
|
logWarn(LogCategory.GENERAL, tr("updateConnectionServerUniqueId(...) has been called twice"));
|
||||||
return;
|
return;
|
||||||
} else if(entry.value.serverUniqueId !== kUnknownServerUniqueId) {
|
} else if(entry.value.serverUniqueId !== kUnknownHistoryServerUniqueId) {
|
||||||
throw tr("connection attempt has already a server unique id set");
|
throw tr("connection attempt has already a server unique id set");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,7 +276,7 @@ export class ConnectionHistory {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
|
let entry = await this.resolveDatabaseServerInfo(serverUniqueId, "readonly");
|
||||||
if(!entry) {
|
if(!entry) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -279,6 +292,8 @@ export class ConnectionHistory {
|
||||||
name: value.name,
|
name: value.name,
|
||||||
iconId: value.iconId,
|
iconId: value.iconId,
|
||||||
|
|
||||||
|
country: value.country,
|
||||||
|
|
||||||
clientsOnline: value.clientsOnline,
|
clientsOnline: value.clientsOnline,
|
||||||
clientsMax: value.clientsMax,
|
clientsMax: value.clientsMax,
|
||||||
|
|
||||||
|
@ -297,7 +312,7 @@ export class ConnectionHistory {
|
||||||
|
|
||||||
const result: ConnectionHistoryEntry[] = [];
|
const result: ConnectionHistoryEntry[] = [];
|
||||||
|
|
||||||
const transaction = this.database.transaction(["attempt-history"], "readwrite");
|
const transaction = this.database.transaction(["attempt-history"], "readonly");
|
||||||
const store = transaction.objectStore("attempt-history");
|
const store = transaction.objectStore("attempt-history");
|
||||||
|
|
||||||
const cursor = store.index("timestamp").openCursor(undefined, "prev");
|
const cursor = store.index("timestamp").openCursor(undefined, "prev");
|
||||||
|
@ -315,21 +330,17 @@ export class ConnectionHistory {
|
||||||
id: entry.value.id,
|
id: entry.value.id,
|
||||||
timestamp: entry.value.timestamp,
|
timestamp: entry.value.timestamp,
|
||||||
|
|
||||||
targetHost: entry.value.targetHost,
|
targetAddress: entry.value.targetAddress,
|
||||||
targetPort: entry.value.targetPort,
|
|
||||||
|
|
||||||
serverUniqueId: entry.value.serverUniqueId,
|
serverUniqueId: entry.value.serverUniqueId,
|
||||||
} as ConnectionHistoryEntry;
|
} as ConnectionHistoryEntry;
|
||||||
entry.continue();
|
entry.continue();
|
||||||
|
|
||||||
if(parsedEntry.serverUniqueId !== kUnknownServerUniqueId) {
|
if(parsedEntry.serverUniqueId !== kUnknownHistoryServerUniqueId) {
|
||||||
if(result.findIndex(entry => entry.serverUniqueId === parsedEntry.serverUniqueId) !== -1) {
|
if(result.findIndex(entry => entry.serverUniqueId === parsedEntry.serverUniqueId) !== -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if(result.findIndex(entry => {
|
if(result.findIndex(entry => entry.targetAddress === parsedEntry.targetAddress) !== -1) {
|
||||||
return entry.targetHost === parsedEntry.targetHost && entry.targetPort === parsedEntry.targetPort;
|
|
||||||
}) !== -1) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -339,6 +350,22 @@ export class ConnectionHistory {
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async countConnectCount(target: string, targetType: "address" | "server-unique-id") : Promise<number> {
|
||||||
|
if(!this.database) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const transaction = this.database.transaction(["attempt-history"], "readonly");
|
||||||
|
const store = transaction.objectStore("attempt-history");
|
||||||
|
|
||||||
|
|
||||||
|
const count = store.index(targetType === "server-unique-id" ? "serverUniqueId" : "targetAddress").count(target);
|
||||||
|
return await new Promise<number>((resolve, reject) => {
|
||||||
|
count.onsuccess = () => resolve(count.result);
|
||||||
|
count.onerror = () => reject(count.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
|
const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
|
||||||
|
@ -347,7 +374,8 @@ const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
|
||||||
"virtualserver_flag_password",
|
"virtualserver_flag_password",
|
||||||
"virtualserver_maxclients",
|
"virtualserver_maxclients",
|
||||||
"virtualserver_clientsonline",
|
"virtualserver_clientsonline",
|
||||||
"virtualserver_flag_password"
|
"virtualserver_flag_password",
|
||||||
|
"virtualserver_country_code"
|
||||||
];
|
];
|
||||||
|
|
||||||
class ConnectionHistoryUpdateListener {
|
class ConnectionHistoryUpdateListener {
|
||||||
|
@ -394,6 +422,8 @@ class ConnectionHistoryUpdateListener {
|
||||||
name: event.server_properties.virtualserver_name,
|
name: event.server_properties.virtualserver_name,
|
||||||
iconId: event.server_properties.virtualserver_icon_id,
|
iconId: event.server_properties.virtualserver_icon_id,
|
||||||
|
|
||||||
|
country: event.server_properties.virtualserver_country_code,
|
||||||
|
|
||||||
clientsMax: event.server_properties.virtualserver_maxclients,
|
clientsMax: event.server_properties.virtualserver_maxclients,
|
||||||
clientsOnline: event.server_properties.virtualserver_clientsonline,
|
clientsOnline: event.server_properties.virtualserver_clientsonline,
|
||||||
|
|
||||||
|
|
|
@ -52,6 +52,8 @@ import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/Conn
|
||||||
import {server_connections} from "tc-shared/ConnectionManager";
|
import {server_connections} from "tc-shared/ConnectionManager";
|
||||||
import ContextMenuEvent = JQuery.ContextMenuEvent;
|
import ContextMenuEvent = JQuery.ContextMenuEvent;
|
||||||
|
|
||||||
|
import "./ui/modal/connect/Controller";
|
||||||
|
|
||||||
let preventWelcomeUI = false;
|
let preventWelcomeUI = false;
|
||||||
async function initialize() {
|
async function initialize() {
|
||||||
try {
|
try {
|
||||||
|
|
41
shared/js/ui/modal/connect/Controller.ts
Normal file
41
shared/js/ui/modal/connect/Controller.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import {ConnectProperties, ConnectUiEvents} from "tc-shared/ui/modal/connect/Definitions";
|
||||||
|
import {spawnReactModal} from "tc-shared/ui/react-elements/Modal";
|
||||||
|
import {ConnectModal} from "tc-shared/ui/modal/connect/Renderer";
|
||||||
|
|
||||||
|
class ConnectController {
|
||||||
|
readonly uiEvents: Registry<ConnectUiEvents>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.uiEvents = new Registry<ConnectUiEvents>();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private sendProperty(property: keyof ConnectProperties) {
|
||||||
|
switch (property) {
|
||||||
|
case "address":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectModalOptions = {
|
||||||
|
connectInANewTab?: boolean,
|
||||||
|
defaultAddress?: string,
|
||||||
|
defaultProfile?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spawnConnectModalNew(options: ConnectModalOptions) {
|
||||||
|
const controller = new ConnectController();
|
||||||
|
const modal = spawnReactModal(ConnectModal, controller.uiEvents, options.connectInANewTab || false);
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
modal.events.one("destroy", () => {
|
||||||
|
controller.destroy();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).spawnConnectModalNew = spawnConnectModalNew;
|
94
shared/js/ui/modal/connect/Definitions.ts
Normal file
94
shared/js/ui/modal/connect/Definitions.ts
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import {kUnknownHistoryServerUniqueId} from "tc-shared/connectionlog/History";
|
||||||
|
|
||||||
|
export type ConnectProfileEntry = {
|
||||||
|
id: string,
|
||||||
|
name: string,
|
||||||
|
valid: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectHistoryEntry = {
|
||||||
|
id: number,
|
||||||
|
targetAddress: string,
|
||||||
|
uniqueServerId: string | typeof kUnknownHistoryServerUniqueId,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectHistoryServerInfo = {
|
||||||
|
iconId: number,
|
||||||
|
name: string,
|
||||||
|
password: boolean,
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectServerAddress = {
|
||||||
|
currentAddress: string,
|
||||||
|
defaultAddress: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectServerNickname = {
|
||||||
|
currentNickname: string,
|
||||||
|
defaultNickname: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ConnectProfiles = {
|
||||||
|
profiles: ConnectProfileEntry[],
|
||||||
|
selected: string
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ConnectProperties {
|
||||||
|
address: ConnectServerAddress,
|
||||||
|
nickname: ConnectServerNickname,
|
||||||
|
password: string,
|
||||||
|
profiles: ConnectProfiles,
|
||||||
|
history: {
|
||||||
|
history: ConnectHistoryEntry[],
|
||||||
|
selected: number | -1,
|
||||||
|
state: "shown" | "hidden"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PropertyValidState {
|
||||||
|
address: boolean,
|
||||||
|
nickname: boolean,
|
||||||
|
password: boolean,
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnectProperty<T extends keyof ConnectProperties> = {
|
||||||
|
property: T,
|
||||||
|
value: ConnectProperties[T]
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ConnectUiEvents {
|
||||||
|
action_manage_profiles: {},
|
||||||
|
action_select_profile: { id: string },
|
||||||
|
action_select_history: { id: number },
|
||||||
|
action_connect: { newTab: boolean },
|
||||||
|
action_toggle_history: { enabled: boolean }
|
||||||
|
action_delete_history: {
|
||||||
|
target: string,
|
||||||
|
targetType: "address" | "server-unique-id"
|
||||||
|
}
|
||||||
|
|
||||||
|
query_property: {
|
||||||
|
property: keyof ConnectProperties
|
||||||
|
},
|
||||||
|
|
||||||
|
notify_property: ConnectProperty<keyof ConnectProperties>
|
||||||
|
|
||||||
|
query_history_entry: {
|
||||||
|
serverUniqueId: string
|
||||||
|
},
|
||||||
|
query_history_connections: {
|
||||||
|
target: string,
|
||||||
|
targetType: "address" | "server-unique-id"
|
||||||
|
}
|
||||||
|
|
||||||
|
notify_history_entry: {
|
||||||
|
serverUniqueId: string,
|
||||||
|
info: ConnectHistoryServerInfo
|
||||||
|
},
|
||||||
|
notify_history_connections: {
|
||||||
|
target: string,
|
||||||
|
targetType: "address" | "server-unique-id",
|
||||||
|
value: number
|
||||||
|
}
|
||||||
|
}
|
418
shared/js/ui/modal/connect/Renderer.scss
Normal file
418
shared/js/ui/modal/connect/Renderer.scss
Normal file
|
@ -0,0 +1,418 @@
|
||||||
|
@import "../../../../css/static/mixin";
|
||||||
|
@import "../../../../css/static/properties";
|
||||||
|
|
||||||
|
.container {
|
||||||
|
@include user-select(none);
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 1em;
|
||||||
|
|
||||||
|
width: 50em;
|
||||||
|
min-width: 25em;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex!important;
|
||||||
|
flex-direction: column!important;
|
||||||
|
justify-content: stretch!important;
|
||||||
|
|
||||||
|
.container-last-servers {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-left: 2px solid #7a7a7a;
|
||||||
|
|
||||||
|
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out);
|
||||||
|
&.shown {
|
||||||
|
/* apply the default padding */
|
||||||
|
padding: 0 24px 24px;
|
||||||
|
|
||||||
|
max-height: 100%;
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
@include transition(max-height .5s ease-in-out, opacity .5s ease-in-out, padding .5s ease-in-out)
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
width: calc(100% + 46px);
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
margin: 0 0 0 -23px;
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #090909;
|
||||||
|
|
||||||
|
margin-bottom: .75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
color: #7a7a7a;
|
||||||
|
|
||||||
|
/* general table class */
|
||||||
|
.table {
|
||||||
|
width: 100em;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #161618;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.body {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #202022;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: #131315;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-empty {
|
||||||
|
height: 3em;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-around;
|
||||||
|
font-size: 1.25em;
|
||||||
|
color: rgba(121, 121, 121, 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
padding-right: .25em;
|
||||||
|
padding-left: .25em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
&:not(:last-of-type) {
|
||||||
|
border-right: 1px solid #161618;
|
||||||
|
}
|
||||||
|
|
||||||
|
> a {
|
||||||
|
max-width: 100%;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* connect table */
|
||||||
|
.table {
|
||||||
|
margin-left: -1.5em; /* the delete row */
|
||||||
|
|
||||||
|
.head {
|
||||||
|
margin-left: 1.5em; /* the delete row */
|
||||||
|
.column.delete {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
align-self: center;
|
||||||
|
.country, .icon-container {
|
||||||
|
align-self: center;
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mixin fixed-column($name, $width) {
|
||||||
|
&.#{$name} {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
width: $width;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include fixed-column(delete, 1.5em);
|
||||||
|
@include fixed-column(password, 5em);
|
||||||
|
@include fixed-column(country-name, 7em);
|
||||||
|
@include fixed-column(clients, 4em);
|
||||||
|
@include fixed-column(connections, 6.5em);
|
||||||
|
|
||||||
|
&.delete {
|
||||||
|
opacity: 0;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: none;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
@include transition(opacity .25 ease-in-out);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
@include transition(opacity .25 ease-in-out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.address {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.name {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
width: 60%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.connectContainer_ {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
/* apply the default padding */
|
||||||
|
padding: .75em 24px;
|
||||||
|
|
||||||
|
border-left: 2px solid #0073d4;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
> .row {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
> *:not(:last-of-type) {
|
||||||
|
margin-right: 3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-address-password {
|
||||||
|
.container-address {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-password {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 4;
|
||||||
|
|
||||||
|
min-width: 21.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-profile-manage {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 4;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.container-select-profile {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
min-width: 14em;
|
||||||
|
|
||||||
|
> .invalid-feedback {
|
||||||
|
width: max-content; /* allow overflow here */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-manage {
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 4;
|
||||||
|
|
||||||
|
margin-left: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-manage-profiles {
|
||||||
|
min-width: 7em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-nickname {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-buttons {
|
||||||
|
padding-top: 1em;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.container-buttons-connect {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
min-width: 6em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-right {
|
||||||
|
min-width: 7em;
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-left {
|
||||||
|
min-width: 14em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
border-color: #7a7a7a;
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.inputAddress, .inputNickname {
|
||||||
|
width: 75%;
|
||||||
|
min-width: 10em;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
|
||||||
|
margin-right: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputPassword, .inputProfile {
|
||||||
|
width: 25%;
|
||||||
|
|
||||||
|
min-width: 15em;
|
||||||
|
max-width: 21em;
|
||||||
|
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputProfile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
.input {
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
flex-shrink: 1;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
height: 2em;
|
||||||
|
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
align-self: flex-end;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
margin-left: .5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media all and (max-width: 55rem) {
|
||||||
|
.container {
|
||||||
|
padding: .5em!important;
|
||||||
|
|
||||||
|
.container-address-password {
|
||||||
|
.container-password {
|
||||||
|
min-width: unset!important;
|
||||||
|
margin-left: 1em!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-buttons {
|
||||||
|
justify-content: flex-end!important;
|
||||||
|
|
||||||
|
.button-toggle-last-servers {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-profile-name {
|
||||||
|
flex-direction: column!important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container-last-servers {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.connectContainer {
|
||||||
|
.inputAddress, .inputNickname {
|
||||||
|
margin-right: 1em!important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
232
shared/js/ui/modal/connect/Renderer.tsx
Normal file
232
shared/js/ui/modal/connect/Renderer.tsx
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
import {ConnectProperties, ConnectUiEvents} from "tc-shared/ui/modal/connect/Definitions";
|
||||||
|
import {useContext, useState} from "react";
|
||||||
|
import {Registry} from "tc-shared/events";
|
||||||
|
import * as React from "react";
|
||||||
|
import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller";
|
||||||
|
import {Translatable} from "tc-shared/ui/react-elements/i18n";
|
||||||
|
import {ControlledSelect, FlatInputField, Select} from "tc-shared/ui/react-elements/InputField";
|
||||||
|
import {useTr} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
import {Button} from "tc-shared/ui/react-elements/Button";
|
||||||
|
|
||||||
|
const EventContext = React.createContext<Registry<ConnectUiEvents>>(undefined);
|
||||||
|
const ConnectDefaultNewTabContext = React.createContext<boolean>(false);
|
||||||
|
|
||||||
|
const cssStyle = require("./Renderer.scss");
|
||||||
|
|
||||||
|
/*
|
||||||
|
<div class="container-connect-input">
|
||||||
|
<div class="row container-address-password">
|
||||||
|
<div class="form-group container-address">
|
||||||
|
<label>{{tr "Server Address" /}}</label>
|
||||||
|
<input type="text" class="form-control" aria-describedby="input-connect-address-help"
|
||||||
|
placeholder="ts.teaspeak.de" autocomplete="off">
|
||||||
|
<div class="invalid-feedback">{{tr "Please enter a valid server address" /}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group container-password">
|
||||||
|
<label class="bmd-label-floating">{{tr "Server password" /}}</label>
|
||||||
|
<form autocomplete="off" onsubmit="return false;">
|
||||||
|
<input id="input-connect-password-{{>password_id}}" type="password" class="form-control"
|
||||||
|
autocomplete="off">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row container-profile-name">
|
||||||
|
<div class="form-group container-nickname">
|
||||||
|
<label>{{tr "Nickname" /}}</label>
|
||||||
|
<input type="text" class="form-control" aria-describedby="input-connect-nickname-help"
|
||||||
|
placeholder="Another TeaSpeak user">
|
||||||
|
<div class="invalid-feedback">{{tr "Please enter a valid server nickname" /}}</div>
|
||||||
|
<!-- <small id="input-connect-nickname-help" class="form-text text-muted">We'll never share your email with anyone else.</small> -->
|
||||||
|
</div>
|
||||||
|
<div class="container-profile-manage">
|
||||||
|
<div class="form-group container-select-profile">
|
||||||
|
<label for="select-connect-profile">{{tr "Connect Profile" /}}</label>
|
||||||
|
<select class="form-control" id="select-connect-profile"> </select>
|
||||||
|
<div class="invalid-feedback">{{tr "Selected profile is invalid. Select another one or fix the profile." /}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button type="button" class="btn btn-raised button-manage-profiles">{{tr "Profiles" /}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container-buttons">
|
||||||
|
<button type="button" class="btn btn-raised button-toggle-last-servers"><a>{{tr "Show last servers"
|
||||||
|
/}}</a>
|
||||||
|
<div class="arrow down"></div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="container-buttons-connect">
|
||||||
|
{{if default_connect_new_tab}}
|
||||||
|
<button type="button" class="btn btn-raised btn-success button-connect button-left">{{tr
|
||||||
|
"Connect in same tab" /}}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-raised btn-success button-connect-new-tab button-right">
|
||||||
|
{{tr "Connect" /}}
|
||||||
|
</button>
|
||||||
|
{{else}}
|
||||||
|
{{if multi_tab}}
|
||||||
|
<button type="button" class="btn btn-raised btn-success button-connect-new-tab button-left">{{tr
|
||||||
|
"Connect in a new tab" /}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
<button type="button" class="btn btn-raised btn-success button-connect button-right">{{tr
|
||||||
|
"Connect" /}}
|
||||||
|
</button>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container-last-servers">
|
||||||
|
<hr>
|
||||||
|
<div class="table">
|
||||||
|
<div class="head">
|
||||||
|
<div class="column delete">Nr</div>
|
||||||
|
<div class="column name">{{tr "Name" /}}</div>
|
||||||
|
<div class="column address">{{tr "Address" /}}</div>
|
||||||
|
<div class="column password">{{tr "Password" /}}</div>
|
||||||
|
<div class="column country-name">{{tr "Country" /}}</div>
|
||||||
|
<div class="column clients">{{tr "Clients" /}}</div>
|
||||||
|
<div class="column connections">{{tr "Connections" /}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="body">
|
||||||
|
<div class="body-empty">
|
||||||
|
<a>{{tr "No connections yet made" /}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
*/
|
||||||
|
|
||||||
|
function useProperty<T extends keyof ConnectProperties, V>(key: T, defaultValue: V) : ConnectProperties[T] | V {
|
||||||
|
const events = useContext(EventContext);
|
||||||
|
const [ value, setValue ] = useState<ConnectProperties[T] | V>(() => {
|
||||||
|
events.fire("query_property", { property: key });
|
||||||
|
return defaultValue;
|
||||||
|
});
|
||||||
|
events.reactUse("notify_property", event => event.property === key && setValue(event.value as any));
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputServerAddress = () => {
|
||||||
|
return (
|
||||||
|
<FlatInputField
|
||||||
|
className={cssStyle.inputAddress}
|
||||||
|
value={"ts.teaspeak.de"}
|
||||||
|
placeholder={"ts.teaspeak.de"}
|
||||||
|
label={<Translatable>Server address</Translatable>}
|
||||||
|
labelType={"static"}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputServerPassword = () => {
|
||||||
|
return (
|
||||||
|
<FlatInputField
|
||||||
|
className={cssStyle.inputPassword}
|
||||||
|
value={"ts.teaspeak.de"}
|
||||||
|
placeholder={"ts.teaspeak.de"}
|
||||||
|
type={"password"}
|
||||||
|
label={<Translatable>Server password</Translatable>}
|
||||||
|
labelType={"floating"}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputNickname = () => {
|
||||||
|
const nickname = useProperty("nickname", undefined);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatInputField
|
||||||
|
className={cssStyle.inputNickname}
|
||||||
|
value={nickname?.currentNickname || ""}
|
||||||
|
placeholder={nickname ? nickname.defaultNickname : tr("loading...")}
|
||||||
|
label={<Translatable>Nickname</Translatable>}
|
||||||
|
labelType={"static"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputProfile = () => {
|
||||||
|
const profiles = useProperty("profiles", undefined);
|
||||||
|
const selectedProfile = profiles?.profiles.find(profile => profile.id === profiles?.selected);
|
||||||
|
|
||||||
|
let invalidMarker;
|
||||||
|
if(profiles) {
|
||||||
|
if(!selectedProfile) {
|
||||||
|
invalidMarker = <Translatable key={"no-profile"}>Select a profile</Translatable>;
|
||||||
|
} else if(!selectedProfile.valid) {
|
||||||
|
invalidMarker = <Translatable key={"invalid"}>Selected profile is invalid</Translatable>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cssStyle.inputProfile}>
|
||||||
|
<ControlledSelect
|
||||||
|
className={cssStyle.input}
|
||||||
|
value={profiles?.selected || "loading"}
|
||||||
|
type={"flat"}
|
||||||
|
label={<Translatable>Connect profile</Translatable>}
|
||||||
|
invalid={invalidMarker}
|
||||||
|
>
|
||||||
|
<option key={"loading"} value={"invalid"} style={{ display: "none" }}>{useTr("unknown profile")}</option>
|
||||||
|
<option key={"loading"} value={"loading"} style={{ display: "none" }}>{useTr("loading") + "..."}</option>
|
||||||
|
{profiles?.profiles.forEach(profile => {
|
||||||
|
return (
|
||||||
|
<option key={"profile-" + profile.id}>{profile.name}</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ControlledSelect>
|
||||||
|
<Button className={cssStyle.button} type={"small"} color={"none"}>
|
||||||
|
<Translatable>Profiles</Translatable>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConnectContainer = () => (
|
||||||
|
<div className={cssStyle.connectContainer}>
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<InputServerAddress />
|
||||||
|
<InputServerPassword />
|
||||||
|
</div>
|
||||||
|
<div className={cssStyle.row}>
|
||||||
|
<InputNickname />
|
||||||
|
<InputProfile />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export class ConnectModal extends InternalModal {
|
||||||
|
private readonly events: Registry<ConnectUiEvents>;
|
||||||
|
private readonly connectNewTabByDefault: boolean;
|
||||||
|
|
||||||
|
constructor(events: Registry<ConnectUiEvents>, connectNewTabByDefault: boolean) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
this.events = events;
|
||||||
|
this.connectNewTabByDefault = connectNewTabByDefault;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderBody(): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<EventContext.Provider value={this.events}>
|
||||||
|
<ConnectDefaultNewTabContext.Provider value={this.connectNewTabByDefault}>
|
||||||
|
<div className={cssStyle.container}>
|
||||||
|
<ConnectContainer />
|
||||||
|
</div>
|
||||||
|
</ConnectDefaultNewTabContext.Provider>
|
||||||
|
</EventContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
title(): string | React.ReactElement {
|
||||||
|
return <Translatable>Connect to a server</Translatable>;
|
||||||
|
}
|
||||||
|
|
||||||
|
color(): "none" | "blue" {
|
||||||
|
return "blue";
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ReactElement} from "react";
|
import {ReactElement} from "react";
|
||||||
|
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
|
||||||
|
|
||||||
const cssStyle = require("./InputField.scss");
|
const cssStyle = require("./InputField.scss");
|
||||||
|
|
||||||
|
@ -128,6 +129,8 @@ export interface FlatInputFieldProperties {
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
labelFloatingClassName?: string;
|
labelFloatingClassName?: string;
|
||||||
|
|
||||||
|
type?: "text" | "password" | "number";
|
||||||
|
|
||||||
help?: string | React.ReactElement;
|
help?: string | React.ReactElement;
|
||||||
helpClassName?: string;
|
helpClassName?: string;
|
||||||
|
|
||||||
|
@ -173,19 +176,20 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
|
||||||
const disabled = typeof this.state.disabled === "boolean" ? this.state.disabled : typeof this.props.disabled === "boolean" ? this.props.disabled : false;
|
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 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;
|
const placeholder = typeof this.state.placeholder === "string" ? this.state.placeholder : typeof this.props.placeholder === "string" ? this.props.placeholder : undefined;
|
||||||
|
const filled = this.state.filled || this.props.value?.length > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (this.state.filled ? cssStyle.isFilled : "") + " " + (this.props.className || "")}>
|
<div className={cssStyle.containerFlat + " " + (this.state.isInvalid ? cssStyle.isInvalid : "") + " " + (filled ? cssStyle.isFilled : "") + " " + (this.props.className || "")}>
|
||||||
{this.props.label ?
|
{this.props.label ?
|
||||||
<label className={
|
<label className={
|
||||||
cssStyle["type-" + (this.props.labelType || "static")] + " " +
|
cssStyle["type-" + (this.props.labelType || "static")] + " " +
|
||||||
(this.props.labelClassName || "") + " " +
|
(this.props.labelClassName || "") + " " +
|
||||||
(this.props.labelFloatingClassName && this.state.filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
|
(this.props.labelFloatingClassName && filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
|
||||||
<input
|
<input
|
||||||
defaultValue={this.props.defaultValue}
|
defaultValue={this.props.defaultValue}
|
||||||
value={this.props.value}
|
value={this.props.value}
|
||||||
|
|
||||||
type={"text"}
|
type={this.props.type || "text"}
|
||||||
ref={this.refInput}
|
ref={this.refInput}
|
||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
@ -228,6 +232,61 @@ export class FlatInputField extends React.Component<FlatInputFieldProperties, Fl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ControlledSelect = (props: {
|
||||||
|
type?: "flat" | "boxed",
|
||||||
|
className?: string,
|
||||||
|
|
||||||
|
value: string,
|
||||||
|
placeHolder?: string,
|
||||||
|
|
||||||
|
label?: React.ReactNode,
|
||||||
|
labelClassName?: string,
|
||||||
|
|
||||||
|
help?: React.ReactNode,
|
||||||
|
helpClassName?: string,
|
||||||
|
|
||||||
|
invalid?: React.ReactNode,
|
||||||
|
invalidClassName?: string,
|
||||||
|
|
||||||
|
disabled?: boolean,
|
||||||
|
|
||||||
|
onFocus?: () => void,
|
||||||
|
onBlur?: () => void,
|
||||||
|
|
||||||
|
onChange?: (event?: React.ChangeEvent<HTMLSelectElement>) => void,
|
||||||
|
|
||||||
|
children: React.ReactElement<HTMLOptionElement | HTMLOptGroupElement> | React.ReactElement<HTMLOptionElement | HTMLOptGroupElement>[]
|
||||||
|
}) => {
|
||||||
|
const disabled = typeof props.disabled === "boolean" ? props.disabled : false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={joinClassList(
|
||||||
|
props.type === "boxed" ? cssStyle.containerBoxed : cssStyle.containerFlat,
|
||||||
|
cssStyle["size-normal"],
|
||||||
|
props.invalid ? cssStyle.isInvalid : undefined,
|
||||||
|
props.className,
|
||||||
|
cssStyle.noLeftIcon, cssStyle.noRightIcon
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{!props.label ? undefined :
|
||||||
|
<label className={joinClassList(cssStyle["type-static"], props.labelClassName)} key={"label"}>{props.label}</label>
|
||||||
|
}
|
||||||
|
<select
|
||||||
|
value={props.value}
|
||||||
|
disabled={disabled}
|
||||||
|
|
||||||
|
onFocus={props.onFocus}
|
||||||
|
onBlur={props.onBlur}
|
||||||
|
onChange={props.onChange}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</select>
|
||||||
|
{props.invalid ? <small className={joinClassList(cssStyle.invalidFeedback, props.invalidClassName)} key={"invalid"}>{props.invalid}</small> : undefined}
|
||||||
|
{props.help ? <small className={joinClassList(cssStyle.invalidFeedback, props.help)} key={"help"}>{props.help}</small> : undefined}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface SelectProperties {
|
export interface SelectProperties {
|
||||||
type?: "flat" | "boxed";
|
type?: "flat" | "boxed";
|
||||||
|
|
|
@ -42,11 +42,14 @@ class BodyRenderer {
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance(instance: AbstractModal) {
|
setInstance(instance: AbstractModal) {
|
||||||
if(this.modalInstance)
|
if(this.modalInstance) {
|
||||||
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
ReactDOM.unmountComponentAtNode(this.htmlContainer);
|
||||||
|
}
|
||||||
|
|
||||||
this.modalInstance = instance;
|
this.modalInstance = instance;
|
||||||
if(this.modalInstance)
|
if(this.modalInstance) {
|
||||||
ReactDOM.render(<>{this.modalInstance.renderBody()}</>, this.htmlContainer);
|
ReactDOM.render(<>{this.modalInstance.renderBody()}</>, this.htmlContainer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
|
||||||
import {ClientIcon} from "svg-sprites/client-icons";
|
import {ClientIcon} from "svg-sprites/client-icons";
|
||||||
|
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
|
||||||
|
|
||||||
const cssStyle = require("./Modal.scss");
|
const cssStyle = require("./Modal.scss");
|
||||||
|
|
||||||
|
@ -23,7 +24,11 @@ export const InternalModalContentRenderer = (props: {
|
||||||
<div className={cssStyle.icon}>
|
<div className={cssStyle.icon}>
|
||||||
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
|
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.title + " " + props.headerTitleClass}>{props.modal.title()}</div>
|
<div className={cssStyle.title + " " + props.headerTitleClass}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
{props.modal.title()}
|
||||||
|
</ErrorBoundary>
|
||||||
|
</div>
|
||||||
{!props.onMinimize ? undefined : (
|
{!props.onMinimize ? undefined : (
|
||||||
<div className={cssStyle.button} onClick={props.onMinimize}>
|
<div className={cssStyle.button} onClick={props.onMinimize}>
|
||||||
<div className={"icon_em " + ClientIcon.MinimizeButton} />
|
<div className={"icon_em " + ClientIcon.MinimizeButton} />
|
||||||
|
@ -36,7 +41,9 @@ export const InternalModalContentRenderer = (props: {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={cssStyle.body + " " + props.bodyClass}>
|
<div className={cssStyle.body + " " + props.bodyClass}>
|
||||||
{props.modal.renderBody()}
|
<ErrorBoundary>
|
||||||
|
{props.modal.renderBody()}
|
||||||
|
</ErrorBoundary>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Add table
Reference in a new issue