From 5da2877c8b1d00523f530780876ba824fb915baf Mon Sep 17 00:00:00 2001 From: WolverinDEV Date: Fri, 18 Dec 2020 19:18:01 +0100 Subject: [PATCH] Added file transfer to the side bar --- shared/js/tree/Channel.ts | 2 + shared/js/ui/frames/SideBarRenderer.tsx | 20 +- .../js/ui/frames/side/ChannelBarController.ts | 10 +- .../ui/frames/side/ChannelBarDefinitions.ts | 3 +- .../js/ui/frames/side/ChannelBarRenderer.tsx | 14 +- .../side/ChannelFileBrowserController.ts | 64 ++ .../side/ChannelFileBrowserDefinitions.ts | 11 + .../side/ChannelFileBrowserRenderer.scss | 45 ++ .../side/ChannelFileBrowserRenderer.tsx | 53 ++ ...ller.ts => FileBrowserControllerRemote.ts} | 39 +- .../modal/transfer/FileBrowserRenderer.scss | 377 ++++++++++ ...ileBrowser.tsx => FileBrowserRenderer.tsx} | 672 +++++++++++++----- .../js/ui/modal/transfer/FileDefinitions.ts | 164 +++++ ...{TransferInfo.tsx => FileTransferInfo.tsx} | 55 +- ...oller.ts => FileTransferInfoController.ts} | 12 +- .../transfer/FileTransferInfoDefinitions.ts | 50 ++ ...nfo.scss => FileTransferInfoRenderer.scss} | 0 .../ui/modal/transfer/ModalFileTransfer.scss | 384 ---------- .../ui/modal/transfer/ModalFileTransfer.tsx | 195 +---- .../js/ui/react-elements/ErrorBoundary.scss | 0 shared/js/ui/react-elements/ErrorBoundary.ts | 23 + shared/js/ui/react-elements/Helper.ts | 4 + shared/js/ui/react-elements/InputField.tsx | 2 +- shared/js/ui/react-elements/Table.tsx | 3 +- 24 files changed, 1388 insertions(+), 814 deletions(-) create mode 100644 shared/js/ui/frames/side/ChannelFileBrowserController.ts create mode 100644 shared/js/ui/frames/side/ChannelFileBrowserDefinitions.ts create mode 100644 shared/js/ui/frames/side/ChannelFileBrowserRenderer.scss create mode 100644 shared/js/ui/frames/side/ChannelFileBrowserRenderer.tsx rename shared/js/ui/modal/transfer/{RemoteFileBrowserController.ts => FileBrowserControllerRemote.ts} (97%) create mode 100644 shared/js/ui/modal/transfer/FileBrowserRenderer.scss rename shared/js/ui/modal/transfer/{FileBrowser.tsx => FileBrowserRenderer.tsx} (60%) create mode 100644 shared/js/ui/modal/transfer/FileDefinitions.ts rename shared/js/ui/modal/transfer/{TransferInfo.tsx => FileTransferInfo.tsx} (93%) rename shared/js/ui/modal/transfer/{TransferInfoController.ts => FileTransferInfoController.ts} (93%) create mode 100644 shared/js/ui/modal/transfer/FileTransferInfoDefinitions.ts rename shared/js/ui/modal/transfer/{TransferInfo.scss => FileTransferInfoRenderer.scss} (100%) create mode 100644 shared/js/ui/react-elements/ErrorBoundary.scss create mode 100644 shared/js/ui/react-elements/ErrorBoundary.ts diff --git a/shared/js/tree/Channel.ts b/shared/js/tree/Channel.ts index 83483c0f..7c83220c 100644 --- a/shared/js/tree/Channel.ts +++ b/shared/js/tree/Channel.ts @@ -202,6 +202,7 @@ export class ChannelEntry extends ChannelTreeEntry { this.channelTree = channelTree; this.events = new Registry(); + this.subscribed = false; this.properties = new ChannelProperties(); this.channelId = channelId; this.properties.channel_name = channelName; @@ -712,6 +713,7 @@ export class ChannelEntry extends ChannelTreeEntry { cached_password() { return this.cachedPasswordHash; } async updateSubscribeMode() { + console.error("Update subscribe mode"); let shouldBeSubscribed = false; switch (this.subscriptionMode) { case ChannelSubscribeMode.INHERITED: diff --git a/shared/js/ui/frames/SideBarRenderer.tsx b/shared/js/ui/frames/SideBarRenderer.tsx index 4a655295..5d0752f7 100644 --- a/shared/js/ui/frames/SideBarRenderer.tsx +++ b/shared/js/ui/frames/SideBarRenderer.tsx @@ -8,6 +8,7 @@ import {PrivateConversationsPanel} from "tc-shared/ui/frames/side/PrivateConvers import {ChannelBarRenderer} from "tc-shared/ui/frames/side/ChannelBarRenderer"; import {LogCategory, logWarn} from "tc-shared/log"; import React = require("react"); +import {ErrorBoundary} from "tc-shared/ui/react-elements/ErrorBoundary"; const cssStyle = require("./SideBarRenderer.scss"); @@ -52,6 +53,7 @@ const ContentRendererClientInfo = () => { const contentData = useContentData("client-info"); if(!contentData) { return null; } + throw "XX"; return ( { const SideBarFrame = (props: { type: SideBarType }) => { switch (props.type) { case "channel": - return ; + return ( + + + + ); case "private-chat": - return ; + return ( + + + + ); case "client-info": - return ; + return ( + + + + ); case "music-manage": /* TODO! */ diff --git a/shared/js/ui/frames/side/ChannelBarController.ts b/shared/js/ui/frames/side/ChannelBarController.ts index c08f39f0..1facdf2e 100644 --- a/shared/js/ui/frames/side/ChannelBarController.ts +++ b/shared/js/ui/frames/side/ChannelBarController.ts @@ -5,12 +5,14 @@ import {ChannelEntry, ChannelSidebarMode} from "tc-shared/tree/Channel"; import {ChannelConversationController} from "tc-shared/ui/frames/side/ChannelConversationController"; import {ChannelDescriptionController} from "tc-shared/ui/frames/side/ChannelDescriptionController"; import {LocalClientEntry} from "tc-shared/tree/Client"; +import {ChannelFileBrowserController} from "tc-shared/ui/frames/side/ChannelFileBrowserController"; export class ChannelBarController { readonly uiEvents: Registry; private channelConversations: ChannelConversationController; private description: ChannelDescriptionController; + private fileBrowser: ChannelFileBrowserController; private currentConnection: ConnectionHandler; private listenerConnection: (() => void)[]; @@ -25,6 +27,7 @@ export class ChannelBarController { this.channelConversations = new ChannelConversationController(); this.description = new ChannelDescriptionController(); + this.fileBrowser = new ChannelFileBrowserController(); this.uiEvents.on("query_mode", () => this.notifyChannelMode()); this.uiEvents.on("query_channel_id", () => this.notifyChannelId()); @@ -41,6 +44,9 @@ export class ChannelBarController { this.currentChannel = undefined; this.currentConnection = undefined; + this.fileBrowser?.destroy(); + this.fileBrowser = undefined; + this.channelConversations?.destroy(); this.channelConversations = undefined; @@ -56,6 +62,7 @@ export class ChannelBarController { } this.channelConversations.setConnectionHandler(handler); + this.fileBrowser.setConnectionHandler(handler); this.listenerConnection.forEach(callback => callback()); this.listenerConnection = []; @@ -96,6 +103,7 @@ export class ChannelBarController { return; } + this.fileBrowser.setChannel(channel); this.description.setChannel(channel); this.listenerChannel.forEach(callback => callback()); @@ -185,9 +193,9 @@ export class ChannelBarController { this.uiEvents.fire_react("notify_data", { content: "file-transfer", data: { + events: this.fileBrowser.uiEvents } }); - /* TODO! */ break; } } diff --git a/shared/js/ui/frames/side/ChannelBarDefinitions.ts b/shared/js/ui/frames/side/ChannelBarDefinitions.ts index a14ffc18..51d552ed 100644 --- a/shared/js/ui/frames/side/ChannelBarDefinitions.ts +++ b/shared/js/ui/frames/side/ChannelBarDefinitions.ts @@ -1,6 +1,7 @@ import {Registry} from "tc-shared/events"; import {ChannelConversationUiEvents} from "tc-shared/ui/frames/side/ChannelConversationDefinitions"; import {ChannelDescriptionUiEvents} from "tc-shared/ui/frames/side/ChannelDescriptionDefinitions"; +import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions"; export type ChannelBarMode = "conversation" | "description" | "file-transfer" | "none"; @@ -12,7 +13,7 @@ export interface ChannelBarModeData { events: Registry }, "file-transfer": { - /* TODO! */ + events: Registry }, "none": {} } diff --git a/shared/js/ui/frames/side/ChannelBarRenderer.tsx b/shared/js/ui/frames/side/ChannelBarRenderer.tsx index bfaf031f..cc8b1a97 100644 --- a/shared/js/ui/frames/side/ChannelBarRenderer.tsx +++ b/shared/js/ui/frames/side/ChannelBarRenderer.tsx @@ -5,6 +5,7 @@ import * as React from "react"; import {ConversationPanel} from "tc-shared/ui/frames/side/AbstractConversationRenderer"; import {useDependentState} from "tc-shared/ui/react-elements/Helper"; import {ChannelDescriptionRenderer} from "tc-shared/ui/frames/side/ChannelDescriptionRenderer"; +import {ChannelFileBrowser} from "tc-shared/ui/frames/side/ChannelFileBrowserRenderer"; const EventContext = React.createContext>(undefined); const ChannelContext = React.createContext<{ channelId: number, handlerId: string }>(undefined); @@ -37,8 +38,7 @@ const ModeRenderer = () => { return ; case "file-transfer": - /* TODO! */ - return null; + return ; case "none": default: @@ -72,6 +72,16 @@ const ModeRendererDescription = React.memo(() => { ); }); +const ModeRendererFileTransfer = React.memo(() => { + const channelContext = useContext(ChannelContext); + const data = useModeData("file-transfer", [ channelContext ]); + if(!data) { return null; } + + return ( + + ); +}); + export const ChannelBarRenderer = (props: { events: Registry }) => { const [ channelContext, setChannelContext ] = useState<{ channelId: number, handlerId: string }>(() => { props.events.fire("query_channel_id"); diff --git a/shared/js/ui/frames/side/ChannelFileBrowserController.ts b/shared/js/ui/frames/side/ChannelFileBrowserController.ts new file mode 100644 index 00000000..49de6cd2 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelFileBrowserController.ts @@ -0,0 +1,64 @@ +import {ConnectionHandler} from "tc-shared/ConnectionHandler"; +import {Registry} from "tc-shared/events"; +import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer/FileBrowserControllerRemote"; +import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions"; +import {ChannelEntry} from "tc-shared/tree/Channel"; + +export class ChannelFileBrowserController { + readonly uiEvents: Registry; + + private currentConnection: ConnectionHandler; + private remoteBrowseEvents: Registry; + + private currentChannel: ChannelEntry; + + constructor() { + this.uiEvents = new Registry(); + this.uiEvents.on("query_events", () => this.notifyEvents()); + } + + destroy() { + this.currentChannel = undefined; + this.setConnectionHandler(undefined); + } + + setConnectionHandler(connection: ConnectionHandler) { + if(this.currentConnection === connection) { + return; + } + + if(this.remoteBrowseEvents) { + this.remoteBrowseEvents.fire("notify_destroy"); + this.remoteBrowseEvents.destroy(); + } + + this.currentConnection = connection; + + if(connection) { + this.remoteBrowseEvents = new Registry(); + initializeRemoteFileBrowserController(connection, this.remoteBrowseEvents); + } + + this.setChannel(undefined); + this.notifyEvents(); + } + + setChannel(channel: ChannelEntry | undefined) { + if(channel === this.currentChannel) { + return; + } + + this.currentChannel = channel; + + if(channel) { + this.remoteBrowseEvents?.fire("action_navigate_to", { path: "/" + channelPathPrefix + channel.channelId + "/" }); + } else { + this.remoteBrowseEvents?.fire("action_navigate_to", { path: "/" }); + } + } + + private notifyEvents() { + this.uiEvents.fire_react("notify_events", { browserEvents: this.remoteBrowseEvents, channelId: this.currentChannel?.channelId }); + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelFileBrowserDefinitions.ts b/shared/js/ui/frames/side/ChannelFileBrowserDefinitions.ts new file mode 100644 index 00000000..aba7fb8d --- /dev/null +++ b/shared/js/ui/frames/side/ChannelFileBrowserDefinitions.ts @@ -0,0 +1,11 @@ +import {FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {Registry} from "tc-shared/events"; + +export interface ChannelFileBrowserUiEvents { + query_events: {}, + + notify_events: { + browserEvents: Registry, + channelId: number + }, +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelFileBrowserRenderer.scss b/shared/js/ui/frames/side/ChannelFileBrowserRenderer.scss new file mode 100644 index 00000000..063bb556 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelFileBrowserRenderer.scss @@ -0,0 +1,45 @@ +.container { + display: flex; + flex-direction: column; + justify-content: stretch; + + height: 100%; + width: 100%; + + color: #999; + + .navbar { + flex-shrink: 0; + flex-grow: 0; + + padding: .5em; + } +} + +.fileTable { + border: none; + border-radius: 0; + + background-color: var(--chat-background); + + .header { + background-color: var(--chat-background); + } +} + +.fileEntry:hover, .fileEntry.hovered { + background-color: var(--channel-tree-entry-hovered) !important; +} + +.fileEntrySelected { + background-color: var(--channel-tree-entry-selected) !important; +} + +.boxedInput { + background-color: var(--side-info-background); + border-color: #212121; + + &:focus-within { + background-color: #242424; + } +} \ No newline at end of file diff --git a/shared/js/ui/frames/side/ChannelFileBrowserRenderer.tsx b/shared/js/ui/frames/side/ChannelFileBrowserRenderer.tsx new file mode 100644 index 00000000..1e69b2a3 --- /dev/null +++ b/shared/js/ui/frames/side/ChannelFileBrowserRenderer.tsx @@ -0,0 +1,53 @@ +import {useState} from "react"; +import {Registry} from "tc-shared/events"; +import {ChannelFileBrowserUiEvents} from "tc-shared/ui/frames/side/ChannelFileBrowserDefinitions"; +import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import { + FileBrowserClassContext, + FileBrowserRenderer, FileBrowserRendererClasses, + NavigationBar +} from "tc-shared/ui/modal/transfer/FileBrowserRenderer"; +import * as React from "react"; + +const cssStyle = require("./ChannelFileBrowserRenderer.scss"); + +const kFileBrowserClasses: FileBrowserRendererClasses = { + navigation: { + boxedInput: cssStyle.boxedInput + }, + fileTable: { + table: cssStyle.fileTable, + header: cssStyle.header + }, + fileEntry: { + entry: cssStyle.fileEntry, + dropHovered: cssStyle.hovered, + selected: cssStyle.fileEntrySelected + } +}; + +export const ChannelFileBrowser = (props: { events: Registry }) => { + const [ events, setEvents ] = useState<{ events: Registry, channelId: number }>(() => { + props.events.fire("query_events"); + return undefined; + }); + props.events.reactUse("notify_events", event => setEvents({ + events: event.browserEvents, + channelId: event.channelId + })); + + if(!events) { + return null; + } + + return ( +
+ +
+ 0 ? "/" + channelPathPrefix + events.channelId + "/" : "/"} /> +
+ 0 ? "/" + channelPathPrefix + events.channelId + "/" : "/"} events={events.events} key={"browser"} /> +
+
+ ); +}; \ No newline at end of file diff --git a/shared/js/ui/modal/transfer/RemoteFileBrowserController.ts b/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts similarity index 97% rename from shared/js/ui/modal/transfer/RemoteFileBrowserController.ts rename to shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts index f1a589b0..e7cfb694 100644 --- a/shared/js/ui/modal/transfer/RemoteFileBrowserController.ts +++ b/shared/js/ui/modal/transfer/FileBrowserControllerRemote.ts @@ -18,15 +18,13 @@ import { TransferTargetType } from "../../../file/Transfer"; import {createErrorModal} from "../../../ui/elements/Modal"; +import {ErrorCode} from "../../../connection/ErrorCode"; import { avatarsPathPrefix, - channelPathPrefix, - FileBrowserEvents, - iconPathPrefix, - ListedFileInfo, + channelPathPrefix, FileBrowserEvents, + iconPathPrefix, ListedFileInfo, PathInfo -} from "../../../ui/modal/transfer/ModalFileTransfer"; -import {ErrorCode} from "../../../connection/ErrorCode"; +} from "tc-shared/ui/modal/transfer/FileDefinitions"; function parsePath(path: string, connection: ConnectionHandler): PathInfo { if (path === "/" || !path) { @@ -46,7 +44,7 @@ function parsePath(path: string, connection: ConnectionHandler): PathInfo { const channel = connection.channelTree.findChannel(channelId); if (!channel) { - throw tr("Channel not visible anymore"); + throw tr("Invalid channel id"); } return { @@ -79,13 +77,13 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand try { const info = parsePath(event.path, connection); - events.fire_react("action_navigate_to_result", { + events.fire_react("notify_current_path", { path: event.path || "/", status: "success", pathInfo: info }); } catch (error) { - events.fire_react("action_navigate_to_result", { + events.fire_react("notify_current_path", { path: event.path, status: "error", error: error @@ -284,8 +282,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand let sourcePath: PathInfo, targetPath: PathInfo; try { sourcePath = parsePath(event.oldPath, connection); - if (sourcePath.type !== "channel") + if (sourcePath.type !== "channel") { throw tr("Icon/avatars could not be renamed"); + } } catch (error) { events.fire_react("action_rename_file_result", { oldPath: event.oldPath, @@ -297,8 +296,9 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand } try { targetPath = parsePath(event.newPath, connection); - if (sourcePath.type !== "channel") + if (sourcePath.type !== "channel") { throw tr("Target path isn't a channel"); + } } catch (error) { events.fire_react("action_rename_file_result", { oldPath: event.oldPath, @@ -374,15 +374,22 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand let currentPath = "/"; let currentPathInfo: PathInfo; let selection: { name: string, type: FileType }[] = []; - events.on("action_navigate_to_result", result => { - if (result.status !== "success") + events.on("notify_current_path", result => { + if (result.status !== "success") { return; + } currentPathInfo = result.pathInfo; currentPath = result.path; selection = []; }); + events.on("query_current_path", () => events.fire_react("notify_current_path", { + status: "success", + path: currentPath, + pathInfo: currentPathInfo + })); + events.on("action_rename_file_result", result => { if (result.status !== "success") return; @@ -812,10 +819,10 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand }); const closeListener = () => unregisterEvents(); - events.on("notify_modal_closed", closeListener); + events.on("notify_destroy", closeListener); const unregisterEvents = () => { - events.off("notify_modal_closed", closeListener); + events.off("notify_destroy", closeListener); transfer.events.off("notify_progress", progressListener); }; }; @@ -823,7 +830,7 @@ export function initializeRemoteFileBrowserController(connection: ConnectionHand const registeredListener = event => listenToTransfer(event.transfer); connection.fileManager.events.on("notify_transfer_registered", registeredListener); - events.on("notify_modal_closed", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener)); + events.on("notify_destroy", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener)); connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer)); } diff --git a/shared/js/ui/modal/transfer/FileBrowserRenderer.scss b/shared/js/ui/modal/transfer/FileBrowserRenderer.scss new file mode 100644 index 00000000..b9920413 --- /dev/null +++ b/shared/js/ui/modal/transfer/FileBrowserRenderer.scss @@ -0,0 +1,377 @@ +@import "../../../../css/static/mixin"; +@import "../../../../css/static/properties"; + +html:root { + --modal-transfer-refresh-hover: #ffffff0e; + --modal-transfer-path-hover: #E6E6E6; + + --modal-transfer-error-overlay-text: #9e9494; + + --modal-transfer-filelist: #28292b; + --modal-transfer-filelist-border: #161616; + + --modal-transfer-entry-hover: #2c2d2f; + --modal-transfer-entry-selected: #1a1a1b; + + --modal-transfer-indicator-red: #a10000; + --modal-transfer-indicator-red-end: #e60000; + + --modal-transfer-indicator-blue: #005fa1; + --modal-transfer-indicator-blue-end: #007acc; + + --modal-transfer-indicator-green: #389738; + --modal-transfer-indicator-green-end: #4ecc4e; + + --modal-transfer-indicator-hidden: #28292b00; + --modal-transfer-indicator-hidden-end: #28292b00; +} + +.container { + position: relative; + padding: 1em 1em 4em; /* 4em for the transfer info */ + + display: flex; + flex-direction: column; + justify-content: stretch; + + flex-shrink: 1; + flex-grow: 1; + + width: 90em; + min-width: 10em; + max-width: 100%; + + height: 55em; + max-height: 100%; + min-height: 10em; +} + +.arrow { + width: 1em; + + flex-shrink: 0; + flex-grow: 0; + + display: flex; + flex-direction: column; + justify-content: center; + + .inner { + flex-grow: 0; + flex-shrink: 0; + + align-self: center; + margin-left: -.09em; + + transform: rotate(-45deg); + -webkit-transform: rotate(-45deg); + + display: inline-block; + border: solid var(--text); + + border-width: 0 0.125em 0.125em 0; + padding: 0.15em; + + height: 0.15em; + width: .15em; + } +} + +.navigation { + flex-grow: 0; + flex-shrink: 0; + + .containerIcon { + margin: auto .25em; + padding: .2em; + + display: flex; + flex-direction: column; + justify-content: center; + + cursor: pointer; + + > div { + padding: .1em; + } + } + + .refreshIcon { + border-radius: 1px; + + @include transition(background-color $button_hover_animation_time ease-in-out); + + > div { + padding: .1em; + } + + &.enabled { + cursor: pointer; + + &:hover { + background-color: var(--modal-transfer-refresh-hover); + } + } + } + + .directoryIcon { + margin-right: -.25em; + } + + input { + margin-left: .5em; + } + + .containerPath { + @include user-select(none); + + display: flex; + flex-direction: row; + justify-content: flex-start; + + overflow: hidden; + white-space: nowrap; + + width: calc(100% - 1em); /* some space for the text editing */ + + a.pathShrink { + flex-shrink: 1; + min-width: 5em; + + @include text-dotdotdot(); + } + + a { + cursor: pointer; + + @include transition(color $button_hover_animation_time ease-in-out); + + &:hover, &.hovered { + color: var(--modal-transfer-path-hover); + } + } + } +} + +.fileTable { + min-height: 5em; + + flex-grow: 1; + flex-shrink: 1; + + border: 1px var(--modal-transfer-filelist-border) solid; + border-radius: 0.2em; + background-color: var(--modal-transfer-filelist); + + .header { + z-index: 1; + padding-top: .2em; + padding-bottom: .2em; + + background-color: var(--modal-transfer-filelist); + + .columnName, .columnSize, .columnType, .columnChanged { + position: relative; + + display: flex; + flex-direction: row; + justify-content: center; + + .separator { + position: absolute; + right: .2em; + top: .2em; + bottom: .2em; + + width: .1em; + background-color: var(--text); + } + } + + .columnSize { + width: 8em; + text-align: end; + } + + .columnName { + padding-left: .5em; + } + + > div:last-of-type { + .separator { + display: none; + } + } + } + + .body { + @include user-select(none); + @include chat-scrollbar-vertical(); + + .columnName { + padding-left: .5em; + + display: flex; + flex-direction: row; + justify-content: flex-start; + + a, div, img { + align-self: center; + margin-right: .5em; + + @include text-dotdotdot(); + } + + img, div { + flex-shrink: 0; + + height: 1em; + width: 1em; + } + + input { + height: 1.3em; + align-self: center; + + flex-grow: 1; + margin-right: .5em; + + border-style: inherit; + padding: .1em; + } + } + + .overlay { + position: absolute; + + top: 0; + left: 0; + right: 0; + bottom: 0; + + display: flex; + flex-direction: column; + justify-content: center; + + a { + text-align: center; + } + } + + .overlayError { + a { + font-size: 1.2em; + color: var(--modal-transfer-error-overlay-text); + } + } + + .overlayEmptyFolder { + align-self: center; + margin-top: 1em; + } + + .directoryEntry { + cursor: pointer; + + &:hover { + background-color: var(--modal-transfer-entry-hover); + } + + &.selected { + background-color: var(--modal-transfer-entry-selected); + } + + /* drag hovered overrides selected */ + &.hovered { + background-color: var(--modal-transfer-entry-hover); + } + + $indicator_transform_time: .5s; + + .indicator { + position: absolute; + + left: 0; + right: 30%; + top: 0; + bottom: 0; + + opacity: .4; + margin-right: 10px; /* for the gradient at the end */ + + .status { + position: absolute; + + left: 0; + top: 0; + bottom: 0; + + width: 2px; + @include transition(all $indicator_transform_time ease-in-out); + } + + &:after { + content: ' '; + + position: absolute; + + top: 0; + right: 0; + bottom: 0; + + height: 100%; + width: 10px; + + background-image: linear-gradient(to right, var(--modal-transfer-filelist), var(--modal-transfer-filelist)); + @include transition(all $indicator_transform_time ease-in-out); + } + + @include transition(all $indicator_transform_time ease-in-out); + + @mixin define-indicator($color, $colorLight) { + background-color: $color; + + .status { + background-color: $colorLight; + + -webkit-box-shadow: 0 0 12px 3px $colorLight; + -moz-box-shadow: 0 0 12px 3px $colorLight; + box-shadow: 0 0 12px 3px $colorLight; + } + + &:after { + background-image: linear-gradient(to right, $color, var(--modal-transfer-filelist)); + } + } + + &.red { + @include define-indicator(var(--modal-transfer-indicator-red), var(--modal-transfer-indicator-red-end)); + } + + &.blue { + @include define-indicator(var(--modal-transfer-indicator-blue), var(--modal-transfer-indicator-blue-end)); + } + + &.green { + @include define-indicator(var(--modal-transfer-indicator-green), var(--modal-transfer-indicator-green-end)); + } + + &.hidden { + @include define-indicator(var(--modal-transfer-indicator-hidden), var(--modal-transfer-indicator-hidden-end)); + } + } + } + } + + .columnSize { + text-align: end; + + a { + margin-right: 1em; + } + } + + .columnType { + text-align: center; + } +} \ No newline at end of file diff --git a/shared/js/ui/modal/transfer/FileBrowser.tsx b/shared/js/ui/modal/transfer/FileBrowserRenderer.tsx similarity index 60% rename from shared/js/ui/modal/transfer/FileBrowser.tsx rename to shared/js/ui/modal/transfer/FileBrowserRenderer.tsx index 2e79a46f..e45c442e 100644 --- a/shared/js/ui/modal/transfer/FileBrowser.tsx +++ b/shared/js/ui/modal/transfer/FileBrowserRenderer.tsx @@ -1,5 +1,5 @@ import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; -import {useEffect, useRef, useState} from "react"; +import {useContext, useEffect, useRef, useState} from "react"; import {FileType} from "tc-shared/file/FileManager"; import * as ppt from "tc-backend/ppt"; import {SpecialKey} from "tc-shared/PPTListener"; @@ -12,21 +12,42 @@ import {Translatable} from "tc-shared/ui/react-elements/i18n"; import * as Moment from "moment"; import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu"; import {BoxedInputField} from "tc-shared/ui/react-elements/InputField"; +import * as log from "tc-shared/log"; +import {LogCategory} from "tc-shared/log"; +import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; +import React = require("react"); import { FileBrowserEvents, FileTransferUrlMediaType, ListedFileInfo, TransferStatus -} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; -import * as log from "tc-shared/log"; -import {LogCategory} from "tc-shared/log"; -import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; -import React = require("react"); +} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {joinClassList} from "tc-shared/ui/react-elements/Helper"; -const cssStyle = require("./ModalFileTransfer.scss"); +export interface FileBrowserRendererClasses { + navigation?: { + boxedInput?: string + }, + fileTable?: { + table?: string, + header?: string, + body?: string + }, + fileEntry?: { + entry?: string, + selected?: string, + dropHovered?: string + } +} + +const EventsContext = React.createContext>(undefined); +const CustomClassContext = React.createContext(undefined); +export const FileBrowserClassContext = CustomClassContext; + +const cssStyle = require("./FileBrowserRenderer.scss"); interface NavigationBarProperties { - currentPath: string; + initialPath: string; events: Registry; } @@ -36,9 +57,11 @@ interface NavigationBarState { state: "editing" | "navigating" | "normal"; } -const ArrowRight = () =>
-
-
; +const ArrowRight = () => ( +
+
+
+); const NavigationEntry = (props: { events: Registry, path: string, name: string }) => { const [dragHovered, setDragHovered] = useState(false); @@ -56,11 +79,15 @@ const NavigationEntry = (props: { events: Registry, path: str 9 ? cssStyle.pathShrink : "") + " " + (dragHovered ? cssStyle.hovered : "")} title={props.name} - onClick={() => props.events.fire("action_navigate_to", {path: props.path})} + onClick={event => { + event.preventDefault(); + props.events.fire("action_navigate_to", {path: props.path}); + }} onDragOver={event => { const types = event.dataTransfer.types; - if (types.length !== 1) + if (types.length !== 1) { return; + } if (types[0] === FileTransferUrlMediaType) { /* TODO: Detect if its remote move or internal move */ @@ -77,8 +104,9 @@ const NavigationEntry = (props: { events: Registry, path: str onDragLeave={() => setDragHovered(false)} onDrop={event => { const types = event.dataTransfer.types; - if (types.length !== 1) + if (types.length !== 1) { return; + } /* TODO: Fix this code duplicate! */ if (types[0] === FileTransferUrlMediaType) { @@ -121,7 +149,7 @@ export class NavigationBar extends ReactComponentBase -
-
-
- } + + {customClasses => ( + +
+
+
+ } - rightIcon={() => -
-
-
- } + rightIcon={() => +
+
+
+ } - onChange={path => this.onPathEntered(path)} - onBlur={() => this.onInputPathBluer()} - /> + onChange={path => this.onPathEntered(path)} + onBlur={() => this.onInputPathBluer()} + className={customClasses?.navigation?.boxedInput} + /> + )} + ); } else if (this.state.state === "navigating" || this.state.state === "normal") { input = ( - -
this.onPathClicked(event, -1)}> -
-
- } + + {customClasses => ( + +
this.onPathClicked(event, -1)}> +
+
+ } - rightIcon={() => -
this.onButtonRefreshClicked()}> -
-
- } + rightIcon={() => +
this.onButtonRefreshClicked()}> +
+
+ } - inputBox={() => -
- {this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [ - , - - ])} -
- } + inputBox={() => +
+ {this.state.currentPath.split("/").filter(e => !!e).map((e, index, arr) => [ + , + + ])} +
+ } - editable={this.state.state === "normal"} - onFocus={() => this.onRenderedPathClicked()} - /> + editable={this.state.state === "normal"} + onFocus={event => !event.defaultPrevented && this.onRenderedPathClicked()} + className={customClasses?.navigation?.boxedInput} + /> + )} + ); } - return
{input}
; + return ( +
+ {input} +
+ ); } componentDidUpdate(prevProps: Readonly, prevState: Readonly, snapshot?: any): void { @@ -216,8 +258,9 @@ export class NavigationBar extends ReactComponentBase this.ignoreBlur = false); } - @EventHandler("action_navigate_to_result") - private handleNavigateResult(event: FileBrowserEvents["action_navigate_to_result"]) { - if (event.status === "success") + @EventHandler("notify_current_path") + private handleCurrentPath(event: FileBrowserEvents["notify_current_path"]) { + if (event.status === "success") { this.lastSucceededPath = event.path; + } this.setState({ state: "normal", @@ -279,7 +324,7 @@ export class NavigationBar extends ReactComponentBase; } @@ -288,7 +333,8 @@ interface FileListTableState { errorMessage?: string; } -const FileName = (props: { path: string, events: Registry, file: ListedFileInfo }) => { +const FileName = (props: { path: string, file: ListedFileInfo }) => { + const events = useContext(EventsContext); const [editing, setEditing] = useState(props.file.mode === "create"); const [fileName, setFileName] = useState(props.file.name); const refInput = useRef(); @@ -333,7 +379,7 @@ const FileName = (props: { path: string, events: Registry, fi if (props.file.mode === "create") { name = name || props.file.name; - props.events.fire("action_create_directory", { + events.fire("action_create_directory", { path: props.path, name: name }); @@ -342,7 +388,7 @@ const FileName = (props: { path: string, events: Registry, fi props.file.mode = "creating"; } else { if (name.length > 0 && name !== props.file.name) { - props.events.fire("action_rename_file", { + events.fire("action_rename_file", { oldName: props.file.name, newName: name, oldPath: props.path, @@ -381,19 +427,19 @@ const FileName = (props: { path: string, events: Registry, fi return; event.stopPropagation(); - props.events.fire("action_select_files", { + events.fire("action_select_files", { mode: "exclusive", files: [{name: props.file.name, type: props.file.type}] }); - props.events.fire("action_start_rename", { + events.fire("action_start_rename", { path: props.path, name: props.file.name }); }}>{fileName}; } - props.events.reactUse("action_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path)); - props.events.reactUse("action_rename_file_result", event => { + events.reactUse("action_start_rename", event => setEditing(event.name === props.file.name && event.path === props.path)); + events.reactUse("action_rename_file_result", event => { if (event.oldPath !== props.path || event.oldName !== props.file.name) return; @@ -418,10 +464,11 @@ const FileName = (props: { path: string, events: Registry, fi return <>{icon} {name}; }; -const FileSize = (props: { path: string, events: Registry, file: ListedFileInfo }) => { +const FileSize = (props: { path: string, file: ListedFileInfo }) => { + const events = useContext(EventsContext); const [size, setSize] = useState(-1); - props.events.reactUse("notify_transfer_status", event => { + events.reactUse("notify_transfer_status", event => { if (event.id !== props.file.transfer?.id) return; @@ -440,7 +487,7 @@ const FileSize = (props: { path: string, events: Registry, fi } }); - props.events.reactUse("notify_transfer_progress", event => { + events.reactUse("notify_transfer_progress", event => { if (event.id !== props.file.transfer?.id) return; @@ -450,13 +497,23 @@ const FileSize = (props: { path: string, events: Registry, fi setSize(event.fileSize); }); - if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined")) - return
unknown; - return {network.format_bytes(size >= 0 ? size : props.file.size, { - unit: "B", - time: "", - exact: false - })}; + if (size < 0 && (props.file.size < 0 || typeof props.file.size === "undefined")) { + return ( + + unknown + + ); + } + + return ( + + {network.format_bytes(size >= 0 ? size : props.file.size, { + unit: "B", + time: "", + exact: false + })} + + ); }; const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry }) => { @@ -520,6 +577,7 @@ const FileTransferIndicator = (props: { file: ListedFileInfo, events: Registry
@@ -532,18 +590,21 @@ const FileListEntry = (props: { row: TableRow, columns: TableCol const [hidden, setHidden] = useState(false); const [selected, setSelected] = useState(false); const [dropHovered, setDropHovered] = useState(false); + const customClasses = useContext(CustomClassContext); const onDoubleClicked = () => { if (file.type === FileType.DIRECTORY) { - if (file.mode === "creating" || file.mode === "create") + if (file.mode === "creating" || file.mode === "create") { return; + } props.events.fire("action_navigate_to", { path: file.path + file.name + "/" }); } else { - if (file.mode === "uploading" || file.virtual) + if (file.mode === "uploading" || file.virtual) { return; + } props.events.fire("action_start_download", { files: [{ @@ -577,12 +638,19 @@ const FileListEntry = (props: { row: TableRow, columns: TableCol }); }, !hidden); - if (hidden) + if (hidden) { return null; + } + + const elementClassList = joinClassList( + cssStyle.directoryEntry, customClasses?.fileEntry?.entry, + selected && cssStyle.selected, selected && customClasses?.fileEntry?.selected, + dropHovered && cssStyle.hovered, dropHovered && customClasses?.fileEntry?.dropHovered + ); return ( , columns: TableCol ); }; +type FileListState = { + state: "querying" | "invalid-password" +} | { + state: "no-permissions", + failedPermission: string +} | { + state: "error", + reason: string +} | { + state: "normal", + files: ListedFileInfo[] +}; + + +function fileTableHeaderContextMenu(event: React.MouseEvent, table: Table | undefined) { + event.preventDefault(); + + if(!table) { + return; + } + + spawn_context_menu(event.pageX, event.pageY, { + type: MenuEntryType.CHECKBOX, + name: tr("Size"), + checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "size") === -1, + callback: () => { + table.state.hiddenColumns.toggle("size"); + table.forceUpdate(); + } + }, { + type: MenuEntryType.CHECKBOX, + name: tr("Type"), + checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "type") === -1, + callback: () => { + table.state.hiddenColumns.toggle("type"); + table.forceUpdate(); + } + }, { + type: MenuEntryType.CHECKBOX, + name: tr("Last changed"), + checkbox_checked: table.state.hiddenColumns.findIndex(e => e === "change-date") === -1, + callback: () => { + table.state.hiddenColumns.toggle("change-date"); + table.forceUpdate(); + } + }) +} + +// const FileListRenderer = React.memo((props: { path: string }) => { +// const events = useContext(EventsContext); +// const customClasses = useContext(CustomClassContext); +// +// const refTable = useRef(); +// +// const [ state, setState ] = useState(() => { +// events.fire("query_files", { path: props.path }); +// return { state: "querying" }; +// }); +// +// events.reactUse("query_files", event => { +// if(event.path === props.path) { +// setState({ state: "querying" }); +// } +// }); +// +// events.reactUse("query_files_result", event => { +// if(event.path !== props.path) { +// return; +// } +// +// switch(event.status) { +// case "no-permissions": +// setState({ state: "no-permissions", failedPermission: event.error }); +// break; +// +// case "error": +// setState({ state: "error", reason: event.error }); +// break; +// +// case "success": +// setState({ state: "normal", files: event.files }); +// break; +// +// case "invalid-password": +// setState({ state: "invalid-password" }); +// break; +// +// case "timeout": +// setState({ state: "error", reason: tr("query timeout") }); +// break; +// +// default: +// setState({ state: "error", reason: tra("invalid query result state {}", event.status) }); +// break; +// } +// }); +// +// let rows: TableRow[] = []; +// let overlay; +// +// switch (state.state) { +// case "querying": +// overlay = () => ( +//
+// loading +//
+// ); +// break; +// +// case "error": +// overlay = () => ( +// +// ); +// break; +// +// case "no-permissions": +// overlay = () => ( +// +// ); +// break; +// +// case "invalid-password": +// /* TODO: Allow the user to enter a password */ +// overlay = () => ( +// +// ); +// break; +// +// case "normal": +// if(state.files.length === 0) { +// overlay = () => ( +// +// ); +// } else { +// const directories = state.files.filter(e => e.type === FileType.DIRECTORY); +// const files = state.files.filter(e => e.type === FileType.FILE); +// +// for (const directory of directories.sort((a, b) => a.name > b.name ? 1 : -1)) { +// rows.push({ +// columns: { +// "name": () => , +// "type": () => Directory, +// "change-date": () => directory.datetime ? +// {Moment(directory.datetime).format("DD/MM/YYYY HH:mm")} : undefined +// }, +// className: cssStyle.directoryEntry, +// userData: directory +// }); +// } +// +// for (const file of files.sort((a, b) => a.name > b.name ? 1 : -1)) { +// rows.push({ +// columns: { +// "name": () => , +// "size": () => , +// "type": () => File, +// "change-date": () => file.datetime ? +// {Moment(file.datetime).format("DD/MM/YYYY HH:mm")} : +// undefined +// }, +// className: cssStyle.directoryEntry, +// userData: file +// }); +// } +// } +// break; +// } +// +// return ( +//
[ +// Name, +//
+// ], width: 80, className: cssStyle.columnName +// }, +// { +// name: "type", header: () => [ +// Type, +//
+// ], fixedWidth: "8em", className: cssStyle.columnType +// }, +// { +// name: "size", header: () => [ +// Size, +//
+// ], fixedWidth: "8em", className: cssStyle.columnSize +// }, +// { +// name: "change-date", header: () => [ +// Last changed, +//
+// ], fixedWidth: "8em", className: cssStyle.columnChanged +// }, +// ]} +// rows={rows} +// +// bodyOverlayOnly={rows.length === 0} +// bodyOverlay={overlay} +// +// hiddenColumns={["type"]} +// +// onHeaderContextMenu={e => fileTableHeaderContextMenu(e, refTable.current)} +// onBodyContextMenu={event => { +// event.preventDefault(); +// events.fire("action_select_files", { mode: "exclusive", files: [] }); +// events.fire("action_selection_context_menu", { pageY: event.pageY, pageX: event.pageX }); +// }} +// onDrop={e => this.onDrop(e)} +// onDragOver={event => { +// const types = event.dataTransfer.types; +// if (types.length !== 1) +// return; +// +// if (types[0] === FileTransferUrlMediaType) { +// /* TODO: Detect if its remote move or internal move */ +// event.dataTransfer.effectAllowed = "move"; +// } else if (types[0] === "Files") { +// event.dataTransfer.effectAllowed = "copy"; +// } else { +// return; +// } +// +// event.preventDefault(); +// }} +// +// renderRow={(row: TableRow, columns, uniqueId) => ( +// +// )} +// /> +// ); +// }); + @ReactEventHandler(e => e.props.events) -export class FileBrowser extends ReactComponentBase { +export class FileBrowserRenderer extends ReactComponentBase { private refTable = React.createRef
(); private currentPath: string; private fileList: ListedFileInfo[]; @@ -724,8 +1041,7 @@ export class FileBrowser extends ReactComponentBase a.name > b.name ? 1 : -1)) { rows.push({ columns: { - "name": () => , + "name": () => , "type": () => Directory, "change-date": () => directory.datetime ? {Moment(directory.datetime).format("DD/MM/YYYY HH:mm")} : undefined @@ -738,8 +1054,8 @@ export class FileBrowser extends ReactComponentBase a.name > b.name ? 1 : -1)) { rows.push({ columns: { - "name": () => , - "size": () => , + "name": () => , + "size": () => , "type": () => File, "change-date": () => file.datetime ? {Moment(file.datetime).format("DD/MM/YYYY HH:mm")} : undefined @@ -752,74 +1068,84 @@ export class FileBrowser extends ReactComponentBase [ - Name, -
- ], width: 80, className: cssStyle.columnName - }, - { - name: "type", header: () => [ - Type, -
- ], fixedWidth: "8em", className: cssStyle.columnType - }, - { - name: "size", header: () => [ - Size, -
- ], fixedWidth: "8em", className: cssStyle.columnSize - }, - { - name: "change-date", header: () => [ - Last changed, -
- ], fixedWidth: "8em", className: cssStyle.columnChanged - }, - ]} - rows={rows} + + + {classes => ( +
[ + Name, +
+ ], width: 80, className: cssStyle.columnName + }, + { + name: "type", header: () => [ + Type, +
+ ], fixedWidth: "8em", className: cssStyle.columnType + }, + { + name: "size", header: () => [ + Size, +
+ ], fixedWidth: "8em", className: cssStyle.columnSize + }, + { + name: "change-date", header: () => [ + Last changed, +
+ ], fixedWidth: "8em", className: cssStyle.columnChanged + }, + ]} + rows={rows} - bodyOverlayOnly={overlayOnly} - bodyOverlay={overlay} + bodyOverlayOnly={overlayOnly} + bodyOverlay={overlay} - hiddenColumns={["type"]} + hiddenColumns={["type"]} - onHeaderContextMenu={e => this.onHeaderContextMenu(e)} - onBodyContextMenu={e => this.onBodyContextMenu(e)} - onDrop={e => this.onDrop(e)} - onDragOver={event => { - const types = event.dataTransfer.types; - if (types.length !== 1) - return; + onHeaderContextMenu={e => this.onHeaderContextMenu(e)} + onBodyContextMenu={e => this.onBodyContextMenu(e)} + onDrop={e => this.onDrop(e)} + onDragOver={event => { + const types = event.dataTransfer.types; + if (types.length !== 1) + return; - if (types[0] === FileTransferUrlMediaType) { - /* TODO: Detect if its remote move or internal move */ - event.dataTransfer.effectAllowed = "move"; - } else if (types[0] === "Files") { - event.dataTransfer.effectAllowed = "copy"; - } else { - return; - } + if (types[0] === FileTransferUrlMediaType) { + /* TODO: Detect if its remote move or internal move */ + event.dataTransfer.effectAllowed = "move"; + } else if (types[0] === "Files") { + event.dataTransfer.effectAllowed = "copy"; + } else { + return; + } - event.preventDefault(); - }} + event.preventDefault(); + }} - renderRow={(row: TableRow, columns, uniqueId) => } - /> + renderRow={(row: TableRow, columns, uniqueId) => ( + + )} + /> + )} + + ); } componentDidMount(): void { this.selection = []; - this.currentPath = this.props.currentPath; + this.currentPath = this.props.initialPath; + + this.props.events.fire("query_current_path", {}); this.props.events.fire("query_files", { path: this.currentPath }); @@ -827,21 +1153,22 @@ export class FileBrowser extends ReactComponentBase decodeURIComponent(e)); for (const fileUrl of fileUrls) { @@ -903,13 +1230,14 @@ export class FileBrowser extends ReactComponentBase("action_navigate_to_result") - private handleNavigationResult(event: FileBrowserEvents["action_navigate_to_result"]) { - if (event.status !== "success") + @EventHandler("notify_current_path") + private handleNavigationResult(event: FileBrowserEvents["notify_current_path"]) { + if (event.status !== "success") { return; + } this.currentPath = event.path; this.selection = []; @@ -983,8 +1311,9 @@ export class FileBrowser extends ReactComponentBase("action_start_create_directory") private handleActionFileCreateBegin(event: FileBrowserEvents["action_start_create_directory"]) { let index = 0; - while (this.fileList.find(e => e.name === (event.defaultName + (index > 0 ? " (" + index + ")" : "")))) + while (this.fileList.find(e => e.name === (event.defaultName + (index > 0 ? " (" + index + ")" : "")))) { index++; + } const name = event.defaultName + (index > 0 ? " (" + index + ")" : ""); this.fileList.push({ @@ -998,12 +1327,14 @@ export class FileBrowser extends ReactComponentBase this.props.events.fire_react("action_select_files", { - files: [{ - name: name, - type: FileType.DIRECTORY - }], mode: "exclusive" - })); + this.forceUpdate(() => { + this.props.events.fire_react("action_select_files", { + files: [{ + name: name, + type: FileType.DIRECTORY + }], mode: "exclusive" + }); + }); } @EventHandler("action_create_directory_result") @@ -1112,8 +1443,9 @@ export class FileBrowser extends ReactComponentBase("notify_transfer_status") private handleTransferStatus(event: FileBrowserEvents["notify_transfer_status"]) { const index = this.fileList.findIndex(e => e.transfer?.id === event.id); - if (index === -1) + if (index === -1) { return; + } let element = this.fileList[index]; if (event.status === "errored") { diff --git a/shared/js/ui/modal/transfer/FileDefinitions.ts b/shared/js/ui/modal/transfer/FileDefinitions.ts new file mode 100644 index 00000000..7342c624 --- /dev/null +++ b/shared/js/ui/modal/transfer/FileDefinitions.ts @@ -0,0 +1,164 @@ +import {FileType} from "tc-shared/file/FileManager"; +import {ChannelEntry} from "tc-shared/tree/Channel"; + +export const channelPathPrefix = tr("Channel") + " "; +export const iconPathPrefix = tr("Icons"); +export const avatarsPathPrefix = tr("Avatars"); +export const FileTransferUrlMediaType = "application/x-teaspeak-ft-urls"; + +export type TransferStatus = "pending" | "transferring" | "finished" | "errored" | "none"; +export type FileMode = "password" | "empty" | "create" | "creating" | "normal" | "uploading"; + +export type ListedFileInfo = { + path: string; + name: string; + type: FileType; + + datetime: number; + size: number; + + virtual: boolean; + mode: FileMode; + + transfer?: { + id: number; + direction: "upload" | "download"; + status: TransferStatus; + percent: number; + } | undefined +}; + +export type PathInfo = { + channelId: number; + channel: ChannelEntry; + + path: string; + type: "icon" | "avatar" | "channel" | "root"; +} + +export interface FileBrowserEvents { + action_navigate_to: { + path: string + }, + action_delete_file: { + files: { + path: string, + name: string + }[] | "selection"; + mode: "force" | "ask"; + }, + action_delete_file_result: { + results: { + path: string, + name: string, + status: "success" | "timeout" | "error"; + error?: string; + }[], + }, + + action_start_create_directory: { + defaultName: string + }, + action_create_directory: { + path: string, + name: string + }, + action_create_directory_result: { + path: string, + name: string, + status: "success" | "timeout" | "error"; + + error?: string; + }, + + action_rename_file: { + oldPath: string, + oldName: string, + + newPath: string; + newName: string + }, + action_rename_file_result: { + oldPath: string, + oldName: string, + status: "success" | "timeout" | "error" | "no-changes"; + + newPath?: string, + newName?: string, + error?: string; + }, + + action_start_rename: { + path: string; + name: string; + }, + + action_select_files: { + files: { + name: string, + type: FileType + }[] + mode: "exclusive" | "toggle" + }, + action_selection_context_menu: { + pageX: number, + pageY: number + }, + + action_start_download: { + files: { + path: string, + name: string + }[] + }, + action_start_upload: { + path: string; + mode: "files" | "browse"; + + files?: File[]; + }, + + query_files: { path: string }, + query_files_result: { + path: string, + status: "success" | "timeout" | "error" | "no-permissions" | "invalid-password", + + error?: string, + files?: ListedFileInfo[] + }, + query_current_path: {}, + + notify_current_path: { + path: string, + status: "success" | "timeout" | "error"; + error?: string; + pathInfo?: PathInfo + }, + + notify_transfer_start: { + path: string; + name: string; + + id: number; + mode: "upload" | "download"; + }, + + notify_transfer_status: { + id: number; + status: TransferStatus; + fileSize?: number; + }, + notify_transfer_progress: { + id: number; + progress: number; + fileSize: number; + status: TransferStatus + } + notify_drag_ended: {}, + /* Attention: Only use in sync mode! */ + notify_drag_started: { + event: DragEvent + } + + notify_destroy: {}, +} diff --git a/shared/js/ui/modal/transfer/TransferInfo.tsx b/shared/js/ui/modal/transfer/FileTransferInfo.tsx similarity index 93% rename from shared/js/ui/modal/transfer/TransferInfo.tsx rename to shared/js/ui/modal/transfer/FileTransferInfo.tsx index 6f1bc715..4f489216 100644 --- a/shared/js/ui/modal/transfer/TransferInfo.tsx +++ b/shared/js/ui/modal/transfer/FileTransferInfo.tsx @@ -1,7 +1,6 @@ import * as React from "react"; import {useEffect, useRef, useState} from "react"; import {EventHandler, ReactEventHandler, Registry} from "tc-shared/events"; -import {TransferStatus} from "tc-shared/ui/modal/transfer/ModalFileTransfer"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {HTMLRenderer} from "tc-shared/ui/react-elements/HTMLRenderer"; import {ProgressBar} from "tc-shared/ui/react-elements/ProgressBar"; @@ -11,60 +10,14 @@ import {format_time, network} from "tc-shared/ui/frames/chat"; import {LoadingDots} from "tc-shared/ui/react-elements/LoadingDots"; import {Checkbox} from "tc-shared/ui/react-elements/Checkbox"; import {Button} from "tc-shared/ui/react-elements/Button"; +import {TransferStatus} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions"; -const cssStyle = require("./TransferInfo.scss"); +const cssStyle = require("./FileTransferInfoRenderer.scss"); const iconArrow = require("./icon_double_arrow.svg"); const iconTransferUpload = require("./icon_transfer_upload.svg"); const iconTransferDownload = require("./icon_transfer_download.svg"); -export interface TransferInfoEvents { - query_transfers: {}, - query_transfer_result: { - status: "success" | "error" | "timeout"; - - error?: string; - transfers?: TransferInfoData[], - showFinished?: boolean - } - - action_toggle_expansion: { visible: boolean }, - action_toggle_finished_transfers: { visible: boolean }, - action_remove_finished: {}, - - notify_transfer_registered: { transfer: TransferInfoData }, - notify_transfer_status: { - id: number, - status: TransferStatus, - error?: string - }, - notify_transfer_progress: { - id: number; - status: TransferStatus, - progress: TransferProgress - }, - - notify_modal_closed: {} -} - -export interface TransferInfoData { - id: number; - - direction: "upload" | "download"; - status: TransferStatus; - - name: string; - path: string; - - progress: number; - error?: string; - - timestampRegistered: number; - timestampBegin: number; - timestampEnd: number; - - transferredBytes: number; -} - const ExpendState = (props: { extended: boolean, events: Registry }) => { const [expended, setExpended] = useState(props.extended); @@ -495,7 +448,7 @@ const ExtendedInfo = (props: { events: Registry }) => {
; }; -export const TransferInfo = (props: { events: Registry }) => ( +export const FileTransferInfo = (props: { events: Registry }) => (
diff --git a/shared/js/ui/modal/transfer/TransferInfoController.ts b/shared/js/ui/modal/transfer/FileTransferInfoController.ts similarity index 93% rename from shared/js/ui/modal/transfer/TransferInfoController.ts rename to shared/js/ui/modal/transfer/FileTransferInfoController.ts index 587ad2f4..b379f784 100644 --- a/shared/js/ui/modal/transfer/TransferInfoController.ts +++ b/shared/js/ui/modal/transfer/FileTransferInfoController.ts @@ -7,14 +7,14 @@ import { TransferProgress, TransferProperties } from "../../../file/Transfer"; +import {Settings, settings} from "../../../settings"; import { avatarsPathPrefix, channelPathPrefix, iconPathPrefix, TransferStatus -} from "../../../ui/modal/transfer/ModalFileTransfer"; -import {Settings, settings} from "../../../settings"; -import {TransferInfoData, TransferInfoEvents} from "../../../ui/modal/transfer/TransferInfo"; +} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {TransferInfoData, TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions"; export const initializeTransferInfoController = (connection: ConnectionHandler, events: Registry) => { const generateTransferPath = (properties: TransferProperties) => { @@ -128,10 +128,10 @@ export const initializeTransferInfoController = (connection: ConnectionHandler, events.fire("notify_transfer_registered", {transfer: generateTransferInfo(transfer)}); const closeListener = () => unregisterEvents(); - events.on("notify_modal_closed", closeListener); + events.on("notify_destroy", closeListener); const unregisterEvents = () => { - events.off("notify_modal_closed", closeListener); + events.off("notify_destroy", closeListener); transfer.events.off("notify_progress", progressListener); }; }; @@ -139,7 +139,7 @@ export const initializeTransferInfoController = (connection: ConnectionHandler, const registeredListener = event => listenToTransfer(event.transfer); connection.fileManager.events.on("notify_transfer_registered", registeredListener); - events.on("notify_modal_closed", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener)); + events.on("notify_destroy", () => connection.fileManager.events.off("notify_transfer_registered", registeredListener)); connection.fileManager.registeredTransfers().forEach(transfer => listenToTransfer(transfer)); } diff --git a/shared/js/ui/modal/transfer/FileTransferInfoDefinitions.ts b/shared/js/ui/modal/transfer/FileTransferInfoDefinitions.ts new file mode 100644 index 00000000..21d88789 --- /dev/null +++ b/shared/js/ui/modal/transfer/FileTransferInfoDefinitions.ts @@ -0,0 +1,50 @@ +import {TransferStatus} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {TransferProgress} from "tc-shared/file/Transfer"; + +export interface TransferInfoEvents { + query_transfers: {}, + query_transfer_result: { + status: "success" | "error" | "timeout"; + + error?: string; + transfers?: TransferInfoData[], + showFinished?: boolean + } + + action_toggle_expansion: { visible: boolean }, + action_toggle_finished_transfers: { visible: boolean }, + action_remove_finished: {}, + + notify_transfer_registered: { transfer: TransferInfoData }, + notify_transfer_status: { + id: number, + status: TransferStatus, + error?: string + }, + notify_transfer_progress: { + id: number; + status: TransferStatus, + progress: TransferProgress + }, + + notify_destroy: {} +} + +export interface TransferInfoData { + id: number; + + direction: "upload" | "download"; + status: TransferStatus; + + name: string; + path: string; + + progress: number; + error?: string; + + timestampRegistered: number; + timestampBegin: number; + timestampEnd: number; + + transferredBytes: number; +} \ No newline at end of file diff --git a/shared/js/ui/modal/transfer/TransferInfo.scss b/shared/js/ui/modal/transfer/FileTransferInfoRenderer.scss similarity index 100% rename from shared/js/ui/modal/transfer/TransferInfo.scss rename to shared/js/ui/modal/transfer/FileTransferInfoRenderer.scss diff --git a/shared/js/ui/modal/transfer/ModalFileTransfer.scss b/shared/js/ui/modal/transfer/ModalFileTransfer.scss index 75774784..e69de29b 100644 --- a/shared/js/ui/modal/transfer/ModalFileTransfer.scss +++ b/shared/js/ui/modal/transfer/ModalFileTransfer.scss @@ -1,384 +0,0 @@ -@import "../../../../css/static/mixin"; -@import "../../../../css/static/properties"; - -html:root { - --modal-transfer-refresh-hover: #ffffff0e; - --modal-transfer-path-hover: #E6E6E6; - - --modal-transfer-error-overlay-text: #9e9494; - - --modal-transfer-filelist: #28292b; - --modal-transfer-filelist-border: #161616; - - --modal-transfer-entry-hover: #2c2d2f; - --modal-transfer-entry-selected: #1a1a1b; - - --modal-transfer-indicator-red: #a10000; - --modal-transfer-indicator-red-end: #e60000; - - --modal-transfer-indicator-blue: #005fa1; - --modal-transfer-indicator-blue-end: #007acc; - - --modal-transfer-indicator-green: #389738; - --modal-transfer-indicator-green-end: #4ecc4e; - - --modal-transfer-indicator-hidden: #28292b00; - --modal-transfer-indicator-hidden-end: #28292b00; -} - -.container { - padding: 1em; - position: relative; - padding-bottom: 4em; /* for the transfer info */ - - display: flex; - flex-direction: column; - justify-content: stretch; - - flex-shrink: 1; - flex-grow: 1; - - width: 90em; - min-width: 10em; - max-width: 100%; - - height: 55em; - max-height: 100%; - min-height: 10em; - - .navigation { - flex-grow: 0; - flex-shrink: 0; - - .icon { - margin: auto .25em; - padding: .2em; - - display: flex; - flex-direction: column; - justify-content: center; - - cursor: pointer; - - > div { - padding: .1em; - } - } - - .refreshIcon { - border-radius: 1px; - - @include transition(background-color $button_hover_animation_time ease-in-out); - - > div { - padding: .1em; - } - - &.enabled { - cursor: pointer; - - &:hover { - background-color: var(--modal-transfer-refresh-hover); - } - } - } - - .directoryIcon { - margin-right: -.25em; - } - - input { - margin-left: .5em; - } - - .containerPath { - @include user-select(none); - - display: flex; - flex-direction: row; - justify-content: flex-start; - - overflow: hidden; - white-space: nowrap; - - width: calc(100% - 1em); /* some space for the text editing */ - - a.pathShrink { - flex-shrink: 1; - min-width: 5em; - - @include text-dotdotdot(); - } - - a { - cursor: pointer; - - @include transition(color $button_hover_animation_time ease-in-out); - - &:hover, &.hovered { - color: var(--modal-transfer-path-hover); - } - } - } - } - - .fileTable { - min-height: 5em; - - flex-grow: 1; - flex-shrink: 1; - - margin-top: 1em; - - border: 1px var(--modal-transfer-filelist-border) solid; - border-radius: 0.2em; - background-color: var(--modal-transfer-filelist); - - .header { - z-index: 1; - padding-top: .2em; - padding-bottom: .2em; - - background-color: var(--modal-transfer-filelist); - - .columnName, .columnSize, .columnType, .columnChanged { - position: relative; - - display: flex; - flex-direction: row; - justify-content: center; - - .seperator { - position: absolute; - right: .2em; - top: .2em; - bottom: .2em; - - width: .1em; - background-color: var(--text); - } - } - - .columnSize { - width: 8em; - text-align: end; - } - - .columnName { - padding-left: .5em; - } - - > div:last-of-type { - .seperator { - display: none; - } - } - } - - .body { - @include user-select(none); - @include chat-scrollbar-vertical(); - - .columnName { - padding-left: .5em; - - display: flex; - flex-direction: row; - justify-content: flex-start; - - a, div, img { - align-self: center; - margin-right: .5em; - - @include text-dotdotdot(); - } - - img, div { - flex-shrink: 0; - - height: 1em; - width: 1em; - } - - input { - height: 1.3em; - align-self: center; - - flex-grow: 1; - margin-right: .5em; - - border-style: inherit; - padding: .1em; - } - } - - .overlay { - position: absolute; - - top: 0; - left: 0; - right: 0; - bottom: 0; - - display: flex; - flex-direction: column; - justify-content: center; - - a { - text-align: center; - } - } - - .overlayError { - a { - font-size: 1.2em; - color: var(--modal-transfer-error-overlay-text); - } - } - - .overlayEmptyFolder { - align-self: center; - margin-top: 1em; - } - - .directoryEntry { - cursor: pointer; - - &:hover { - background-color: var(--modal-transfer-entry-hover); - } - - &.selected { - background-color: var(--modal-transfer-entry-selected); - } - - /* drag hovered overrides selected */ - &.hovered { - background-color: var(--modal-transfer-entry-hover); - } - - $indicator_transform_time: .5s; - - .indicator { - position: absolute; - - left: 0; - right: 30%; - top: 0; - bottom: 0; - - opacity: .4; - margin-right: 10px; /* for the gradient at the end */ - - .status { - position: absolute; - - left: 0; - top: 0; - bottom: 0; - - width: 2px; - @include transition(all $indicator_transform_time ease-in-out); - } - - &:after { - content: ' '; - - position: absolute; - - top: 0; - right: 0; - bottom: 0; - - height: 100%; - width: 10px; - - background-image: linear-gradient(to right, var(--modal-transfer-filelist), var(--modal-transfer-filelist)); - @include transition(all $indicator_transform_time ease-in-out); - } - - @include transition(all $indicator_transform_time ease-in-out); - - @mixin define-indicator($color, $colorLight) { - background-color: $color; - - .status { - background-color: $colorLight; - - -webkit-box-shadow: 0 0 12px 3px $colorLight; - -moz-box-shadow: 0 0 12px 3px $colorLight; - box-shadow: 0 0 12px 3px $colorLight; - } - - &:after { - background-image: linear-gradient(to right, $color, var(--modal-transfer-filelist)); - } - } - - &.red { - @include define-indicator(var(--modal-transfer-indicator-red), var(--modal-transfer-indicator-red-end)); - } - - &.blue { - @include define-indicator(var(--modal-transfer-indicator-blue), var(--modal-transfer-indicator-blue-end)); - } - - &.green { - @include define-indicator(var(--modal-transfer-indicator-green), var(--modal-transfer-indicator-green-end)); - } - - &.hidden { - @include define-indicator(var(--modal-transfer-indicator-hidden), var(--modal-transfer-indicator-hidden-end)); - } - } - } - } - - .columnSize { - text-align: end; - - a { - margin-right: 1em; - } - } - - .columnType { - text-align: center; - } - } - - .row { - - } -} - -.arrow { - width: 1em; - - flex-shrink: 0; - flex-grow: 0; - - display: flex; - flex-direction: column; - justify-content: center; - - .inner { - flex-grow: 0; - flex-shrink: 0; - - align-self: center; - margin-left: -.09em; - - transform: rotate(-45deg); - -webkit-transform: rotate(-45deg); - - display: inline-block; - border: solid var(--text); - - border-width: 0 0.125em 0.125em 0; - padding: 0.15em; - - height: 0.15em; - width: .15em; - } -} \ No newline at end of file diff --git a/shared/js/ui/modal/transfer/ModalFileTransfer.tsx b/shared/js/ui/modal/transfer/ModalFileTransfer.tsx index 25930131..42f61c98 100644 --- a/shared/js/ui/modal/transfer/ModalFileTransfer.tsx +++ b/shared/js/ui/modal/transfer/ModalFileTransfer.tsx @@ -1,180 +1,17 @@ import {spawnReactModal} from "tc-shared/ui/react-elements/Modal"; import * as React from "react"; -import {FileType} from "tc-shared/file/FileManager"; import {Registry} from "tc-shared/events"; -import {FileBrowser, NavigationBar} from "tc-shared/ui/modal/transfer/FileBrowser"; -import {TransferInfo, TransferInfoEvents} from "tc-shared/ui/modal/transfer/TransferInfo"; -import {initializeRemoteFileBrowserController} from "tc-shared/ui/modal/transfer/RemoteFileBrowserController"; -import {ChannelEntry} from "tc-shared/tree/Channel"; -import {initializeTransferInfoController} from "tc-shared/ui/modal/transfer/TransferInfoController"; +import {FileBrowserRenderer, NavigationBar} from "./FileBrowserRenderer"; +import {FileTransferInfo} from "./FileTransferInfo"; +import {initializeRemoteFileBrowserController} from "./FileBrowserControllerRemote"; +import {initializeTransferInfoController} from "./FileTransferInfoController"; import {Translatable} from "tc-shared/ui/react-elements/i18n"; import {InternalModal} from "tc-shared/ui/react-elements/internal-modal/Controller"; import {server_connections} from "tc-shared/ConnectionManager"; +import {channelPathPrefix, FileBrowserEvents} from "tc-shared/ui/modal/transfer/FileDefinitions"; +import {TransferInfoEvents} from "tc-shared/ui/modal/transfer/FileTransferInfoDefinitions"; -const cssStyle = require("./ModalFileTransfer.scss"); -export const channelPathPrefix = tr("Channel") + " "; -export const iconPathPrefix = tr("Icons"); -export const avatarsPathPrefix = tr("Avatars"); -export const FileTransferUrlMediaType = "application/x-teaspeak-ft-urls"; - -export type TransferStatus = "pending" | "transferring" | "finished" | "errored" | "none"; -export type FileMode = "password" | "empty" | "create" | "creating" | "normal" | "uploading"; - -export type ListedFileInfo = { - path: string; - name: string; - type: FileType; - - datetime: number; - size: number; - - virtual: boolean; - mode: FileMode; - - transfer?: { - id: number; - direction: "upload" | "download"; - status: TransferStatus; - percent: number; - } | undefined -}; - -export type PathInfo = { - channelId: number; - channel: ChannelEntry; - - path: string; - type: "icon" | "avatar" | "channel" | "root"; -} - -export interface FileBrowserEvents { - query_files: { path: string }, - query_files_result: { - path: string, - status: "success" | "timeout" | "error" | "no-permissions" | "invalid-password", - - error?: string, - files?: ListedFileInfo[] - }, - - action_navigate_to: { - path: string - }, - action_navigate_to_result: { - path: string, - status: "success" | "timeout" | "error"; - error?: string; - pathInfo?: PathInfo - } - - action_delete_file: { - files: { - path: string, - name: string - }[] | "selection"; - mode: "force" | "ask"; - }, - action_delete_file_result: { - results: { - path: string, - name: string, - status: "success" | "timeout" | "error"; - error?: string; - }[], - }, - - action_start_create_directory: { - defaultName: string - }, - action_create_directory: { - path: string, - name: string - }, - action_create_directory_result: { - path: string, - name: string, - status: "success" | "timeout" | "error"; - - error?: string; - }, - - action_rename_file: { - oldPath: string, - oldName: string, - - newPath: string; - newName: string - }, - action_rename_file_result: { - oldPath: string, - oldName: string, - status: "success" | "timeout" | "error" | "no-changes"; - - newPath?: string, - newName?: string, - error?: string; - }, - - action_start_rename: { - path: string; - name: string; - }, - - action_select_files: { - files: { - name: string, - type: FileType - }[] - mode: "exclusive" | "toggle" - }, - action_selection_context_menu: { - pageX: number, - pageY: number - }, - - action_start_download: { - files: { - path: string, - name: string - }[] - }, - action_start_upload: { - path: string; - mode: "files" | "browse"; - - files?: File[]; - }, - - notify_transfer_start: { - path: string; - name: string; - - id: number; - mode: "upload" | "download"; - }, - - notify_transfer_status: { - id: number; - status: TransferStatus; - fileSize?: number; - }, - notify_transfer_progress: { - id: number; - progress: number; - fileSize: number; - status: TransferStatus - } - - - notify_modal_closed: {}, - notify_drag_ended: {}, - - /* Attention: Only use in sync mode! */ - notify_drag_started: { - event: DragEvent - } -} - +const cssStyle = require("./FileBrowserRenderer.scss"); class FileTransferModal extends InternalModal { readonly remoteBrowseEvents = new Registry(); @@ -196,12 +33,12 @@ class FileTransferModal extends InternalModal { protected onInitialize() { const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/"; - this.remoteBrowseEvents.fire("action_navigate_to", {path: path}); + this.remoteBrowseEvents.fire("action_navigate_to", { path: path }); } protected onDestroy() { - this.remoteBrowseEvents.fire("notify_modal_closed"); - this.transferInfoEvents.fire("notify_modal_closed"); + this.remoteBrowseEvents.fire("notify_destroy"); + this.transferInfoEvents.fire("notify_destroy"); } title() { @@ -210,11 +47,13 @@ class FileTransferModal extends InternalModal { renderBody() { const path = this.defaultChannelId ? "/" + channelPathPrefix + this.defaultChannelId + "/" : "/"; - return
- - - -
+ return ( +
+ + + +
+ ) } } diff --git a/shared/js/ui/react-elements/ErrorBoundary.scss b/shared/js/ui/react-elements/ErrorBoundary.scss new file mode 100644 index 00000000..e69de29b diff --git a/shared/js/ui/react-elements/ErrorBoundary.ts b/shared/js/ui/react-elements/ErrorBoundary.ts new file mode 100644 index 00000000..1f585c8f --- /dev/null +++ b/shared/js/ui/react-elements/ErrorBoundary.ts @@ -0,0 +1,23 @@ +import * as React from "react"; + +interface ErrorBoundaryState { + errorOccurred: boolean +} + +export class ErrorBoundary extends React.Component<{}, ErrorBoundaryState> { + render() { + if(this.state.errorOccurred) { + + } else { + return this.props.children; + } + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("Did catch: %o - %o", error, errorInfo); + } + + static getDerivedStateFromError() : Partial { + return { errorOccurred: true }; + } +} \ No newline at end of file diff --git a/shared/js/ui/react-elements/Helper.ts b/shared/js/ui/react-elements/Helper.ts index ad4a899f..4b5cb9df 100644 --- a/shared/js/ui/react-elements/Helper.ts +++ b/shared/js/ui/react-elements/Helper.ts @@ -23,4 +23,8 @@ export function useDependentState( }, inputs); return [state, setState]; +} + +export function joinClassList(...classes: any[]) : string { + return classes.filter(value => typeof value === "string" && value.length > 0).join(" "); } \ No newline at end of file diff --git a/shared/js/ui/react-elements/InputField.tsx b/shared/js/ui/react-elements/InputField.tsx index 2723039d..98c3dd26 100644 --- a/shared/js/ui/react-elements/InputField.tsx +++ b/shared/js/ui/react-elements/InputField.tsx @@ -26,7 +26,7 @@ export interface BoxedInputFieldProperties { size?: "normal" | "large" | "small"; - onFocus?: () => void; + onFocus?: (event: React.FocusEvent | React.MouseEvent) => void; onBlur?: () => void; onChange?: (newValue: string) => void; diff --git a/shared/js/ui/react-elements/Table.tsx b/shared/js/ui/react-elements/Table.tsx index 76810c96..b466e740 100644 --- a/shared/js/ui/react-elements/Table.tsx +++ b/shared/js/ui/react-elements/Table.tsx @@ -137,8 +137,9 @@ export class Table extends React.Component { return rowRenderer(row, columns, "tr-" + row.__rowIndex); }); - if(this.props.bodyOverlay) + if(this.props.bodyOverlay) { body.push(this.props.bodyOverlay()); + } } return (