Temp changed for connect modal

master
WolverinDEV 2021-01-09 14:25:11 +01:00
parent 38d655eee4
commit c3b64447db
10 changed files with 932 additions and 46 deletions

View File

@ -337,7 +337,7 @@ export class ConnectionHandler {
}
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 {
this.currentConnectId = -1;
}
@ -921,7 +921,7 @@ export class ConnectionHandler {
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) {
this.lastRecordErrorPopup = Date.now();
createErrorModal(tr("Failed to start recording"), tra("Microphone start failed.\nError: {}", errorMessage)).open();

View File

@ -6,16 +6,15 @@ import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {server_connections} from "tc-shared/ConnectionManager";
import {ServerProperties} from "tc-shared/tree/Server";
const kUnknownServerUniqueId = "unknown";
export const kUnknownHistoryServerUniqueId = "unknown";
export type ConnectionHistoryEntry = {
id: number,
timestamp: number,
targetHost: string,
targetPort: number,
serverUniqueId: string | typeof kUnknownServerUniqueId
/* Target address how it has been given by the user */
targetAddress: string;
serverUniqueId: string | typeof kUnknownHistoryServerUniqueId
};
export type ConnectionHistoryServerEntry = {
@ -30,6 +29,8 @@ export type ConnectionHistoryServerInfo = {
name: string,
iconId: number,
country: string,
/* These properties are only available upon server variable retrieval */
clientsOnline: number | -1,
clientsMax: number | -1,
@ -49,28 +50,42 @@ export class ConnectionHistory {
switch (event.oldVersion) {
case 0:
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 });
store.createIndex("timestamp", "timestamp", { unique: false });
store.createIndex("targetHost", "targetHost", { unique: false });
store.createIndex("targetPort", "targetPort", { unique: false });
store.createIndex("targetAddress", "targetAddress", { unique: false });
store.createIndex("serverUniqueId", "serverUniqueId", { unique: false });
}
if(!database.objectStoreNames.contains("server-info")) {
const store = database.createObjectStore("server-info", { keyPath: "uniqueId" });
store.createIndex("firstConnectTimestamp", "firstConnectTimestamp", { unique: false });
store.createIndex("firstConnectId", "firstConnectId", { unique: false });
database.createObjectStore("server-info", { keyPath: "uniqueId" });
/*
Schema:
{
firstConnectTimestamp: number,
firstConnectId: number,
store.createIndex("lastConnectTimestamp", "lastConnectTimestamp", { unique: false });
store.createIndex("lastConnectId", "lastConnectId", { unique: false });
lastConnectTimestamp: number,
lastConnectId: number,
store.createIndex("name", "name", { unique: false });
store.createIndex("iconId", "iconId", { unique: false });
name: string,
iconId: number,
store.createIndex("clientsOnline", "clientsOnline", { unique: false });
store.createIndex("clientsMax", "clientsMax", { unique: false });
country: string,
store.createIndex("passwordProtected", "passwordProtected", { unique: false });
clientsOnline: number | -1,
clientsMax: number | -1,
passwordProtected: boolean
}
*/
}
/* fall through wanted */
@ -96,11 +111,10 @@ export class ConnectionHistory {
/**
* Register a new connection attempt.
* @param targetHost
* @param targetPort
* @param targetAddress
* @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) {
return;
}
@ -111,9 +125,8 @@ export class ConnectionHistory {
const id = await new Promise<IDBValidKey>((resolve, reject) => {
const insert = store.put({
timestamp: Date.now(),
targetHost: targetHost,
targetPort: targetPort,
serverUniqueId: kUnknownServerUniqueId
targetAddress: targetAddress,
serverUniqueId: kUnknownHistoryServerUniqueId
});
insert.onsuccess = () => resolve(insert.result);
@ -128,8 +141,8 @@ export class ConnectionHistory {
return id;
}
private async resolveDatabaseServerInfo(serverUniqueId: string) : Promise<IDBCursorWithValue | null> {
const transaction = this.database.transaction(["server-info"], "readwrite");
private async resolveDatabaseServerInfo(serverUniqueId: string, mode: IDBTransactionMode) : Promise<IDBCursorWithValue | null> {
const transaction = this.database.transaction(["server-info"], mode);
const store = transaction.objectStore("server-info");
return await new Promise<IDBCursorWithValue | null>((resolve, reject) => {
@ -140,7 +153,7 @@ export class ConnectionHistory {
}
private async updateDatabaseServerInfo(serverUniqueId: string, updateCallback: (databaseValue) => void) {
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
let entry = await this.resolveDatabaseServerInfo(serverUniqueId, "readwrite");
if(entry) {
const newValue = Object.assign({}, entry.value);
@ -209,7 +222,7 @@ export class ConnectionHistory {
if(entry.value.serverUniqueId === serverUniqueId) {
logWarn(LogCategory.GENERAL, tr("updateConnectionServerUniqueId(...) has been called twice"));
return;
} else if(entry.value.serverUniqueId !== kUnknownServerUniqueId) {
} else if(entry.value.serverUniqueId !== kUnknownHistoryServerUniqueId) {
throw tr("connection attempt has already a server unique id set");
}
@ -263,7 +276,7 @@ export class ConnectionHistory {
return undefined;
}
let entry = await this.resolveDatabaseServerInfo(serverUniqueId);
let entry = await this.resolveDatabaseServerInfo(serverUniqueId, "readonly");
if(!entry) {
return;
}
@ -279,6 +292,8 @@ export class ConnectionHistory {
name: value.name,
iconId: value.iconId,
country: value.country,
clientsOnline: value.clientsOnline,
clientsMax: value.clientsMax,
@ -297,7 +312,7 @@ export class ConnectionHistory {
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 cursor = store.index("timestamp").openCursor(undefined, "prev");
@ -315,21 +330,17 @@ export class ConnectionHistory {
id: entry.value.id,
timestamp: entry.value.timestamp,
targetHost: entry.value.targetHost,
targetPort: entry.value.targetPort,
targetAddress: entry.value.targetAddress,
serverUniqueId: entry.value.serverUniqueId,
} as ConnectionHistoryEntry;
entry.continue();
if(parsedEntry.serverUniqueId !== kUnknownServerUniqueId) {
if(parsedEntry.serverUniqueId !== kUnknownHistoryServerUniqueId) {
if(result.findIndex(entry => entry.serverUniqueId === parsedEntry.serverUniqueId) !== -1) {
continue;
}
} else {
if(result.findIndex(entry => {
return entry.targetHost === parsedEntry.targetHost && entry.targetPort === parsedEntry.targetPort;
}) !== -1) {
if(result.findIndex(entry => entry.targetAddress === parsedEntry.targetAddress) !== -1) {
continue;
}
}
@ -339,6 +350,22 @@ export class ConnectionHistory {
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)[] = [
@ -347,7 +374,8 @@ const kConnectServerInfoUpdatePropertyKeys: (keyof ServerProperties)[] = [
"virtualserver_flag_password",
"virtualserver_maxclients",
"virtualserver_clientsonline",
"virtualserver_flag_password"
"virtualserver_flag_password",
"virtualserver_country_code"
];
class ConnectionHistoryUpdateListener {
@ -394,6 +422,8 @@ class ConnectionHistoryUpdateListener {
name: event.server_properties.virtualserver_name,
iconId: event.server_properties.virtualserver_icon_id,
country: event.server_properties.virtualserver_country_code,
clientsMax: event.server_properties.virtualserver_maxclients,
clientsOnline: event.server_properties.virtualserver_clientsonline,

View File

@ -52,6 +52,8 @@ import {defaultConnectProfile, findConnectProfile} from "tc-shared/profiles/Conn
import {server_connections} from "tc-shared/ConnectionManager";
import ContextMenuEvent = JQuery.ContextMenuEvent;
import "./ui/modal/connect/Controller";
let preventWelcomeUI = false;
async function initialize() {
try {

View 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;

View 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
}
}

View 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;
}
}
}

View 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";
}
}

View File

@ -1,5 +1,6 @@
import * as React from "react";
import {ReactElement} from "react";
import {joinClassList} from "tc-shared/ui/react-elements/Helper";
const cssStyle = require("./InputField.scss");
@ -128,6 +129,8 @@ export interface FlatInputFieldProperties {
labelClassName?: string;
labelFloatingClassName?: string;
type?: "text" | "password" | "number";
help?: string | React.ReactElement;
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 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 filled = this.state.filled || this.props.value?.length > 0;
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 ?
<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}
(this.props.labelFloatingClassName && filled ? this.props.labelFloatingClassName : "")}>{this.props.label}</label> : undefined}
<input
defaultValue={this.props.defaultValue}
value={this.props.value}
type={"text"}
type={this.props.type || "text"}
ref={this.refInput}
readOnly={readOnly}
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 {
type?: "flat" | "boxed";

View File

@ -42,11 +42,14 @@ class BodyRenderer {
}
setInstance(instance: AbstractModal) {
if(this.modalInstance)
if(this.modalInstance) {
ReactDOM.unmountComponentAtNode(this.htmlContainer);
}
this.modalInstance = instance;
if(this.modalInstance)
if(this.modalInstance) {
ReactDOM.render(<>{this.modalInstance.renderBody()}</>, this.htmlContainer);
}
}
}

View File

@ -1,6 +1,7 @@
import * as React from "react";
import {AbstractModal} from "tc-shared/ui/react-elements/ModalDefinitions";
import {ClientIcon} from "svg-sprites/client-icons";
import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary";
const cssStyle = require("./Modal.scss");
@ -23,7 +24,11 @@ export const InternalModalContentRenderer = (props: {
<div className={cssStyle.icon}>
<img src="img/favicon/teacup.png" alt={tr("Modal - Icon")} />
</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 : (
<div className={cssStyle.button} onClick={props.onMinimize}>
<div className={"icon_em " + ClientIcon.MinimizeButton} />
@ -36,7 +41,9 @@ export const InternalModalContentRenderer = (props: {
)}
</div>
<div className={cssStyle.body + " " + props.bodyClass}>
{props.modal.renderBody()}
<ErrorBoundary>
{props.modal.renderBody()}
</ErrorBoundary>
</div>
</div>
);