Using a React modal for the YesNo prompt and ensured that only string will be passed

master
WolverinDEV 2021-04-24 14:52:08 +02:00
parent 5ef2d85d6f
commit a414caef42
20 changed files with 316 additions and 89 deletions

View File

@ -7,7 +7,6 @@ import {LogCategory, logDebug, logError, logInfo, logWarn} from "tc-shared/log";
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {RecorderProfile, setDefaultRecorder} from "tc-shared/voice/RecorderProfile";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {openModalNewcomer} from "tc-shared/ui/modal/ModalNewcomer";
import {global_client_actions} from "tc-shared/events/GlobalEvents";
@ -53,6 +52,7 @@ import "./text/bbcode/InviteController";
import "./text/bbcode/YoutubeController";
import {initializeSounds, setSoundMasterVolume} from "./audio/Sounds";
import {getInstanceConnectHandler, setupIpcHandler} from "./ipc/BrowserIPC";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
assertMainApplication();
@ -203,11 +203,12 @@ async function doHandleConnectRequest(serverAddress: string, serverUniqueId: str
if(!getAudioBackend().isInitialized()) {
/* Trick the client into clicking somewhere on the site to initialize audio */
const resultPromise = new Promise<boolean>(resolve => {
spawnYesNo(tra("Connect to {}", serverAddress), tra("Would you like to connect to {}?", serverAddress), resolve).open();
const result = await promptYesNo({
title: tra("Connect to {}", serverAddress),
question: tra("Would you like to connect to {}?", serverAddress)
});
if(!(await resultPromise)) {
if(!result) {
/* Well... the client don't want to... */
return { status: "client-aborted" };
}
@ -441,13 +442,12 @@ const task_connect_handler: loader.Task = {
const chandler = getInstanceConnectHandler();
if(chandler && AppParameters.getValue(AppParameters.KEY_CONNECT_NO_SINGLE_INSTANCE)) {
try {
await chandler.post_connect_request(connectData, () => new Promise<boolean>(resolve => {
spawnYesNo(tr("Another TeaWeb instance is already running"), tra("Another TeaWeb instance is already running.\nWould you like to connect there?"), response => {
resolve(response);
}, {
closeable: false
}).open();
}));
await chandler.post_connect_request(connectData, async () => {
return await promptYesNo({
title: tr("Another TeaWeb instance is already running"),
question: tra("Another TeaWeb instance is already running.\nWould you like to connect there?")
});
});
logInfo(LogCategory.CLIENT, tr("Executed connect successfully in another browser window. Closing this window"));
createInfoModal(

View File

@ -10,13 +10,10 @@ import {ClientEntry, LocalClientEntry, MusicClientEntry} from "./Client";
import {ChannelTreeEntry} from "./ChannelTreeEntry";
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
import {Registry} from "tc-shared/events";
import * as React from "react";
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
import {createInputModal} from "tc-shared/ui/elements/Modal";
import {spawnBanClient} from "tc-shared/ui/modal/ModalBanClient";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessageString} from "tc-shared/ui/frames/chat";
import {tr, tra} from "tc-shared/i18n/localize";
import {initializeChannelTreeUiEvents} from "tc-shared/ui/tree/Controller";
import {ChannelTreePopoutController} from "tc-shared/ui/tree/popout/Controller";
@ -26,6 +23,7 @@ import {ClientIcon} from "svg-sprites/client-icons";
import "./EntryTagsHandler";
import {spawnChannelEditNew} from "tc-shared/ui/modal/channel-edit/Controller";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
export interface ChannelTreeEvents {
/* general tree notified */
@ -694,16 +692,19 @@ export class ChannelTree {
disabled: false,
callback: () => {
const param_string = clients.map((_, index) => "{" + index + "}").join(', ');
const param_values = clients.map(client => client.createChatTag(true));
const tag = $.spawn("div").append(...formatMessage(tr("Do you really want to delete ") + param_string, ...param_values));
const tag_container = $.spawn("div").append(tag);
spawnYesNo(tr("Are you sure?"), tag_container, result => {
if(result) {
for(const client of clients) {
this.client.serverConnection.send_command("musicbotdelete", {
botid: client.properties.client_database_id
});
}
const param_values = clients.map(client => client.clientNickName());
promptYesNo({
title: tr("Are you sure?"),
question: formatMessageString(tr("Do you really want to delete ") + param_string, ...param_values)
}).then(result => {
if(!result) {
return;
}
for(const client of clients) {
this.client.serverConnection.send_command("musicbotdelete", {
botid: client.properties.client_database_id
});
}
});
},
@ -797,11 +798,16 @@ export class ChannelTree {
name: tr("Delete all channels"),
icon_class: "client-delete",
callback: () => {
spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} channels?", channels.length), result => {
if(typeof result === "boolean" && result) {
for(const channel of channels) {
this.client.serverConnection.send_command("channeldelete", { cid: channel.channelId });
}
promptYesNo({
title: tr("Are you sure?"),
question: tra("Do you really want to delete {0} channels?", channels.length)
}).then(result => {
if(!result) {
return;
}
for(const channel of channels) {
this.client.serverConnection.send_command("channeldelete", { cid: channel.channelId });
}
});
}

View File

@ -15,8 +15,6 @@ import {ConnectionHandler, ViewReasonId} from "../ConnectionHandler";
import {openClientInfo} from "../ui/modal/ModalClientInfo";
import {spawnBanClient} from "../ui/modal/ModalBanClient";
import {spawnChangeLatency} from "../ui/modal/ModalChangeLatency";
import {formatMessage} from "../ui/frames/chat";
import {spawnYesNo} from "../ui/modal/ModalYesNo";
import * as hex from "../crypto/hex";
import {ChannelTreeEntry, ChannelTreeEntryEvents} from "./ChannelTreeEntry";
import {spawnClientVolumeChange, spawnMusicBotVolumeChange} from "../ui/modal/ModalChangeVolumeNew";
@ -27,10 +25,11 @@ import {VoiceClient} from "../voice/VoiceClient";
import {VoicePlayerEvents, VoicePlayerState} from "../voice/VoicePlayer";
import {ChannelTreeUIEvents} from "tc-shared/ui/tree/Definitions";
import {VideoClient} from "tc-shared/connection/VideoConnection";
import { tr } from "tc-shared/i18n/localize";
import {tr, tra} from "tc-shared/i18n/localize";
import {EventClient} from "tc-shared/connectionlog/Definitions";
import {W2GPluginCmdHandler} from "tc-shared/ui/modal/video-viewer/W2GPlugin";
import {spawnServerGroupAssignments} from "tc-shared/ui/modal/group-assignment/Controller";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
export enum ClientType {
CLIENT_VOICE,
@ -1236,13 +1235,17 @@ export class MusicClientEntry extends ClientEntry<MusicClientEvents> {
icon_class: "client-delete",
disabled: false,
callback: () => {
const tag = $.spawn("div").append(formatMessage(tr("Do you really want to delete {0}"), this.createChatTag(false)));
spawnYesNo(tr("Are you sure?"), $.spawn("div").append(tag), result => {
if(result) {
this.channelTree.client.serverConnection.send_command("musicbotdelete", {
bot_id: this.properties.client_database_id
}).then(() => {});
promptYesNo({
title: tr("Are you sure?"),
question: tra("Do you really want to delete {0}", this.clientNickName())
}).then(result => {
if(!result) {
return;
}
this.channelTree.client.serverConnection.send_command("musicbotdelete", {
bot_id: this.properties.client_database_id
}).then(() => {});
});
},
type: contextmenu.MenuEntryType.ENTRY

View File

@ -2,10 +2,10 @@ import {createErrorModal, createModal} from "../../ui/elements/Modal";
import {LogCategory, logError} from "../../log";
import {ConnectionHandler} from "../../ConnectionHandler";
import {base64_encode_ab} from "../../utils/buffers";
import {spawnYesNo} from "../../ui/modal/ModalYesNo";
import {ClientEntry} from "../../tree/Client";
import moment from "moment";
import {tr} from "tc-shared/i18n/localize";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
const avatar_to_uid = (id: string) => {
const buffer = new Uint8Array(id.length / 2);
@ -90,7 +90,10 @@ export function spawnAvatarList(client: ConnectionHandler) {
*/
callback_delete = () => {
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this avatar?"), result => {
promptYesNo({
title: tr("Are you sure?"),
question: tr("Do you really want to delete this avatar?"),
}).then(result => {
if (result) {
createErrorModal(tr("Not implemented"), tr("Avatar delete hasn't implemented yet")).open();
//TODO Implement avatar delete

View File

@ -2,12 +2,12 @@ import {createModal, Modal} from "tc-shared/ui/elements/Modal";
import {tra} from "tc-shared/i18n/localize";
import {Registry} from "tc-shared/events";
import {modal_settings, SettingProfileEvents} from "tc-shared/ui/modal/ModalSettings";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {initialize_audio_microphone_controller} from "tc-shared/ui/modal/settings/Microphone";
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
import * as React from "react";
import * as ReactDOM from "react-dom";
import {MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
export interface EventModalNewcomer {
"show_step": {
@ -65,7 +65,10 @@ export function openModalNewcomer(): Modal {
event_registry.on("exit_guide", event => {
if (event.ask_yesno) {
spawnYesNo(tr("Are you sure?"), tr("Do you really want to skip the basic setup guide?"), result => {
promptYesNo({
title: tr("Are you sure?"),
question: tr("Do you really want to skip the basic setup guide?"),
}).then(result => {
if (result)
event_registry.fire("exit_guide", {ask_yesno: false});
});

View File

@ -116,7 +116,10 @@ export function spawnQueryManage(client: ConnectionHandler) {
template.find(".button-query-delete").on('click', () => {
if(!selected_query) return;
Modals.spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this account?"), result => {
Modals.promptYesNo({
title: tr("Are you sure?"),
question: tr("Do you really want to delete this account?"),
}).then(result => {
if(result) {
client.serverConnection.send_command("querydelete", {
client_login_name: selected_query.username
@ -161,7 +164,6 @@ import {createErrorModal, createInfoModal, createInputModal, createModal, Modal}
import {CommandResult, QueryListEntry} from "../../connection/ServerConnectionDeclaration";
import {SingleCommandHandler} from "../../connection/ConnectionBase";
import {copyToClipboard} from "../../utils/helpers";
import {spawnYesNo} from "../../ui/modal/ModalYesNo";
import {LogCategory, logError} from "../../log";
import PermissionType from "../../permission/PermissionType";
import {ConnectionHandler} from "../../ConnectionHandler";
@ -169,6 +171,7 @@ import {spawnQueryCreate, spawnQueryCreated} from "../../ui/modal/ModalQuery";
import {formatMessage} from "../../ui/frames/chat";
import {ErrorCode} from "../../connection/ErrorCode";
import {tr} from "tc-shared/i18n/localize";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
export function spawnQueryManage(client: ConnectionHandler) {
let modal: Modal;
@ -336,7 +339,10 @@ export function spawnQueryManage(client: ConnectionHandler) {
button_delete.on('click', event => {
if (!selected_query) return;
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this account?"), result => {
promptYesNo({
title: tr("Are you sure?"),
question: tr("Do you really want to delete this account?"),
}).then(result => {
if (result) {
client.serverConnection.send_command("querydelete", {
client_login_name: selected_query.username

View File

@ -13,7 +13,6 @@ import {LogCategory, logDebug, logError, logTrace, logWarn} from "tc-shared/log"
import * as i18n from "tc-shared/i18n/localize";
import {RepositoryTranslation, TranslationRepository} from "tc-shared/i18n/localize";
import {Registry} from "tc-shared/events";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import * as i18nc from "tc-shared/i18n/country";
import * as forum from "tc-shared/profiles/identities/teaspeak-forum";
import {formatMessage, set_icon_size} from "tc-shared/ui/frames/chat";
@ -27,6 +26,7 @@ import {initialize_audio_microphone_controller} from "tc-shared/ui/modal/setting
import {MicrophoneSettings} from "tc-shared/ui/modal/settings/MicrophoneRenderer";
import {getBackend} from "tc-shared/backend";
import {MicrophoneSettingsEvents} from "tc-shared/ui/modal/settings/MicrophoneDefinitions";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
type ProfileInfoEvent = {
id: string,
@ -411,7 +411,10 @@ function settings_general_language(container: JQuery, modal: Modal) {
repo_tag.find(".button-delete").on('click', e => {
e.preventDefault();
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this repository?"), answer => {
promptYesNo({
title: tr("Are you sure?"),
question: tr("Do you really want to delete this repository?"),
}).then(answer => {
if (answer) {
i18n.delete_repository(repo);
update_list();
@ -1323,7 +1326,10 @@ export namespace modal_settings {
button.on('click', event => {
if (!current_profile || current_profile === "default") return;
spawnYesNo(tr("Are you sure?"), tr("Do you really want to delete this profile?"), result => {
promptYesNo({
title: tr("Are you sure?"),
question: tr("Do you really want to delete this profile?"),
}).then(result => {
if (result)
event_registry.fire("delete-profile", {profile_id: current_profile});
});
@ -1646,7 +1652,10 @@ export namespace modal_settings {
{
button_new.on('click', event => {
if (is_profile_generated) {
spawnYesNo(tr("Are you sure"), tr("Do you really want to generate a new identity and override the old identity?"), result => {
promptYesNo({
title: tr("Are you sure"),
question: tr("Do you really want to generate a new identity and override the old identity?"),
}).then(result => {
if (result) event_registry.fire("generate-identity-teamspeak", {profile_id: current_profile});
});
} else {
@ -1671,7 +1680,10 @@ export namespace modal_settings {
{
button_import.on('click', event => {
if (is_profile_generated) {
spawnYesNo(tr("Are you sure"), tr("Do you really want to import a new identity and override the old identity?"), result => {
promptYesNo({
title: tr("Are you sure"),
question: tr("Do you really want to import a new identity and override the old identity?"),
}).then(result => {
if (result) event_registry.fire("import-identity-teamspeak", {profile_id: current_profile});
});
} else {

View File

@ -1,11 +1,11 @@
import {BodyCreator, createModal, ModalFunctions} from "../../ui/elements/Modal";
import {BodyCreator, createModal, Modal, ModalFunctions} from "../../ui/elements/Modal";
export function spawnYesNo(header: BodyCreator, body: BodyCreator, callback: (_: boolean) => any, properties?: {
text_yes?: string,
text_no?: string,
closeable?: boolean;
}) {
}) : Modal {
properties = properties || {};
const props = ModalFunctions.warpProperties({});

View File

@ -8,7 +8,7 @@ import PermissionType from "tc-shared/permission/PermissionType";
import {getOwnAvatarStorage, LocalAvatarInfo} from "tc-shared/file/OwnAvatarStorage";
import {LogCategory, logError, logInfo, logWarn} from "tc-shared/log";
import {Mutex} from "tc-shared/Mutex";
import {tr, trJQuery} from "tc-shared/i18n/localize";
import {tr, tra, trJQuery} from "tc-shared/i18n/localize";
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {formatMessage} from "tc-shared/ui/frames/chat";
@ -262,7 +262,7 @@ class Controller {
if (transfer.transferState() !== FileTransferState.FINISHED) {
if (transfer.transferState() === FileTransferState.ERRORED) {
logWarn(LogCategory.FILE_TRANSFER, tr("Failed to upload clients avatar: %o"), transfer.currentError());
createErrorModal(tr("Failed to upload avatar"), tr("Failed to upload avatar:\n{0}", transfer.currentErrorMessage())).open();
createErrorModal(tr("Failed to upload avatar"), tra("Failed to upload avatar:\n{0}", transfer.currentErrorMessage())).open();
return;
} else if (transfer.transferState() === FileTransferState.CANCELED) {
createErrorModal(tr("Failed to upload avatar"), tr("Your avatar upload has been canceled.")).open();

View File

@ -19,7 +19,6 @@ import {getIconManager} from "tc-shared/file/Icons";
import {ClientIcon} from "svg-sprites/client-icons";
import {ClientIconRenderer} from "tc-shared/ui/react-elements/Icons";
import {spawnContextMenu} from "tc-shared/ui/ContextMenu";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {createErrorModal, createInfoModal, createInputModal} from "tc-shared/ui/elements/Modal";
import {HostBannerRenderer} from "tc-shared/ui/frames/HostBannerRenderer";
@ -32,6 +31,8 @@ import ServerInfoImage from "./serverinfo.png";
import {IconTooltip} from "tc-shared/ui/react-elements/Tooltip";
import {CountryCode} from "tc-shared/ui/react-elements/CountryCode";
import {downloadTextAsFile, requestFileAsText} from "tc-shared/file/Utils";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
import {tra} from "tc-shared/i18n/localize";
const EventContext = React.createContext<Registry<ModalBookmarkEvents>>(undefined);
const VariableContext = React.createContext<UiVariableConsumer<ModalBookmarkVariables>>(undefined);
@ -54,14 +55,16 @@ const BookmarkListEntryRenderer = React.memo((props: { entry: BookmarkListEntry
const tryDelete = () => {
if(props.entry.type === "directory" && props.entry.childCount > 0) {
spawnYesNo(tr("Are you sure?"), formatMessage(
tr("Do you really want to delete the directory \"{0}\"?{:br:}The directory contains {1} entries."),
props.entry.displayName, props.entry.childCount
), result => {
if(result) {
events.fire("action_delete_bookmark", { uniqueId: props.entry.uniqueId });
promptYesNo({
title: tr("Are you sure?"),
question: tra("Do you really want to delete the directory \"{0}\"?\nThe directory contains {1} entries.", props.entry.displayName, props.entry.childCount)
}).then(result => {
if(!result) {
return;
}
}).open();
events.fire("action_delete_bookmark", { uniqueId: props.entry.uniqueId });
});
} else {
events.fire("action_delete_bookmark", { uniqueId: props.entry.uniqueId });
}

View File

@ -5,8 +5,7 @@ import {DefaultTabValues} from "tc-shared/ui/modal/permission/ModalRenderer";
import {Group, GroupTarget, GroupType} from "tc-shared/permission/GroupManager";
import {createErrorModal, createInfoModal} from "tc-shared/ui/elements/Modal";
import {ClientNameInfo, CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
import {formatMessage} from "tc-shared/ui/frames/chat";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {formatMessage, formatMessageString} from "tc-shared/ui/frames/chat";
import {tra} from "tc-shared/i18n/localize";
import {PermissionType} from "tc-shared/permission/PermissionType";
import {GroupedPermissions, PermissionValue} from "tc-shared/permission/PermissionManager";
@ -32,6 +31,7 @@ import {
PermissionModalEvents
} from "tc-shared/ui/modal/permission/ModalDefinitions";
import {EditorGroupedPermissions, PermissionEditorEvents} from "tc-shared/ui/modal/permission/EditorDefinitions";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
export function spawnPermissionEditorModal(connection: ConnectionHandler, defaultTab: PermissionEditorTab = "groups-server", defaultTabValues?: DefaultTabValues) {
const modalEvents = new Registry<PermissionModalEvents>();
@ -96,7 +96,10 @@ function initializePermissionModalResultHandlers(events: Registry<PermissionModa
if (event.mode === "force")
return;
spawnYesNo(tr("Are you sure?"), formatMessage(tr("Do you really want to delete this group?")), result => {
promptYesNo({
title: tr("Are you sure?"),
question: formatMessageString(tr("Do you really want to delete this group?")),
}).then(result => {
if (result !== true)
return;

View File

@ -6,7 +6,6 @@ import PermissionType from "../../../permission/PermissionType";
import {LogCategory, logError, logTrace} from "../../../log";
import {Entry, MenuEntry, MenuEntryType, spawn_context_menu} from "../../../ui/elements/ContextMenu";
import {getKeyBoard, SpecialKey} from "../../../PPTListener";
import {spawnYesNo} from "../../../ui/modal/ModalYesNo";
import {tr, tra} from "../../../i18n/localize";
import {
FileTransfer,
@ -25,6 +24,7 @@ import {
ListedFileInfo,
PathInfo
} from "tc-shared/ui/modal/transfer/FileDefinitions";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
function parsePath(path: string, connection: ConnectionHandler): PathInfo {
if (path === "/" || !path) {
@ -508,12 +508,18 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand
}) : event.files;
if (event.mode === "ask") {
spawnYesNo(tr("Are you sure?"), tra("Do you really want to delete {0} {1}?", files.length, files.length === 1 ? tr("files") : tr("files")), result => {
if (result)
events.fire("action_delete_file", {
files: files,
mode: "force"
});
promptYesNo({
title: tr("Are you sure?"),
question: tra("Do you really want to delete {0} {1}?", files.length, files.length === 1 ? tr("files") : tr("files")),
}).then(result => {
if(!result) {
return;
}
events.fire("action_delete_file", {
files: files,
mode: "force"
});
});
return;
}

View File

@ -1,3 +1,33 @@
import {spawnModal} from "tc-shared/ui/react-elements/modal";
import {Registry} from "tc-events";
import {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
import {IpcUiVariableProvider} from "tc-shared/ui/utils/IpcVariable";
import {CallOnce, ignorePromise} from "tc-shared/proto";
class Controller {
readonly properties: YesNoParameters;
readonly events: Registry<ModalYesNoEvents>;
readonly variables: IpcUiVariableProvider<ModalYesNoVariables>;
constructor(properties: YesNoParameters) {
this.properties = properties;
this.events = new Registry<ModalYesNoEvents>();
this.variables = new IpcUiVariableProvider<ModalYesNoVariables>();
this.variables.setVariableProvider("title", () => this.properties.title);
this.variables.setVariableProvider("question", () => this.properties.question);
this.variables.setVariableProvider("textYes", () => this.properties.textYes);
this.variables.setVariableProvider("textNo", () => this.properties.textNo);
}
@CallOnce
destroy() {
this.events.destroy();
this.variables.destroy();
}
}
export interface YesNoParameters {
title: string,
question: string,
@ -20,5 +50,23 @@ export async function promptYesNo(properties: YesNoParameters) : Promise<boolean
throw "yes-no question isn't a string";
}
return false;
const controller = new Controller(properties);
const modal = spawnModal("modal-yes-no", [
controller.events.generateIpcDescription(),
controller.variables.generateConsumerDescription()
], {
popoutable: false,
destroyOnClose: true
});
modal.getEvents().on("destroy", () => controller.destroy());
ignorePromise(modal.show());
return await new Promise<boolean | undefined>(resolve => {
modal.getEvents().on("destroy", () => resolve());
controller.events.on("action_submit", event => {
resolve(event.status);
modal.destroy();
});
});
}

View File

@ -0,0 +1,11 @@
export interface ModalYesNoVariables {
readonly title: string,
readonly question: string,
readonly textYes: string | undefined,
readonly textNo: string | undefined,
}
export interface ModalYesNoEvents {
action_submit: { status: boolean }
}

View File

@ -0,0 +1,23 @@
.container {
padding: 1em;
}
.question {
min-height: 1.6em;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-end;
margin-top: 2em;
button {
min-width: 6em;
&:not(:last-of-type) {
margin-right: 1em;
}
}
}

View File

@ -0,0 +1,92 @@
import {AbstractModal} from "tc-shared/ui/react-elements/modal/Definitions";
import {IpcRegistryDescription, Registry} from "tc-events";
import {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
import {UiVariableConsumer} from "tc-shared/ui/utils/Variable";
import {createIpcUiVariableConsumer} from "tc-shared/ui/utils/IpcVariable";
import React from "react";
import {Translatable} from "tc-shared/ui/react-elements/i18n";
import {Button} from "tc-shared/ui/react-elements/Button";
const cssStyle = require("./Renderer.scss");
const QuestionRenderer = React.memo((props: { variables: UiVariableConsumer<ModalYesNoVariables> }) => {
const question = props.variables.useReadOnly("question", undefined, undefined);
return (
<div className={cssStyle.question}>
{question}
</div>
);
});
const TitleRenderer = React.memo((props: { variables: UiVariableConsumer<ModalYesNoVariables> }) => {
const title = props.variables.useReadOnly("title", undefined, undefined);
if(typeof title !== "undefined") {
return <React.Fragment key={"loaded"}>{title}</React.Fragment>;
} else {
return <Translatable key={"loading"}>loading</Translatable>;
}
});
const TextYes = React.memo((props: { variables: UiVariableConsumer<ModalYesNoVariables> }) => {
const text = props.variables.useReadOnly("textYes", undefined, undefined);
if(typeof text !== "undefined") {
return <React.Fragment key={"custom"}>{text}</React.Fragment>;
} else {
return <Translatable key={"default"}>Yes</Translatable>;
}
});
const TextNo = React.memo((props: { variables: UiVariableConsumer<ModalYesNoVariables> }) => {
const text = props.variables.useReadOnly("textNo", undefined, undefined);
if(typeof text !== "undefined") {
return <React.Fragment key={"custom"}>{text}</React.Fragment>;
} else {
return <Translatable key={"default"}>No</Translatable>;
}
});
class Modal extends AbstractModal {
private readonly events: Registry<ModalYesNoEvents>;
private readonly variables: UiVariableConsumer<ModalYesNoVariables>;
constructor(events: IpcRegistryDescription<ModalYesNoEvents>, variables: IpcRegistryDescription<ModalYesNoVariables>) {
super();
this.events = Registry.fromIpcDescription(events);
this.variables = createIpcUiVariableConsumer(variables);
}
protected onDestroy() {
super.onDestroy();
this.events.destroy();
this.variables.destroy();
}
color(): "none" | "blue" | "red" {
return "red";
}
renderBody(): React.ReactElement {
return (
<div className={cssStyle.container}>
<QuestionRenderer variables={this.variables} />
<div className={cssStyle.buttons}>
<Button color={"red"} onClick={() => this.events.fire("action_submit", { status: false })}>
<TextNo variables={this.variables} />
</Button>
<Button color={"green"} onClick={() => this.events.fire("action_submit", { status: true })}>
<TextYes variables={this.variables} />
</Button>
</div>
</div>
);
}
renderTitle(): string | React.ReactElement {
return (
<TitleRenderer variables={this.variables} />
);
}
}
export default Modal;

View File

@ -24,6 +24,7 @@ import {ModalInputProcessorEvents, ModalInputProcessorVariables} from "tc-shared
import {ModalServerInfoEvents, ModalServerInfoVariables} from "tc-shared/ui/modal/server-info/Definitions";
import {ModalAboutVariables} from "tc-shared/ui/modal/about/Definitions";
import {ModalServerBandwidthEvents} from "tc-shared/ui/modal/server-bandwidth/Definitions";
import {ModalYesNoEvents, ModalYesNoVariables} from "tc-shared/ui/modal/yes-no/Definitions";
export type ModalType = "error" | "warning" | "info" | "none";
export type ModalRenderType = "page" | "dialog";
@ -138,7 +139,7 @@ export abstract class AbstractModal {
/* only valid for the "inline" modals */
type() : ModalType { return "none"; }
color() : "none" | "blue" { return "none"; }
color() : "none" | "blue" | "red" { return "none"; }
verticalAlignment() : "top" | "center" | "bottom" { return "center"; }
/** @deprecated */
@ -165,6 +166,10 @@ export function constructAbstractModalClass<T extends keyof ModalConstructorArgu
export interface ModalConstructorArguments {
"__internal__modal__": any[],
"modal-yes-no": [
/* events */ IpcRegistryDescription<ModalYesNoEvents>,
/* variables */ IpcVariableDescriptor<ModalYesNoVariables>,
],
"video-viewer": [
/* events */ IpcRegistryDescription<VideoViewerEvents>,

View File

@ -19,6 +19,12 @@ function registerModal<T extends keyof ModalConstructorArguments>(modal: Registe
registeredModals[modal.modalId] = modal as any;
}
registerModal({
modalId: "modal-yes-no",
classLoader: async () => await import("tc-shared/ui/modal/yes-no/Renderer"),
popoutSupported: true
})
registerModal({
modalId: "video-viewer",
classLoader: async () => await import("tc-shared/ui/modal/video-viewer/Renderer"),

View File

@ -12,6 +12,4 @@ export class UnreadMarkerRenderer extends React.Component<{ entry: RDPEntry }, {
return null;
}
}
}
export class RendererTreeEntry<Props, State> extends ReactComponentBase<Props, State> { }
}

View File

@ -8,10 +8,10 @@ import {
import {assertMainApplication} from "tc-shared/ui/utils";
import {Registry} from "tc-events";
import {getIpcInstance} from "tc-shared/ipc/BrowserIPC";
import {spawnYesNo} from "tc-shared/ui/modal/ModalYesNo";
import {tr, tra} from "tc-shared/i18n/localize";
import {guid} from "tc-shared/crypto/uid";
import _ from "lodash";
import {promptYesNo} from "tc-shared/ui/modal/yes-no/Controller";
assertMainApplication();
@ -64,17 +64,16 @@ export class WebWindowManager implements WindowManager {
let windowInstance = this.tryCreateWindow(options, windowUniqueId);
if(!windowInstance) {
try {
await new Promise((resolve, reject) => {
spawnYesNo(tr("Would you like to open the popup?"), tra("Would you like to open window {}?", options.windowName), callback => {
if(!callback) {
reject();
return;
}
windowInstance = this.tryCreateWindow(options, windowUniqueId);
resolve();
});
const result = await promptYesNo({
title: tr("Would you like to open the popup?"),
question: tra("Would you like to open window {}?", options.windowName)
});
if(!result) {
return { status: "error-user-rejected" };
}
windowInstance = this.tryCreateWindow(options, windowUniqueId);
} catch (_) {
return { status: "error-user-rejected" };
}