diff --git a/dist/challenge/index.html b/dist/challenge/index.html index a3b3fe7aff..a6c77b5938 100644 --- a/dist/challenge/index.html +++ b/dist/challenge/index.html @@ -15,7 +15,7 @@ src: url('../font/inter/bold.woff2') format('woff2'); } - html, body { height: 100%; } + html, body { height: 100%; -webkit-app-region: drag; } /*html.dark body { background-color: #171717; color: #a09f92; }*/ body { font-family: 'Inter', Helvetica, Arial, sans-serif; font-size: 14px; line-height: 22px; background-color: #fff; color: #252525; } diff --git a/dist/embed/iframe.html b/dist/embed/iframe.html index a8963150fd..d4ca48529c 100644 --- a/dist/embed/iframe.html +++ b/dist/embed/iframe.html @@ -82,6 +82,7 @@ let cachedHtml = ''; let height = 0; let blockId = ''; + let player; win.off('message resize'); @@ -139,6 +140,23 @@ insertHtml(html); }; + if (processor == Processor.Youtube) { + window.onYouTubeIframeAPIReady = () => { + player = new YT.Player('player', { + events: { + onReady, + onStateChange, + } + }); + }; + + const onReady = (event) => { + }; + + const onStateChange = (event) => { + }; + }; + loadLibs(libs, () => { if (!insertBeforeLoad) { insertHtml(html); @@ -258,6 +276,10 @@ libs.push('https://cpwebassets.codepen.io/assets/embed/ei.js'); break; }; + + case Processor.Youtube: + libs.push('https://www.youtube.com/iframe_api'); + break; }; return { diff --git a/dist/extension/auth/index.html b/dist/extension/auth/index.html new file mode 100644 index 0000000000..152adfb788 --- /dev/null +++ b/dist/extension/auth/index.html @@ -0,0 +1,10 @@ + + + + + Anytype Web Clipper + + + + + \ No newline at end of file diff --git a/dist/workers/graph.js b/dist/workers/graph.js index 87e6741c3b..94395585e3 100644 --- a/dist/workers/graph.js +++ b/dist/workers/graph.js @@ -317,7 +317,6 @@ updateSettings = (param) => { updateTheme = ({ theme, colors }) => { data.colors = colors; - initTheme(theme); redraw(); }; @@ -624,7 +623,9 @@ onClick = ({ x, y }) => { onSelect = ({ x, y, selectRelated }) => { const d = getNodeByCoords(x, y); - let related = []; + + let related = []; + if (d) { if (selectRelated) { related = edgeMap.get(d.id); @@ -638,6 +639,7 @@ onSetRootId = ({ x, y }) => { const d = getNodeByCoords(x, y); if (d) { this.setRootId({ rootId: d.id }); + send('setRootId', { node: d.id }); }; }; diff --git a/electron.js b/electron.js index 04ae6096bd..f4f897c5c4 100644 --- a/electron.js +++ b/electron.js @@ -9,6 +9,7 @@ const protocol = 'anytype'; const remote = require('@electron/remote/main'); const { installNativeMessagingHost } = require('./electron/js/lib/installNativeMessagingHost.js'); const binPath = fixPathForAsarUnpack(path.join(__dirname, 'dist', `anytypeHelper${is.windows ? '.exe' : ''}`)); +const Store = require('electron-store'); // Fix notifications app name if (is.windows) { @@ -16,6 +17,7 @@ if (is.windows) { }; storage.setDataPath(app.getPath('userData')); +Store.initRenderer(); const Api = require('./electron/js/api.js'); const ConfigManager = require('./electron/js/config.js'); diff --git a/electron/js/api.js b/electron/js/api.js index 308f5452e0..17d9d8d658 100644 --- a/electron/js/api.js +++ b/electron/js/api.js @@ -50,8 +50,14 @@ class Api { WindowManager.sendToAll('pin-check'); }; - setConfig (win, config) { - ConfigManager.set(config, () => Util.send(win, 'config', ConfigManager.config)); + setConfig (win, config, callBack) { + ConfigManager.set(config, () => { + Util.send(win, 'config', ConfigManager.config); + + if (callBack) { + callBack(); + }; + }); }; setAccount (win, account) { diff --git a/electron/js/menu.js b/electron/js/menu.js index 4492b86d45..5848a4e4ac 100644 --- a/electron/js/menu.js +++ b/electron/js/menu.js @@ -1,5 +1,6 @@ const { app, shell, Menu, Tray } = require('electron'); const { is } = require('electron-util'); +const fs = require('fs'); const path = require('path'); const ConfigManager = require('./config.js'); const Util = require('./util.js'); @@ -64,16 +65,40 @@ class MenuManager { Separator, { - label: Util.translate('electronMenuDirectory'), submenu: [ + label: Util.translate('electronMenuOpen'), submenu: [ { label: Util.translate('electronMenuWorkDirectory'), click: () => shell.openPath(Util.userPath()) }, { label: Util.translate('electronMenuDataDirectory'), click: () => shell.openPath(Util.dataPath()) }, { label: Util.translate('electronMenuConfigDirectory'), click: () => shell.openPath(Util.defaultUserDataPath()) }, { label: Util.translate('electronMenuLogsDirectory'), click: () => shell.openPath(Util.logPath()) }, + { + label: Util.translate('electronMenuCustomCss'), + click: () => { + const fp = path.join(Util.userPath(), 'custom.css'); + + if (!fs.existsSync(fp)) { + fs.writeFileSync(fp, ''); + }; + + shell.openPath(fp); + }, + }, ] }, Separator, + { + label: Util.translate('electronMenuApplyCustomCss'), type: 'checkbox', checked: !config.disableCss, + click: () => { + config.disableCss = !config.disableCss; + Api.setConfig(this.win, { disableCss: config.disableCss }, () => { + WindowManager.reloadAll(); + }); + }, + }, + + Separator, + { role: 'close', label: Util.translate('electronMenuClose') }, ] }, @@ -150,11 +175,11 @@ class MenuManager { submenu: [ { label: `${Util.translate('electronMenuReleaseNotes')} (${app.getVersion()})`, - click: () => Util.send(this.win, 'popup', 'help', { preventResize: true, data: { document: 'whatsNew' } }) + click: () => Util.send(this.win, 'popup', 'help', { data: { document: 'whatsNew' } }) }, { label: Util.translate('electronMenuShortcuts'), accelerator: 'Ctrl+Space', - click: () => Util.send(this.win, 'popup', 'shortcut', { preventResize: true }) + click: () => Util.send(this.win, 'popup', 'shortcut', {}) }, Separator, @@ -235,6 +260,7 @@ class MenuManager { { label: Util.translate('electronMenuDebugStat'), click: () => Util.send(this.win, 'commandGlobal', 'debugStat') }, { label: Util.translate('electronMenuDebugReconcile'), click: () => Util.send(this.win, 'commandGlobal', 'debugReconcile') }, { label: Util.translate('electronMenuDebugNet'), click: () => Util.send(this.win, 'commandGlobal', 'debugNet') }, + { label: Util.translate('electronMenuDebugLog'), click: () => Util.send(this.win, 'commandGlobal', 'debugLog') }, Separator, @@ -319,7 +345,7 @@ class MenuManager { this.tray = new Tray (this.getTrayIcon()); this.tray.setToolTip('Anytype'); this.tray.setContextMenu(Menu.buildFromTemplate([ - { label: Util.translate('electronMenuOpen'), click: () => this.winShow() }, + { label: Util.translate('electronMenuOpenApp'), click: () => this.winShow() }, Separator, diff --git a/electron/js/preload.js b/electron/js/preload.js index 55aaa97744..77f321592f 100644 --- a/electron/js/preload.js +++ b/electron/js/preload.js @@ -5,6 +5,9 @@ const os = require('os'); const path = require('path'); const mime = require('mime-types'); const tmpPath = () => app.getPath('temp'); +const Store = require('electron-store'); +const suffix = app.isPackaged ? '' : 'dev'; +const store = new Store({ name: [ 'localStorage', suffix ].join('-') }); contextBridge.exposeInMainWorld('Electron', { version: { @@ -16,6 +19,10 @@ contextBridge.exposeInMainWorld('Electron', { platform: os.platform(), arch: process.arch, + storeSet: (key, value) => store.set(key, value), + storeGet: key => store.get(key), + storeDelete: key => store.delete(key), + isPackaged: app.isPackaged, userPath: () => app.getPath('userData'), tmpPath, @@ -26,7 +33,11 @@ contextBridge.exposeInMainWorld('Electron', { fileMime: fp => mime.lookup(fp), fileExt: fp => path.extname(fp).replace(/^./, ''), fileSize: fp => fs.statSync(fp).size, - isDirectory: fp => fs.lstatSync(fp).isDirectory(), + isDirectory: fp => { + let ret = false; + try { ret = fs.lstatSync(fp).isDirectory(); } catch (e) {}; + return ret; + }, defaultPath: () => path.join(app.getPath('appData'), app.getName()), currentWindow: () => getCurrentWindow(), diff --git a/electron/js/server.js b/electron/js/server.js index f52eb77892..c58f291af9 100644 --- a/electron/js/server.js +++ b/electron/js/server.js @@ -7,6 +7,7 @@ const { app, dialog, shell } = require('electron'); const Util = require('./util.js'); let maxStdErrChunksBuffer = 10; +const winShutdownStdinMessage = 'shutdown\n'; class Server { @@ -114,7 +115,12 @@ class Server { }); this.stopTriggered = true; - this.cp.kill(signal); + if (process.platform === 'win32') { + // it is not possible to handle os signals on windows, so we can't do graceful shutdown on go side + this.cp.stdin.write(winShutdownStdinMessage); + } else { + this.cp.kill(signal); + }; } else { resolve(); }; @@ -131,4 +137,4 @@ class Server { }; -module.exports = new Server(); \ No newline at end of file +module.exports = new Server(); diff --git a/electron/js/window.js b/electron/js/window.js index 902ed90488..9b0f36aef7 100644 --- a/electron/js/window.js +++ b/electron/js/window.js @@ -1,4 +1,4 @@ -const { app, BrowserWindow, nativeImage, dialog, screen } = require('electron'); +const { app, BrowserWindow, nativeImage, dialog } = require('electron'); const { is, fixPathForAsarUnpack } = require('electron-util'); const path = require('path'); const windowStateKeeper = require('electron-window-state'); @@ -115,6 +115,7 @@ class WindowManager { height: state.height, }); } else { + const { screen } = require('electron'); const primaryDisplay = screen.getPrimaryDisplay(); const { width, height } = primaryDisplay.workAreaSize; @@ -141,10 +142,16 @@ class WindowManager { }; createChallenge (options) { + const { screen } = require('electron'); + const primaryDisplay = screen.getPrimaryDisplay(); + const { width } = primaryDisplay.workAreaSize; + const win = this.create({}, { backgroundColor: '', width: 424, height: 232, + x: Math.floor(width / 2 - 212), + y: 50, titleBarStyle: 'hidden', }); @@ -152,6 +159,7 @@ class WindowManager { win.setMenu(null); is.windows || is.linux ? win.showInactive() : win.show(); + win.focus(); win.webContents.once('did-finish-load', () => { win.webContents.postMessage('challenge', options); diff --git a/extension/auth.tsx b/extension/auth.tsx new file mode 100644 index 0000000000..7da76dc4ef --- /dev/null +++ b/extension/auth.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import * as hs from 'history'; +import { Router, Route, Switch } from 'react-router-dom'; +import { RouteComponentProps } from 'react-router'; +import { Provider } from 'mobx-react'; +import { configure } from 'mobx'; +import { S, U } from 'Lib'; + +import Index from './auth/index'; +import Success from './auth/success'; +import Util from './lib/util'; + +import './scss/auth.scss'; + +configure({ enforceActions: 'never' }); + +const Routes = [ + { path: '/' }, + { path: '/:page' }, +]; + +const Components = { + index: Index, + success: Success, +}; + +const memoryHistory = hs.createMemoryHistory; +const history = memoryHistory(); + +class RoutePage extends React.Component { + + render () { + const { match } = this.props; + const params = match.params as any; + const page = params.page || 'index'; + const Component = Components[page]; + + return Component ? : null; + }; + +}; + +class Auth extends React.Component { + + render () { + return ( + + +
+ + {Routes.map((item: any, i: number) => ( + + ))} + +
+
+
+ ); + }; + + componentDidMount () { + U.Router.init(history); + + /* @ts-ignore */ + chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + switch (msg.type) { + case 'initAuth': + const { appKey, gatewayPort, serverPort } = msg; + + Util.init(serverPort, gatewayPort); + Util.authorize(appKey); + + sendResponse({}); + break; + }; + return true; + }); + }; + +}; + +export default Auth; \ No newline at end of file diff --git a/extension/popup/challenge.tsx b/extension/auth/index.tsx similarity index 60% rename from extension/popup/challenge.tsx rename to extension/auth/index.tsx index 7ab9f087b7..8c6e98f87b 100644 --- a/extension/popup/challenge.tsx +++ b/extension/auth/index.tsx @@ -8,7 +8,7 @@ interface State { error: string; }; -const Challenge = observer(class Challenge extends React.Component { +const Index = observer(class Index extends React.Component { ref: any = null; state = { @@ -19,22 +19,20 @@ const Challenge = observer(class Challenge extends React.Component +
<Pin ref={ref => this.ref = ref} pinLength={4} - focusOnMount={true} onSuccess={this.onSuccess} - onError={this.onError} + onError={() => {}} /> <Error text={error} /> @@ -43,29 +41,35 @@ const Challenge = observer(class Challenge extends React.Component<I.PageCompone }; onSuccess () { - C.AccountLocalLinkSolveChallenge(S.Extension.challengeId, this.ref?.getValue().trim(), (message: any) => { + const urlParams = new URLSearchParams(window.location.search); + const data = JSON.parse(atob(urlParams.get('data') as string)); + + if (!data) { + this.setState({ error: 'Invalid data' }); + return; + }; + + Util.init(data.serverPort, data.gatewayPort); + + C.AccountLocalLinkSolveChallenge(data.challengeId, this.ref?.getValue().trim(), (message: any) => { if (message.error.code) { this.setState({ error: message.error.description }); return; }; const { appKey } = message; - const { serverPort, gatewayPort } = S.Extension; Storage.set('appKey', appKey); Util.authorize(appKey, () => { - Util.sendMessage({ type: 'initIframe', appKey, serverPort, gatewayPort }, () => {}); + Util.sendMessage({ type: 'initIframe', appKey, ...data }, () => {}); Util.sendMessage({ type: 'initMenu' }, () => {}); - U.Router.go('/create', {}); + U.Router.go('/success', {}); }); }); }; - onError () { - }; - }); -export default Challenge; \ No newline at end of file +export default Index; \ No newline at end of file diff --git a/extension/auth/success.tsx b/extension/auth/success.tsx new file mode 100644 index 0000000000..ab290a1776 --- /dev/null +++ b/extension/auth/success.tsx @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { observer } from 'mobx-react'; +import { I } from 'Lib'; + +interface State { + error: string; +}; + +const Success = observer(class Success extends React.Component<I.PageComponent, State> { + + render () { + return ( + <div className="page pageSuccess"> + <div className="title">Successfully authorized!</div> + </div> + ); + }; + +}); + +export default Success; \ No newline at end of file diff --git a/extension/entry.tsx b/extension/entry.tsx index 9a3c0854b5..924810b3b2 100644 --- a/extension/entry.tsx +++ b/extension/entry.tsx @@ -4,6 +4,7 @@ import $ from 'jquery'; import { C, U, J, S } from 'Lib'; import Popup from './popup'; import Iframe from './iframe'; +import Auth from './auth'; import Util from './lib/util'; import './scss/common.scss'; @@ -38,8 +39,8 @@ window.Anytype = { window.AnytypeGlobalConfig = { emojiUrl: J.Extension.clipper.emojiUrl, menuBorderTop: 16, - menuBorderBottom: 16, - flagsMw: { request: false }, + menuBorderBottom: 16, + flagsMw: { request: true }, }; let rootId = ''; @@ -52,6 +53,10 @@ if (Util.isPopup()) { if (Util.isIframe()) { rootId = `${J.Extension.clipper.prefix}-iframe`; component = <Iframe />; +} else +if (Util.isAuth()) { + rootId = `${J.Extension.clipper.prefix}-auth`; + component = <Auth />; }; if (!rootId) { diff --git a/extension/lib/util.ts b/extension/lib/util.ts index 56e7d7bfc0..5bf16778bd 100644 --- a/extension/lib/util.ts +++ b/extension/lib/util.ts @@ -1,7 +1,8 @@ import { S, U, J, C, dispatcher } from 'Lib'; const INDEX_POPUP = '/popup/index.html'; -const INDEX_IFRAME = '/iframe/index.html' +const INDEX_IFRAME = '/iframe/index.html'; +const INDEX_AUTH = '/auth/index.html'; class Util { @@ -20,6 +21,10 @@ class Util { return this.isExtension() && (location.pathname == INDEX_IFRAME); }; + isAuth () { + return this.isExtension() && (location.pathname == INDEX_AUTH); + }; + fromPopup (url: string) { return url.match(INDEX_POPUP); }; @@ -57,6 +62,13 @@ class Util { }; C.AccountSelect(message.accountId, '', 0, '', (message: any) => { + if (message.error.code) { + if (onError) { + onError(message.error); + }; + return; + }; + S.Auth.accountSet(message.account); S.Common.configSet(message.account.config, false); S.Common.showVaultSet(false); diff --git a/extension/popup.tsx b/extension/popup.tsx index 8e78a0767a..c39dd72df0 100644 --- a/extension/popup.tsx +++ b/extension/popup.tsx @@ -8,7 +8,6 @@ import { ListMenu } from 'Component'; import { S, U, C, J } from 'Lib'; import Index from './popup/index'; -import Challenge from './popup/challenge'; import Create from './popup/create'; import Success from './popup/success'; @@ -23,7 +22,6 @@ const Routes = [ const Components = { index: Index, - challenge: Challenge, create: Create, success: Success, }; diff --git a/extension/popup/index.tsx b/extension/popup/index.tsx index 56b260fa30..cf2930f017 100644 --- a/extension/popup/index.tsx +++ b/extension/popup/index.tsx @@ -81,6 +81,7 @@ const Index = observer(class Index extends React.Component<I.PageComponent, Stat } else { /* @ts-ignore */ const manifest = chrome.runtime.getManifest(); + const { serverPort, gatewayPort } = S.Extension; C.AccountLocalLinkNewChallenge(manifest.name, (message: any) => { if (message.error.code) { @@ -88,8 +89,14 @@ const Index = observer(class Index extends React.Component<I.PageComponent, Stat return; }; - S.Extension.challengeId = message.challengeId; - U.Router.go('/challenge', {}); + const data = { + serverPort, + gatewayPort, + challengeId: message.challengeId, + }; + + /* @ts-ignore */ + chrome.tabs.create({ url: chrome.runtime.getURL('auth/index.html') + `?data=${btoa(JSON.stringify(data))}` }); }); }; }; diff --git a/extension/scss/auth.scss b/extension/scss/auth.scss new file mode 100644 index 0000000000..f90fcf859f --- /dev/null +++ b/extension/scss/auth.scss @@ -0,0 +1,31 @@ +html.anytypeWebclipper-auth { width: 640px; position: absolute; left: 50%; top: 50%; transform: translate3d(-50%, -50%, 0px); } + +#anytypeWebclipper-auth { + @import "~scss/_mixins"; + + .input, .select, .textarea { @include text-small; border: 1px solid var(--color-shape-secondary); width: 100%; border-radius: 1px; } + + .pin { + .input { width: 32px; height: 40px; @include text-paragraph; border-radius: 6px; } + } + + .input, .select { height: 32px; padding: 0px 10px; } + .select { display: flex; align-items: center; padding-right: 20px; } + .textarea { padding: 10px; resize: none; height: 68px; display: block; } + .buttonsWrapper { display: flex; flex-direction: column; justify-content: center; gap: 8px 0px; margin: 16px 0px 0px 0px; } + .loaderWrapper { position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-bg-loader); z-index: 10; } + .error { @include text-small; margin: 1em 0px 0px 0px; } + + .isFocused { border-color: var(--color-ice) !important; box-shadow: 0px 0px 0px 1px var(--color-ice); } + + .page.pageIndex, + .page.pageSuccess { display: flex; flex-direction: column; align-items: center; } + + .page.pageIndex { + .title { @include text-header3; margin: 0px 0px 24px 0px; } + } + + .page.pageSuccess { + .title { @include text-header3; } + } +} \ No newline at end of file diff --git a/extension/scss/common.scss b/extension/scss/common.scss index 6dcb09585e..568f918620 100644 --- a/extension/scss/common.scss +++ b/extension/scss/common.scss @@ -1,5 +1,6 @@ html.anytypeWebclipper-iframe, -html.anytypeWebclipper-popup { +html.anytypeWebclipper-popup, +html.anytypeWebclipper-auth { @import "~scss/font"; @import "~scss/_mixins"; diff --git a/extension/scss/popup.scss b/extension/scss/popup.scss index 3b9b557129..1ae1f10b92 100644 --- a/extension/scss/popup.scss +++ b/extension/scss/popup.scss @@ -33,11 +33,7 @@ html.anytypeWebclipper-popup { width: 268px; } .isFocused { border-color: var(--color-ice) !important; box-shadow: 0px 0px 0px 1px var(--color-ice); } - .page.pageIndex, .page.pageChallenge { padding: 50px 16px; text-align: center; } - - .page.pageChallenge { - .title { @include text-common; font-weight: 600; margin: 0px 0px 24px 0px; } - } + .page.pageIndex { padding: 50px 16px; text-align: center; } .page.pageCreate { padding: 16px; } .page.pageCreate { diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000..e231028dfc --- /dev/null +++ b/flake.lock @@ -0,0 +1,59 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1725103162, + "narHash": "sha256-Ym04C5+qovuQDYL/rKWSR+WESseQBbNAe5DsXNx5trY=", + "rev": "12228ff1752d7b7624a54e9c1af4b222b3c1073b", + "revCount": 674318, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/NixOS/nixpkgs/0.1.674318%2Brev-12228ff1752d7b7624a54e9c1af4b222b3c1073b/0191adaa-df39-7d38-92e0-798658d0033f/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000..dcc9f05fdd --- /dev/null +++ b/flake.nix @@ -0,0 +1,77 @@ +# nix-ld should be enabled in configuration.nix: +# programs.nix-ld.enable = true; +# programs.nix-ld.libraries = with pkgs; [ +# gtk3 +# # Add any missing dynamic libraries for unpackaged programs +# # here, NOT in environment.systemPackages +# ]; + +{ + description = ""; + inputs.nixpkgs.url = "https://flakehub.com/f/NixOS/nixpkgs/0.1.0.tar.gz"; + inputs.flake-utils.url = "github:numtide/flake-utils"; + + outputs = { self, nixpkgs, flake-utils, ... }: + flake-utils.lib.eachDefaultSystem (system: let + pkgs = import nixpkgs { + inherit system; + config = { allowUnfree = true; }; + }; + deps = [ + pkgs.appimage-run + # commit hook + pkgs.husky + # build deps + pkgs.libxcrypt + pkgs.libsecret + pkgs.pkg-config + pkgs.jq + pkgs.nodejs_22 + + # keytar build fails on npm install because python312 has distutils removed + pkgs.python311 + + # electron binary launch deps. + # see also https://nix.dev/guides/faq#how-to-run-non-nix-executables + pkgs.glib + pkgs.nss + pkgs.nspr + pkgs.dbus + pkgs.atk + pkgs.cups + pkgs.libdrm + pkgs.gtk3 + pkgs.adwaita-icon-theme + pkgs.pango + pkgs.cairo + pkgs.xorg.libX11 + pkgs.xorg.libX11 + pkgs.xorg.libXcomposite + pkgs.xorg.libXdamage + pkgs.xorg.libXext + pkgs.xorg.libXfixes + pkgs.xorg.libXrandr + pkgs.mesa + pkgs.expat + pkgs.libxkbcommon + pkgs.xorg.libxcb + pkgs.alsa-lib + pkgs.libGL + pkgs.gdk-pixbuf + ]; + XDG_ICONS_PATH = "${pkgs.hicolor-icon-theme}/share:${pkgs.adwaita-icon-theme}/share"; + in { + devShell = pkgs.mkShell { + name = "anytype-ts-dev"; + SERVER_PORT = 9090; + ANY_SYNC_NETWORK = "/home/zarkone/anytype/local-network-config.yml"; + LD_LIBRARY_PATH = "${pkgs.lib.strings.makeLibraryPath deps}"; + nativeBuildInputs = deps; + shellHook = '' + # fixes "No GSettings schemas" error + export XDG_DATA_DIRS=$GSETTINGS_SCHEMAS_PATH:$XDG_ICONS_PATH:$XDG_DATA_DIRS + ''; + }; + + }); +} diff --git a/middleware.version b/middleware.version index a3c7b05314..09fc9f9ddb 100644 --- a/middleware.version +++ b/middleware.version @@ -1 +1 @@ -0.37.3 \ No newline at end of file +0.38.7 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 0fd597d588..4430f7296d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "anytype", - "version": "0.43.7", + "version": "0.44.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "anytype", - "version": "0.43.7", + "version": "0.44.0", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { @@ -22,11 +22,13 @@ "d3": "^7.0.1", "d3-force": "^3.0.0", "d3-force-cluster": "^0.1.2", + "date-fns": "^4.1.0", "diff": "^5.2.0", "dompurify": "3.1.6", "electron-dl": "^1.14.0", "electron-json-storage": "^4.5.0", "electron-log": "^5.2.0", + "electron-store": "^8.2.0", "electron-updater": "^6.2.1", "electron-util": "^0.12.3", "electron-window-state": "^5.0.3", @@ -44,7 +46,7 @@ "lazy-val": "^1.0.4", "lodash": "^4.17.20", "lodash.isequal": "^4.5.0", - "mermaid": "^11.4.0", + "mermaid": "^11.4.1", "mime-types": "^2.1.35", "mobx": "^6.6.1", "mobx-logger": "^0.7.1", @@ -92,7 +94,7 @@ "@typescript-eslint/parser": "^6.18.1", "cross-env": "^7.0.2", "css-loader": "^3.6.0", - "electron": "^31.0.2", + "electron": "^33.2.1", "electron-builder": "^24.13.3", "eslint": "^8.29.0", "eslint-plugin-react": "^7.31.11", @@ -1090,24 +1092,22 @@ "dev": true }, "node_modules/@rsdoctor/client": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/client/-/client-0.4.8.tgz", - "integrity": "sha512-Jj+37B+WFAgsszwE4l65ySM8USW8Z/VqIE5dUMq+os/johgTRF7wafnlABIPdpd2ih8HTbankzEJOMHyJYHTog==", - "dev": true, - "license": "MIT" + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/client/-/client-0.4.12.tgz", + "integrity": "sha512-oqwqOR4KHUDmfHobDLSsLRvBkZEyqSbO0sb2TDkOA6O/XPTafgifuNczKAdSYBcoqwKZuHFNXZpr0p7SEUw6GA==", + "dev": true }, "node_modules/@rsdoctor/core": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/core/-/core-0.4.8.tgz", - "integrity": "sha512-I2Hl9D5K/yGQCJaUoyLGFAaMF1vpIXW5s2zeprEbfW1EFN8u59LzvatEg4+wgcfaXPMecAB5dfm6J+D36F4aRA==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/core/-/core-0.4.12.tgz", + "integrity": "sha512-l3K5pjpnc17DgqRY2CvL6lVYyPGcnjXky6taXRcUCH64HfX01eEUH6xyra9TGFR2L5rYx5OTzqX/rztE/VxJZg==", "dev": true, - "license": "MIT", "dependencies": { - "@rsdoctor/graph": "0.4.8", - "@rsdoctor/sdk": "0.4.8", - "@rsdoctor/types": "0.4.8", - "@rsdoctor/utils": "0.4.8", - "axios": "^1.7.7", + "@rsdoctor/graph": "0.4.12", + "@rsdoctor/sdk": "0.4.12", + "@rsdoctor/types": "0.4.12", + "@rsdoctor/utils": "0.4.12", + "axios": "^1.7.9", "enhanced-resolve": "5.12.0", "filesize": "^10.1.6", "fs-extra": "^11.1.1", @@ -1123,7 +1123,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1138,7 +1137,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -1151,22 +1149,20 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/@rsdoctor/graph": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/graph/-/graph-0.4.8.tgz", - "integrity": "sha512-F6lKNjU1pewVXT8PLPgL7WuxB4Aru7unpZjJ4Hh1vbcNUsK6VNUNHsAviOu2ZrbuKuwbxqjd3p/NIjThUzX7NQ==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/graph/-/graph-0.4.12.tgz", + "integrity": "sha512-xf66kkOF44wi/SWomr8qgJ5jEbX6DFxdTP+9FQNTz/LRRqYebySkU9uvVTLHmjSxDVBQ1dGx+YsZLhFpF6neWA==", "dev": true, - "license": "MIT", "dependencies": { - "@rsdoctor/types": "0.4.8", - "@rsdoctor/utils": "0.4.8", + "@rsdoctor/types": "0.4.12", + "@rsdoctor/utils": "0.4.12", "lodash": "^4.17.21", - "socket.io": "4.7.2", + "socket.io": "4.8.1", "source-map": "^0.7.4" } }, @@ -1175,23 +1171,21 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/@rsdoctor/rspack-plugin": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/rspack-plugin/-/rspack-plugin-0.4.8.tgz", - "integrity": "sha512-IIyZ/EhiU7b3n0m+1frPTBKXnmjpB6rKbumjntzTDkmY7+OEub69WA9eqSxx0nPWQxBHwBzCpIVjSjYyARRaNg==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/rspack-plugin/-/rspack-plugin-0.4.12.tgz", + "integrity": "sha512-jzU+8sGzXe8TP4jy3+rtsPOTj71J1JDKCeiXqbJFelAWl05Bhfj6nw+ED/23SDb/430EEVW6nSdgEm/iDCHJIA==", "dev": true, - "license": "MIT", "dependencies": { - "@rsdoctor/core": "0.4.8", - "@rsdoctor/graph": "0.4.8", - "@rsdoctor/sdk": "0.4.8", - "@rsdoctor/types": "0.4.8", - "@rsdoctor/utils": "0.4.8", + "@rsdoctor/core": "0.4.12", + "@rsdoctor/graph": "0.4.12", + "@rsdoctor/sdk": "0.4.12", + "@rsdoctor/types": "0.4.12", + "@rsdoctor/utils": "0.4.12", "lodash": "^4.17.21" }, "peerDependencies": { @@ -1199,25 +1193,25 @@ } }, "node_modules/@rsdoctor/sdk": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/sdk/-/sdk-0.4.8.tgz", - "integrity": "sha512-tD14drEa9ZZm5FtKL6BfDsBbOvYbIRbF5aEpQSP5ZQsKa6YfOhHA2dpv+zN7qIUXzd16vKBGNzTT906BT7HvEw==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/sdk/-/sdk-0.4.12.tgz", + "integrity": "sha512-b2ob92cYle2kXFWkxrdVen6/9vNcn8UJW8kFmD4OCfR4AgHiFLZMSWzJ5P41M1Ti1AqY3kVlp2CaEZOIof2SBw==", "dev": true, - "license": "MIT", "dependencies": { - "@rsdoctor/client": "0.4.8", - "@rsdoctor/graph": "0.4.8", - "@rsdoctor/types": "0.4.8", - "@rsdoctor/utils": "0.4.8", + "@rsdoctor/client": "0.4.12", + "@rsdoctor/graph": "0.4.12", + "@rsdoctor/types": "0.4.12", + "@rsdoctor/utils": "0.4.12", "@types/fs-extra": "^11.0.4", "body-parser": "1.20.3", "cors": "2.8.5", "dayjs": "1.11.13", "fs-extra": "^11.1.1", + "json-cycle": "^1.5.0", "lodash": "^4.17.21", "open": "^8.4.2", "serve-static": "1.16.2", - "socket.io": "4.7.2", + "socket.io": "4.8.1", "source-map": "^0.7.4", "tapable": "2.2.1" } @@ -1227,7 +1221,6 @@ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/jsonfile": "*", "@types/node": "*" @@ -1238,7 +1231,6 @@ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" } @@ -1248,7 +1240,6 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1263,7 +1254,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -1276,7 +1266,6 @@ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, - "license": "MIT", "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", @@ -1294,17 +1283,15 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/@rsdoctor/types": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/types/-/types-0.4.8.tgz", - "integrity": "sha512-yY9OyfRLuOva2GhLccjHy1jDVTfoxeGSXF030DingiorrhFXMi+TJjq98LN4p9ha/awxzHayAUcSwZ3nq1XEXQ==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/types/-/types-0.4.12.tgz", + "integrity": "sha512-FqdRPbOllpG6dxSU5f6Pe5pLJGLMuw5yEnwBACb818uIFI+099xwZxkmxBqDg13ZEFrfMNQsruZv3HBQ9hYHPQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/connect": "3.4.38", "@types/estree": "1.0.5", @@ -1325,28 +1312,25 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@rsdoctor/types/node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 8" } }, "node_modules/@rsdoctor/utils": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@rsdoctor/utils/-/utils-0.4.8.tgz", - "integrity": "sha512-5m0dvwkdKAT5mONAf49oG1JEr+FXSThxwo36tfIHrUPNm3pnx8bDXFL3E5rNb9LK8Va7GFYe6hjmxTxyoaveFg==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/@rsdoctor/utils/-/utils-0.4.12.tgz", + "integrity": "sha512-GQahkLhH65nfRvLK1auydLvbVFsLKcDZIwEf1VoL1lpWhtxSEzVJCuex8764Fs8abBlFM1Of22IOpMMQ2Pvdbw==", "dev": true, - "license": "MIT", "dependencies": { "@babel/code-frame": "7.25.7", - "@rsdoctor/types": "0.4.8", + "@rsdoctor/types": "0.4.12", "@types/estree": "1.0.5", "acorn": "^8.10.0", "acorn-import-assertions": "1.9.0", @@ -1370,7 +1354,6 @@ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", "dev": true, - "license": "MIT", "dependencies": { "@babel/highlight": "^7.25.7", "picocolors": "^1.0.0" @@ -1383,15 +1366,13 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@rsdoctor/utils/node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -1406,7 +1387,6 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, - "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -1419,7 +1399,6 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-2.0.4.tgz", "integrity": "sha512-wM1+Z03eypVAVUCE7QdSqpVIvelbOakn1M0bPDoA4SGWPx3sNDVUiMo3L6To6WWGClB7VyXnhQ4Sn7gxiJbE6A==", "dev": true, - "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -2092,8 +2071,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@szmarczak/http-timer": { "version": "4.0.6", @@ -2172,15 +2150,13 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -2358,9 +2334,9 @@ } }, "node_modules/@types/d3-scale-chromatic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz", - "integrity": "sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" }, "node_modules/@types/d3-selection": { "version": "3.0.11", @@ -2416,15 +2392,6 @@ "@types/ms": "*" } }, - "node_modules/@types/dompurify": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz", - "integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==", - "deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.", - "dependencies": { - "dompurify": "*" - } - }, "node_modules/@types/emoji-mart": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/@types/emoji-mart/-/emoji-mart-3.0.14.tgz", @@ -2546,7 +2513,6 @@ "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz", "integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==", "dev": true, - "license": "MIT", "dependencies": { "@types/node": "*" } @@ -2781,11 +2747,16 @@ "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-2.2.7.tgz", "integrity": "sha512-D6QzACV9vNX3r8HQQNTOnpG+Bv1rko+yEA82wKs3O9CQ5+XW7HI7TED17/UE7+5dIxyxZIWTxKbsBeF6uKFCwA==", "dev": true, - "license": "MIT", "dependencies": { "tapable": "^2.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, "node_modules/@types/verror": { "version": "1.10.10", "resolved": "https://registry.npmjs.org/@types/verror/-/verror-1.10.10.tgz", @@ -3252,7 +3223,6 @@ "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "deprecated": "package has been renamed to acorn-import-attributes", "dev": true, - "license": "MIT", "peerDependencies": { "acorn": "^8" } @@ -3342,7 +3312,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, "dependencies": { "ajv": "^8.0.0" }, @@ -3359,7 +3328,6 @@ "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -3374,8 +3342,7 @@ "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/ajv-keywords": { "version": "3.5.2", @@ -3921,6 +3888,14 @@ "node": ">= 4.0.0" } }, + "node_modules/atomically": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-1.7.0.tgz", + "integrity": "sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w==", + "engines": { + "node": ">=10.12.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -3937,11 +3912,10 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dev": true, - "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -3977,7 +3951,6 @@ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", "dev": true, - "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } @@ -4951,6 +4924,71 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/conf": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/conf/-/conf-10.2.0.tgz", + "integrity": "sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg==", + "dependencies": { + "ajv": "^8.6.3", + "ajv-formats": "^2.1.1", + "atomically": "^1.7.0", + "debounce-fn": "^4.0.0", + "dot-prop": "^6.0.1", + "env-paths": "^2.2.1", + "json-schema-typed": "^7.0.3", + "onetime": "^5.1.2", + "pkg-up": "^3.1.0", + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/conf/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/conf/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/confbox": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", @@ -5015,7 +5053,6 @@ "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", @@ -5040,7 +5077,6 @@ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "license": "MIT", "dependencies": { "ms": "2.0.0" } @@ -5050,7 +5086,6 @@ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.8" } @@ -5060,7 +5095,6 @@ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", "dev": true, - "license": "MIT", "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", @@ -5078,15 +5112,13 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/connect/node_modules/on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", "dev": true, - "license": "MIT", "dependencies": { "ee-first": "1.1.1" }, @@ -5099,7 +5131,6 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -5155,7 +5186,6 @@ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dev": true, - "license": "MIT", "dependencies": { "object-assign": "^4", "vary": "^1" @@ -5890,6 +5920,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", @@ -5899,8 +5939,29 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "dev": true, - "license": "MIT" + "dev": true + }, + "node_modules/debounce-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-4.0.0.tgz", + "integrity": "sha512-8pYCQiL9Xdcg0UPSD3d+0KMlOjp+KGU5EPwYddgzQ7DATsg4fuUDjQtsYLmWjnk2obnNHgV3vE2Y4jejSOJVBQ==", + "dependencies": { + "mimic-fn": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debounce-fn/node_modules/mimic-fn": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-3.1.0.tgz", + "integrity": "sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==", + "engines": { + "node": ">=8" + } }, "node_modules/debug": { "version": "4.3.7", @@ -6016,7 +6077,6 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", "dev": true, - "license": "MIT", "dependencies": { "type-detect": "^4.0.0" }, @@ -6485,6 +6545,20 @@ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" }, + "node_modules/dot-prop": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", + "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-9.0.2.tgz", @@ -6539,11 +6613,10 @@ } }, "node_modules/electron": { - "version": "31.7.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-31.7.5.tgz", - "integrity": "sha512-8zFzVJdhxTRmoPcRiKkEmPW0bJHAUsTQJwEX2YJ8X0BVFIJLwSvHkSlpCjEExVbNCAk+gHnkIYX+2OyCXrRwHQ==", + "version": "33.2.1", + "resolved": "https://registry.npmjs.org/electron/-/electron-33.2.1.tgz", + "integrity": "sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==", "hasInstallScript": true, - "license": "MIT", "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^20.9.0", @@ -6734,6 +6807,29 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/electron-store": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-8.2.0.tgz", + "integrity": "sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw==", + "dependencies": { + "conf": "^10.2.0", + "type-fest": "^2.17.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-store/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.43", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.43.tgz", @@ -6867,18 +6963,17 @@ } }, "node_modules/engine.io": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", - "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", "dev": true, - "license": "MIT", "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", - "cookie": "~0.4.1", + "cookie": "~0.7.2", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", @@ -6893,17 +6988,15 @@ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", - "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", "dev": true, - "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6913,7 +7006,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -6935,7 +7027,6 @@ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", "dev": true, - "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -6957,7 +7048,6 @@ "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz", "integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==", "dev": true, - "license": "MIT", "bin": { "envinfo": "dist/cli.js" }, @@ -7531,9 +7621,9 @@ "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==" }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dev": true, "dependencies": { "accepts": "~1.3.8", @@ -7555,7 +7645,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -7570,6 +7660,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express/node_modules/debug": { @@ -7588,9 +7682,9 @@ "dev": true }, "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "dev": true }, "node_modules/ext-list": { @@ -7648,8 +7742,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.3.2", @@ -7694,8 +7787,7 @@ "node_modules/fast-uri": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz", - "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==", - "dev": true + "integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==" }, "node_modules/fastq": { "version": "1.17.1", @@ -7764,7 +7856,6 @@ "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz", "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">= 10.4.0" } @@ -8239,7 +8330,6 @@ "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=8" }, @@ -8698,8 +8788,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/http-cache-semantics": { "version": "4.1.1", @@ -9393,6 +9482,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -9745,6 +9842,15 @@ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, + "node_modules/json-cycle": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/json-cycle/-/json-cycle-1.5.0.tgz", + "integrity": "sha512-GOehvd5PO2FeZ5T4c+RxobeT5a1PiGpF4u9/3+UvrMU4bhnVqzJY7hm39wg8PDCqkU91fWGH8qjWR4bn+wgq9w==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -9763,6 +9869,11 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-schema-typed": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-7.0.3.tgz", + "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -9773,8 +9884,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-stream-stringify/-/json-stream-stringify-3.0.1.tgz", "integrity": "sha512-vuxs3G1ocFDiAQ/SX0okcZbtqXwgj1g71qE9+vrjJ2EkjKQlEFDAcUNRxRU8O+GekV4v5cM2qXP0Wyt/EMDBiQ==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/json-stringify-safe": { "version": "5.0.1", @@ -10824,15 +10934,14 @@ } }, "node_modules/mermaid": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.0.tgz", - "integrity": "sha512-mxCfEYvADJqOiHfGpJXLs4/fAjHz448rH0pfY5fAoxiz70rQiDSzUUy4dNET2T08i46IVpjohPd6WWbzmRHiPA==", + "version": "11.4.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.4.1.tgz", + "integrity": "sha512-Mb01JT/x6CKDWaxigwfZYuYmDZ6xtrNwNlidKZwkSrDaY9n90tdrJTV5Umk+wP1fZscGptmKFXHsXMDEVZ+Q6A==", "dependencies": { "@braintree/sanitize-url": "^7.0.1", "@iconify/utils": "^2.1.32", "@mermaid-js/parser": "^0.3.0", "@types/d3": "^7.4.3", - "@types/dompurify": "^3.0.5", "cytoscape": "^3.29.2", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", @@ -10840,7 +10949,7 @@ "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.11", "dayjs": "^1.11.10", - "dompurify": "^3.0.11 <3.1.7", + "dompurify": "^3.2.1", "katex": "^0.16.9", "khroma": "^2.1.0", "lodash-es": "^4.17.21", @@ -10851,6 +10960,14 @@ "uuid": "^9.0.1" } }, + "node_modules/mermaid/node_modules/dompurify": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", + "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", @@ -11171,7 +11288,6 @@ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", "dev": true, - "license": "MIT", "engines": { "node": ">=10" } @@ -15155,6 +15271,14 @@ "node": ">= 4" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -15349,8 +15473,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "dev": true, - "license": "MIT" + "dev": true }, "node_modules/path-data-parser": { "version": "0.1.0", @@ -15519,6 +15642,73 @@ "pathe": "^1.1.2" } }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-up/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-up/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-up/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, "node_modules/plist": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", @@ -16467,7 +16657,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -17323,7 +17512,6 @@ "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, - "license": "MIT", "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", @@ -17376,17 +17564,16 @@ } }, "node_modules/socket.io": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", - "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", "dev": true, - "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", "cors": "~2.8.5", "debug": "~4.3.2", - "engine.io": "~6.5.2", + "engine.io": "~6.6.0", "socket.io-adapter": "~2.5.2", "socket.io-parser": "~4.2.4" }, @@ -17399,7 +17586,6 @@ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", "dev": true, - "license": "MIT", "dependencies": { "debug": "~4.3.4", "ws": "~8.17.1" @@ -17410,7 +17596,6 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=10.0.0" }, @@ -17432,7 +17617,6 @@ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", "dev": true, - "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", "debug": "~4.3.1" @@ -18557,7 +18741,6 @@ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, - "license": "MIT", "engines": { "node": ">=6" } @@ -18669,7 +18852,6 @@ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", "dev": true, - "license": "MIT", "engines": { "node": ">=4" } @@ -19155,7 +19337,6 @@ "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", "dev": true, - "license": "MIT", "dependencies": { "@discoveryjs/json-ext": "0.5.7", "acorn": "^8.0.4", diff --git a/package.json b/package.json index a9f9a98755..51027d3bba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anytype", - "version": "0.43.7", + "version": "0.44.0", "description": "Anytype", "main": "electron.js", "scripts": { @@ -68,7 +68,7 @@ "@typescript-eslint/parser": "^6.18.1", "cross-env": "^7.0.2", "css-loader": "^3.6.0", - "electron": "^31.0.2", + "electron": "^33.2.1", "electron-builder": "^24.13.3", "eslint": "^8.29.0", "eslint-plugin-react": "^7.31.11", @@ -102,11 +102,13 @@ "d3": "^7.0.1", "d3-force": "^3.0.0", "d3-force-cluster": "^0.1.2", + "date-fns": "^4.1.0", "diff": "^5.2.0", "dompurify": "3.1.6", "electron-dl": "^1.14.0", "electron-json-storage": "^4.5.0", "electron-log": "^5.2.0", + "electron-store": "^8.2.0", "electron-updater": "^6.2.1", "electron-util": "^0.12.3", "electron-window-state": "^5.0.3", @@ -124,7 +126,7 @@ "lazy-val": "^1.0.4", "lodash": "^4.17.20", "lodash.isequal": "^4.5.0", - "mermaid": "^11.4.0", + "mermaid": "^11.4.1", "mime-types": "^2.1.35", "mobx": "^6.6.1", "mobx-logger": "^0.7.1", diff --git a/src/img/icon/chat/buttons/clear.svg b/src/img/icon/chat/buttons/clear.svg index c58fc9f811..6cb0cc2d7f 100644 --- a/src/img/icon/chat/buttons/clear.svg +++ b/src/img/icon/chat/buttons/clear.svg @@ -1,4 +1,3 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M19 9.99023C19 14.9608 14.9706 18.9902 10 18.9902C5.02944 18.9902 1 14.9608 1 9.99023C1 5.01967 5.02944 0.990234 10 0.990234C14.9706 0.990234 19 5.01967 19 9.99023Z" fill="#B6B6B6"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M7.54725 6.44753C7.25507 6.15392 6.7802 6.15276 6.48659 6.44494C6.19298 6.73712 6.19182 7.21199 6.484 7.5056L8.94973 9.9834L6.484 12.4612C6.19182 12.7548 6.19298 13.2297 6.48659 13.5219C6.7802 13.814 7.25507 13.8129 7.54725 13.5193L10.0078 11.0467L12.4684 13.5193C12.7606 13.8129 13.2354 13.814 13.529 13.5219C13.8226 13.2297 13.8238 12.7548 13.5316 12.4612L11.0659 9.9834L13.5316 7.5056C13.8238 7.21199 13.8226 6.73712 13.529 6.44494C13.2354 6.15276 12.7606 6.15392 12.4684 6.44753L10.0078 8.92014L7.54725 6.44753Z" fill="white"/> </svg> diff --git a/src/img/icon/chat/buttons/emoji.svg b/src/img/icon/chat/buttons/emoji.svg index 5ac953e852..9bc303e108 100644 --- a/src/img/icon/chat/buttons/emoji.svg +++ b/src/img/icon/chat/buttons/emoji.svg @@ -1,4 +1,4 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 10C3.5 13.5899 6.41015 16.5 10 16.5C13.5899 16.5 16.5 13.5899 16.5 10C16.5 6.41015 13.5899 3.5 10 3.5C6.41015 3.5 3.5 6.41015 3.5 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM7 8C7 7.44772 7.44772 7 8 7C8.55228 7 9 7.44772 9 8C9 8.55228 8.55228 9 8 9C7.44772 9 7 8.55228 7 8ZM12 7C11.4477 7 11 7.44772 11 8C11 8.55228 11.4477 9 12 9C12.5523 9 13 8.55228 13 8C13 7.44772 12.5523 7 12 7Z" fill="#B6B6B6"/> -<path d="M13.9511 11.2734C14.0838 11.2413 14.209 11.2856 14.277 11.3487C14.3079 11.3773 14.315 11.3985 14.3162 11.4033C14.3168 11.4058 14.3172 11.4082 14.3169 11.4123C14.3166 11.4164 14.3151 11.428 14.3068 11.448C13.4567 13.4823 11.7974 14.75 9.99981 14.75C8.21001 14.75 6.55764 13.4936 5.70405 11.4746C5.69549 11.4544 5.69407 11.4428 5.69377 11.4389C5.69346 11.435 5.69378 11.4328 5.69439 11.4304C5.69561 11.4256 5.70295 11.4038 5.73506 11.3741C5.80602 11.3087 5.93588 11.2636 6.0714 11.2967C7.07906 11.5427 8.42465 11.7587 10.0046 11.7388C11.5931 11.7384 12.9439 11.5172 13.9511 11.2734Z" stroke="#B6B6B6" stroke-width="1.5"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 10C3.5 13.5899 6.41015 16.5 10 16.5C13.5899 16.5 16.5 13.5899 16.5 10C16.5 6.41015 13.5899 3.5 10 3.5C6.41015 3.5 3.5 6.41015 3.5 10ZM10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 5.58172 14.4183 2 10 2ZM7 8C7 7.17157 7.44772 6.5 8 6.5C8.55228 6.5 9 7.17157 9 8C9 8.82843 8.55228 9.5 8 9.5C7.44772 9.5 7 8.82843 7 8ZM12 6.5C11.4477 6.5 11 7.17157 11 8C11 8.82843 11.4477 9.5 12 9.5C12.5523 9.5 13 8.82843 13 8C13 7.17157 12.5523 6.5 12 6.5Z" fill="#B6B6B6"/> +<path d="M13.9549 11.7218C14.2966 11.0129 13.5584 10.4096 12.7997 10.6185C12.0508 10.8246 11.0982 11 9.99984 11C8.91327 11.014 7.9693 10.8452 7.2241 10.6404C6.45931 10.4302 5.71042 11.0374 6.05856 11.7501C6.86359 13.398 8.32748 14.5 9.99984 14.5C11.6818 14.5 13.1528 13.3854 13.9549 11.7218Z" fill="#B6B6B6"/> </svg> \ No newline at end of file diff --git a/src/img/icon/chat/buttons/reaction0.svg b/src/img/icon/chat/buttons/reaction0.svg index 366ea79d50..bdfab28308 100644 --- a/src/img/icon/chat/buttons/reaction0.svg +++ b/src/img/icon/chat/buttons/reaction0.svg @@ -1,6 +1,6 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M11.5821 2.15642C11.0707 2.05383 10.5416 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 9.69754 17.9832 9.399 17.9505 9.10525C17.4912 9.30004 16.9967 9.42811 16.4793 9.47733C16.493 9.64974 16.5 9.82405 16.5 10C16.5 13.5899 13.5899 16.5 10 16.5C6.41015 16.5 3.5 13.5899 3.5 10C3.5 6.41015 6.41015 3.5 10 3.5C10.3689 3.5 10.7306 3.53073 11.0827 3.58976C11.1763 3.08082 11.3469 2.59886 11.5821 2.15642Z" fill="#B6B6B6"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6.5C7.44772 6.5 7 6.94772 7 7.5C7 8.05228 7.44772 8.5 8 8.5C8.55228 8.5 9 8.05228 9 7.5C9 6.94772 8.55228 6.5 8 6.5ZM12 6.5C11.4477 6.5 11 6.94772 11 7.5C11 8.05228 11.4477 8.5 12 8.5C12.5523 8.5 13 8.05228 13 7.5C13 6.94772 12.5523 6.5 12 6.5Z" fill="#B6B6B6"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6C7.44772 6 7 6.67157 7 7.5C7 8.32843 7.44772 9 8 9C8.55228 9 9 8.32843 9 7.5C9 6.67157 8.55228 6 8 6ZM12 6C11.4477 6 11 6.67157 11 7.5C11 8.32843 11.4477 9 12 9C12.5523 9 13 8.32843 13 7.5C13 6.67157 12.5523 6 12 6Z" fill="#B6B6B6"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 1C15.5858 1 15.25 1.33579 15.25 1.75V3.25H13.75C13.3358 3.25 13 3.58579 13 4C13 4.41421 13.3358 4.75 13.75 4.75H15.25V6.25C15.25 6.66421 15.5858 7 16 7C16.4142 7 16.75 6.66421 16.75 6.25V4.75H18.25C18.6642 4.75 19 4.41421 19 4C19 3.58579 18.6642 3.25 18.25 3.25H16.75V1.75C16.75 1.33579 16.4142 1 16 1Z" fill="#B6B6B6"/> -<path d="M13.9511 11.2734C14.0838 11.2413 14.209 11.2856 14.277 11.3487C14.3079 11.3773 14.315 11.3985 14.3162 11.4033C14.3168 11.4058 14.3172 11.4082 14.3169 11.4123C14.3166 11.4164 14.3151 11.428 14.3068 11.448C13.4567 13.4823 11.7974 14.75 9.99981 14.75C8.21001 14.75 6.55764 13.4936 5.70405 11.4746C5.69549 11.4544 5.69407 11.4428 5.69377 11.4389C5.69346 11.435 5.69378 11.4328 5.69439 11.4304C5.69561 11.4256 5.70295 11.4038 5.73506 11.3741C5.80602 11.3087 5.93588 11.2636 6.0714 11.2967C7.07906 11.5427 8.42465 11.7587 10.0046 11.7388C11.5931 11.7384 12.9439 11.5172 13.9511 11.2734Z" stroke="#B6B6B6" stroke-width="1.5"/> +<path d="M13.9549 11.7218C14.2966 11.0129 13.5584 10.4096 12.7997 10.6185C12.0508 10.8246 11.0982 11 9.99984 11C8.91327 11.014 7.9693 10.8452 7.2241 10.6404C6.45931 10.4302 5.71042 11.0374 6.05856 11.7501C6.86359 13.398 8.32748 14.5 9.99984 14.5C11.6818 14.5 13.1528 13.3854 13.9549 11.7218Z" fill="#B6B6B6"/> </svg> \ No newline at end of file diff --git a/src/img/icon/chat/buttons/reaction1.svg b/src/img/icon/chat/buttons/reaction1.svg index 07a434de97..5734380531 100644 --- a/src/img/icon/chat/buttons/reaction1.svg +++ b/src/img/icon/chat/buttons/reaction1.svg @@ -1,6 +1,6 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M11.5821 2.15642C11.0707 2.05383 10.5416 2 10 2C5.58172 2 2 5.58172 2 10C2 14.4183 5.58172 18 10 18C14.4183 18 18 14.4183 18 10C18 9.69754 17.9832 9.399 17.9505 9.10525C17.4912 9.30004 16.9967 9.42811 16.4793 9.47733C16.493 9.64974 16.5 9.82405 16.5 10C16.5 13.5899 13.5899 16.5 10 16.5C6.41015 16.5 3.5 13.5899 3.5 10C3.5 6.41015 6.41015 3.5 10 3.5C10.3689 3.5 10.7306 3.53073 11.0827 3.58976C11.1763 3.08082 11.3469 2.59886 11.5821 2.15642Z" fill="#252525"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6.5C7.44772 6.5 7 6.94772 7 7.5C7 8.05228 7.44772 8.5 8 8.5C8.55228 8.5 9 8.05228 9 7.5C9 6.94772 8.55228 6.5 8 6.5ZM12 6.5C11.4477 6.5 11 6.94772 11 7.5C11 8.05228 11.4477 8.5 12 8.5C12.5523 8.5 13 8.05228 13 7.5C13 6.94772 12.5523 6.5 12 6.5Z" fill="#252525"/> +<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6C7.44772 6 7 6.67157 7 7.5C7 8.32843 7.44772 9 8 9C8.55228 9 9 8.32843 9 7.5C9 6.67157 8.55228 6 8 6ZM12 6C11.4477 6 11 6.67157 11 7.5C11 8.32843 11.4477 9 12 9C12.5523 9 13 8.32843 13 7.5C13 6.67157 12.5523 6 12 6Z" fill="#252525"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 1C15.5858 1 15.25 1.33579 15.25 1.75V3.25H13.75C13.3358 3.25 13 3.58579 13 4C13 4.41421 13.3358 4.75 13.75 4.75H15.25V6.25C15.25 6.66421 15.5858 7 16 7C16.4142 7 16.75 6.66421 16.75 6.25V4.75H18.25C18.6642 4.75 19 4.41421 19 4C19 3.58579 18.6642 3.25 18.25 3.25H16.75V1.75C16.75 1.33579 16.4142 1 16 1Z" fill="#252525"/> -<path d="M13.9511 11.2734C14.0838 11.2413 14.209 11.2856 14.277 11.3487C14.3079 11.3773 14.315 11.3985 14.3162 11.4033C14.3168 11.4058 14.3172 11.4082 14.3169 11.4123C14.3166 11.4164 14.3151 11.428 14.3068 11.448C13.4567 13.4823 11.7974 14.75 9.99981 14.75C8.21001 14.75 6.55764 13.4936 5.70405 11.4746C5.69549 11.4544 5.69407 11.4428 5.69377 11.4389C5.69346 11.435 5.69378 11.4328 5.69439 11.4304C5.69561 11.4256 5.70295 11.4038 5.73506 11.3741C5.80602 11.3087 5.93588 11.2636 6.0714 11.2967C7.07906 11.5427 8.42465 11.7587 10.0046 11.7388C11.5931 11.7384 12.9439 11.5172 13.9511 11.2734Z" stroke="#252525" stroke-width="1.5"/> -</svg> +<path d="M13.9549 11.7218C14.2966 11.0129 13.5584 10.4096 12.7997 10.6185C12.0508 10.8246 11.0982 11 9.99984 11C8.91327 11.014 7.9693 10.8452 7.2241 10.6404C6.45931 10.4302 5.71042 11.0374 6.05856 11.7501C6.86359 13.398 8.32748 14.5 9.99984 14.5C11.6818 14.5 13.1528 13.3854 13.9549 11.7218Z" fill="#252525"/> +</svg> \ No newline at end of file diff --git a/src/img/icon/chat/buttons/remove.svg b/src/img/icon/chat/buttons/remove.svg index 013eb95104..edb3bf2f8c 100644 --- a/src/img/icon/chat/buttons/remove.svg +++ b/src/img/icon/chat/buttons/remove.svg @@ -1,3 +1,3 @@ <svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M6.63008 0.235269C6.94239 -0.0773428 7.44998 -0.0785774 7.76381 0.232511C8.07765 0.5436 8.07889 1.04921 7.76658 1.36182L5.13097 4L7.76658 6.63818C8.07889 6.95079 8.07765 7.4564 7.76381 7.76749C7.44998 8.07858 6.94239 8.07734 6.63008 7.76473L4 5.13208L1.36992 7.76473C1.05761 8.07734 0.550025 8.07858 0.236189 7.76749C-0.0776455 7.4564 -0.078885 6.95079 0.233421 6.63818L2.86903 4L0.233421 1.36182C-0.0788849 1.04921 -0.0776455 0.5436 0.23619 0.232511C0.550025 -0.0785774 1.05761 -0.0773428 1.36992 0.235269L4 2.86792L6.63008 0.235269Z" fill="#B6B6B6"/> -</svg> \ No newline at end of file +<path fill-rule="evenodd" clip-rule="evenodd" d="M6.63008 0.235269C6.94239 -0.0773428 7.44998 -0.0785774 7.76381 0.232511C8.07765 0.5436 8.07889 1.04921 7.76658 1.36182L5.13097 4L7.76658 6.63818C8.07889 6.95079 8.07765 7.4564 7.76381 7.76749C7.44998 8.07858 6.94239 8.07734 6.63008 7.76473L4 5.13208L1.36992 7.76473C1.05761 8.07734 0.550025 8.07858 0.236189 7.76749C-0.0776455 7.4564 -0.078885 6.95079 0.233421 6.63818L2.86903 4L0.233421 1.36182C-0.0788849 1.04921 -0.0776455 0.5436 0.23619 0.232511C0.550025 -0.0785774 1.05761 -0.0773428 1.36992 0.235269L4 2.86792L6.63008 0.235269Z" fill="#252525"/> +</svg> diff --git a/src/img/icon/menu/action/date0.svg b/src/img/icon/menu/action/date0.svg new file mode 100644 index 0000000000..ff38c6cfeb --- /dev/null +++ b/src/img/icon/menu/action/date0.svg @@ -0,0 +1,11 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="9.75" y="10.75" width="20.5" height="18.5" rx="4.25" stroke="#B6B6B6" stroke-width="1.5"/> +<path d="M23 18.9C23 18.6791 23.1791 18.5 23.4 18.5H25.6C25.8209 18.5 26 18.6791 26 18.9V19.6C26 19.8209 25.8209 20 25.6 20H23.4C23.1791 20 23 19.8209 23 19.6V18.9Z" fill="#B6B6B6"/> +<path d="M18.5 21.9547C18.5 21.7338 18.6791 21.5547 18.9 21.5547H21.1C21.3209 21.5547 21.5 21.7338 21.5 21.9547V22.6547C21.5 22.8756 21.3209 23.0547 21.1 23.0547H18.9C18.6791 23.0547 18.5 22.8756 18.5 22.6547V21.9547Z" fill="#B6B6B6"/> +<path d="M23 21.9547C23 21.7338 23.1791 21.5547 23.4 21.5547H25.6C25.8209 21.5547 26 21.7338 26 21.9547V22.6547C26 22.8756 25.8209 23.0547 25.6 23.0547H23.4C23.1791 23.0547 23 22.8756 23 22.6547V21.9547Z" fill="#B6B6B6"/> +<path d="M18.5 18.9C18.5 18.6791 18.6791 18.5 18.9 18.5H21.1C21.3209 18.5 21.5 18.6791 21.5 18.9V19.6C21.5 19.8209 21.3209 20 21.1 20H18.9C18.6791 20 18.5 19.8209 18.5 19.6V18.9Z" fill="#B6B6B6"/> +<path d="M14 21.9C14 21.6791 14.1791 21.5 14.4 21.5H16.6C16.8209 21.5 17 21.6791 17 21.9V22.6C17 22.8209 16.8209 23 16.6 23H14.4C14.1791 23 14 22.8209 14 22.6V21.9Z" fill="#B6B6B6"/> +<path d="M18.5 24.9C18.5 24.6791 18.6791 24.5 18.9 24.5H21.1C21.3209 24.5 21.5 24.6791 21.5 24.9V25.6C21.5 25.8209 21.3209 26 21.1 26H18.9C18.6791 26 18.5 25.8209 18.5 25.6V24.9Z" fill="#B6B6B6"/> +<path d="M14 24.9C14 24.6791 14.1791 24.5 14.4 24.5H16.6C16.8209 24.5 17 24.6791 17 24.9V25.6C17 25.8209 16.8209 26 16.6 26H14.4C14.1791 26 14 25.8209 14 25.6V24.9Z" fill="#B6B6B6"/> +<path d="M9.5 15.5C9.5 13.0147 11.5147 11 14 11H26C28.4853 11 30.5 13.0147 30.5 15.5V16H9.5V15.5Z" fill="#B6B6B6"/> +</svg> \ No newline at end of file diff --git a/src/img/icon/menu/action/graph0.svg b/src/img/icon/menu/action/graph0.svg index 9fb75dcd18..735af9899d 100644 --- a/src/img/icon/menu/action/graph0.svg +++ b/src/img/icon/menu/action/graph0.svg @@ -1,3 +1,7 @@ -<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M12.0002 7.5C13.1048 7.5 14.0002 6.60457 14.0002 5.5C14.0002 4.39543 13.1048 3.5 12.0002 3.5C10.8956 3.5 10.0002 4.39543 10.0002 5.5C10.0002 6.60457 10.8956 7.5 12.0002 7.5ZM14.5 12C14.5 13.3807 13.3807 14.5 12 14.5C10.6193 14.5 9.5 13.3807 9.5 12C9.5 10.6193 10.6193 9.5 12 9.5C13.3807 9.5 14.5 10.6193 14.5 12ZM14.0002 18.5C14.0002 19.6046 13.1048 20.5 12.0002 20.5C10.8956 20.5 10.0002 19.6046 10.0002 18.5C10.0002 17.3954 10.8956 16.5 12.0002 16.5C13.1048 16.5 14.0002 17.3954 14.0002 18.5ZM20 15.5C20 16.6046 19.1046 17.5 18 17.5C16.8954 17.5 16 16.6046 16 15.5C16 14.3954 16.8954 13.5 18 13.5C19.1046 13.5 20 14.3954 20 15.5ZM6 10.5C7.10457 10.5 8 9.60457 8 8.5C8 7.39543 7.10457 6.5 6 6.5C4.89543 6.5 4 7.39543 4 8.5C4 9.60457 4.89543 10.5 6 10.5ZM20 8.5C20 9.60457 19.1046 10.5 18 10.5C16.8954 10.5 16 9.60457 16 8.5C16 7.39543 16.8954 6.5 18 6.5C19.1046 6.5 20 7.39543 20 8.5ZM6 17.5C7.10457 17.5 8 16.6046 8 15.5C8 14.3954 7.10457 13.5 6 13.5C4.89543 13.5 4 14.3954 4 15.5C4 16.6046 4.89543 17.5 6 17.5Z" fill="#B6B6B6"/> -</svg> \ No newline at end of file +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 3.5C11.5 4.32843 10.8284 5 10 5C9.17157 5 8.5 4.32843 8.5 3.5C8.5 2.67157 9.17157 2 10 2C10.8284 2 11.5 2.67157 11.5 3.5ZM12 10C12 11.1046 11.1046 12 10 12C8.89543 12 8 11.1046 8 10C8 8.89543 8.89543 8 10 8C11.1046 8 12 8.89543 12 10ZM10 18C10.8284 18 11.5 17.3284 11.5 16.5C11.5 15.6716 10.8284 15 10 15C9.17157 15 8.5 15.6716 8.5 16.5C8.5 17.3284 9.17157 18 10 18Z" fill="#B6B6B6"/> +<circle cx="4.36935" cy="6.74979" r="1.5" transform="rotate(-60 4.36935 6.74979)" fill="#B6B6B6"/> +<circle cx="15.6272" cy="13.2498" r="1.5" transform="rotate(-60 15.6272 13.2498)" fill="#B6B6B6"/> +<circle cx="4.36935" cy="13.2498" r="1.5" transform="rotate(-120 4.36935 13.2498)" fill="#B6B6B6"/> +<circle cx="15.6272" cy="6.74979" r="1.5" transform="rotate(-120 15.6272 6.74979)" fill="#B6B6B6"/> +</svg> diff --git a/src/img/icon/menu/widget/compact.svg b/src/img/icon/menu/widget/compact.svg index e6068b67ac..9d35669576 100644 --- a/src/img/icon/menu/widget/compact.svg +++ b/src/img/icon/menu/widget/compact.svg @@ -1,15 +1,11 @@ -<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="1.4375" width="39" height="39" rx="5.5" stroke="#E3E3E3"/> -<rect x="10" y="7.25" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M6 8C6 7.44772 6.44772 7 7 7V7C7.55228 7 8 7.44772 8 8V8C8 8.55228 7.55228 9 7 9V9C6.44772 9 6 8.55228 6 8V8Z" fill="#B6B6B6"/> -<path d="M6 13C6 12.4477 6.44772 12 7 12V12C7.55228 12 8 12.4477 8 13V13C8 13.5523 7.55228 14 7 14V14C6.44772 14 6 13.5523 6 13V13Z" fill="#B6B6B6"/> -<path d="M10 13C10 12.5858 10.3358 12.25 10.75 12.25H27.25C27.6642 12.25 28 12.5858 28 13V13C28 13.4142 27.6642 13.75 27.25 13.75H10.75C10.3358 13.75 10 13.4142 10 13V13Z" fill="#B6B6B6"/> -<rect x="10" y="17.25" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M6 18C6 17.4477 6.44772 17 7 17V17C7.55228 17 8 17.4477 8 18V18C8 18.5523 7.55228 19 7 19V19C6.44772 19 6 18.5523 6 18V18Z" fill="#B6B6B6"/> -<path d="M6 23C6 22.4477 6.44772 22 7 22V22C7.55228 22 8 22.4477 8 23V23C8 23.5523 7.55228 24 7 24V24C6.44772 24 6 23.5523 6 23V23Z" fill="#B6B6B6"/> -<path d="M10 23C10 22.5858 10.3358 22.25 10.75 22.25H27.25C27.6642 22.25 28 22.5858 28 23V23C28 23.4142 27.6642 23.75 27.25 23.75H10.75C10.3358 23.75 10 23.4142 10 23V23Z" fill="#B6B6B6"/> -<path d="M6 28C6 27.4477 6.44772 27 7 27V27C7.55228 27 8 27.4477 8 28V28C8 28.5523 7.55228 29 7 29V29C6.44772 29 6 28.5523 6 28V28Z" fill="#B6B6B6"/> -<path d="M10 28C10 27.5858 10.3358 27.25 10.75 27.25H33.25C33.6642 27.25 34 27.5858 34 28V28C34 28.4142 33.6642 28.75 33.25 28.75H10.75C10.3358 28.75 10 28.4142 10 28V28Z" fill="#B6B6B6"/> -<path d="M6 33C6 32.4477 6.44772 32 7 32V32C7.55228 32 8 32.4477 8 33V33C8 33.5523 7.55228 34 7 34V34C6.44772 34 6 33.5523 6 33V33Z" fill="#B6B6B6"/> -<rect x="10" y="32.25" width="18" height="1.5" rx="0.75" fill="#B6B6B6"/> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="20" y="11.25" width="17" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="20" y="15.25" width="12" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="9" y="10" width="8" height="8" rx="2" fill="#E3E3E3"/> +<rect x="20" y="21.25" width="17" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="20" y="25.25" width="12" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="9" y="20" width="8" height="8" rx="2" fill="#E3E3E3"/> +<rect x="20" y="31.25" width="17" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="20" y="35.25" width="12" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="9" y="30" width="8" height="8" rx="2" fill="#E3E3E3"/> </svg> diff --git a/src/img/icon/menu/widget/link.svg b/src/img/icon/menu/widget/link.svg index 26fa763fd6..01d0d66f52 100644 --- a/src/img/icon/menu/widget/link.svg +++ b/src/img/icon/menu/widget/link.svg @@ -1,4 +1,4 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="12.4375" width="39" height="15" rx="3.5" stroke="#E3E3E3"/> -<rect x="6" y="19.25" width="28" height="1.5" rx="0.75" fill="#b6b6b6"/> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="8.5" y="19.5" width="31" height="9" rx="2.5" fill="white" stroke="#E3E3E3"/> +<rect x="12" y="23.5" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> </svg> diff --git a/src/img/icon/menu/widget/list.svg b/src/img/icon/menu/widget/list.svg index dc8abcbb9a..a7647d328d 100644 --- a/src/img/icon/menu/widget/list.svg +++ b/src/img/icon/menu/widget/list.svg @@ -1,19 +1,12 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3043_3792)"> -<rect x="0.5" y="0.4375" width="39" height="39" rx="5.5" stroke="#E3E3E3"/> -<rect x="17" y="7.25" width="17" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="17" y="11.25" width="12" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="6" y="6" width="8" height="8" rx="2" fill="#B6B6B6"/> -<rect x="17" y="17.25" width="17" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="17" y="21.25" width="12" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="6" y="16" width="8" height="8" rx="2" fill="#B6B6B6"/> -<rect x="17" y="27.25" width="17" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="17" y="31.25" width="12" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="6" y="26" width="8" height="8" rx="2" fill="#B6B6B6"/> -</g> -<defs> -<clipPath id="clip0_3043_3792"> -<rect width="40" height="40" fill="white"/> -</clipPath> -</defs> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="13.375" y="13.625" width="23.25" height="0.75" rx="0.375" stroke="#B6B6B6" stroke-width="0.75"/> +<path d="M9.5 14C9.5 13.7239 9.72386 13.5 10 13.5C10.2761 13.5 10.5 13.7239 10.5 14C10.5 14.2761 10.2761 14.5 10 14.5C9.72386 14.5 9.5 14.2761 9.5 14Z" stroke="#B6B6B6"/> +<path d="M9.5 19C9.5 18.7239 9.72386 18.5 10 18.5C10.2761 18.5 10.5 18.7239 10.5 19C10.5 19.2761 10.2761 19.5 10 19.5C9.72386 19.5 9.5 19.2761 9.5 19Z" stroke="#B6B6B6"/> +<path d="M13.5 19C13.5 18.8619 13.6119 18.75 13.75 18.75H30.25C30.3881 18.75 30.5 18.8619 30.5 19C30.5 19.1381 30.3881 19.25 30.25 19.25H13.75C13.6119 19.25 13.5 19.1381 13.5 19Z" stroke="#B6B6B6"/> +<rect x="13.375" y="23.625" width="23.25" height="0.75" rx="0.375" stroke="#B6B6B6" stroke-width="0.75"/> +<path d="M9.5 24C9.5 23.7239 9.72386 23.5 10 23.5C10.2761 23.5 10.5 23.7239 10.5 24C10.5 24.2761 10.2761 24.5 10 24.5C9.72386 24.5 9.5 24.2761 9.5 24Z" stroke="#B6B6B6"/> +<path d="M9.5 29C9.5 28.7239 9.72386 28.5 10 28.5C10.2761 28.5 10.5 28.7239 10.5 29C10.5 29.2761 10.2761 29.5 10 29.5C9.72386 29.5 9.5 29.2761 9.5 29Z" stroke="#B6B6B6"/> +<path d="M13.5 29C13.5 28.8619 13.6119 28.75 13.75 28.75H30.25C30.3881 28.75 30.5 28.8619 30.5 29C30.5 29.1381 30.3881 29.25 30.25 29.25H13.75C13.6119 29.25 13.5 29.1381 13.5 29Z" stroke="#B6B6B6"/> +<path d="M9.5 34C9.5 33.7239 9.72386 33.5 10 33.5C10.2761 33.5 10.5 33.7239 10.5 34C10.5 34.2761 10.2761 34.5 10 34.5C9.72386 34.5 9.5 34.2761 9.5 34Z" stroke="#B6B6B6"/> +<path d="M13.5 34C13.5 33.8619 13.6119 33.75 13.75 33.75H36.25C36.3881 33.75 36.5 33.8619 36.5 34C36.5 34.1381 36.3881 34.25 36.25 34.25H13.75C13.6119 34.25 13.5 34.1381 13.5 34Z" stroke="#B6B6B6"/> </svg> diff --git a/src/img/icon/menu/widget/tree.svg b/src/img/icon/menu/widget/tree.svg index a006fd1676..e20d9f4369 100644 --- a/src/img/icon/menu/widget/tree.svg +++ b/src/img/icon/menu/widget/tree.svg @@ -1,22 +1,12 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3043_3806)"> -<rect x="0.5" y="0.4375" width="39" height="39" rx="5.5" stroke="#E3E3E3"/> -<rect x="10" y="6.25" width="24" height="1.5" rx="0.75" fill="#b6b6b6"/> -<path d="M6 7C6 6.44772 6.44772 6 7 6C7.55228 6 8 6.44772 8 7C8 7.55228 7.55228 8 7 8C6.44772 8 6 7.55228 6 7Z" fill="#b6b6b6"/> -<path d="M6 12C6 11.4477 6.44772 11 7 11C7.55228 11 8 11.4477 8 12C8 12.5523 7.55228 13 7 13C6.44772 13 6 12.5523 6 12Z" fill="#b6b6b6"/> -<path d="M10 17C10 16.4477 10.4477 16 11 16C11.5523 16 12 16.4477 12 17C12 17.5523 11.5523 18 11 18C10.4477 18 10 17.5523 10 17Z" fill="#b6b6b6"/> -<path d="M14 22C14 21.4477 14.4477 21 15 21C15.5523 21 16 21.4477 16 22C16 22.5523 15.5523 23 15 23C14.4477 23 14 22.5523 14 22Z" fill="#b6b6b6"/> -<path d="M10 12C10 11.5858 10.3358 11.25 10.75 11.25H33.25C33.6642 11.25 34 11.5858 34 12C34 12.4142 33.6642 12.75 33.25 12.75H10.75C10.3358 12.75 10 12.4142 10 12Z" fill="#b6b6b6"/> -<rect x="14" y="16.25" width="20" height="1.5" rx="0.75" fill="#b6b6b6"/> -<rect x="18" y="21.25" width="16" height="1.5" rx="0.75" fill="#b6b6b6"/> -<path d="M14 27C14 26.4477 14.4477 26 15 26C15.5523 26 16 26.4477 16 27C16 27.5523 15.5523 28 15 28C14.4477 28 14 27.5523 14 27Z" fill="#b6b6b6"/> -<rect x="18" y="26.25" width="16" height="1.5" rx="0.75" fill="#b6b6b6"/> -<path d="M6 32C6 31.4477 6.44772 31 7 31C7.55228 31 8 31.4477 8 32C8 32.5523 7.55228 33 7 33C6.44772 33 6 32.5523 6 32Z" fill="#b6b6b6"/> -<rect x="10" y="31.25" width="24" height="1.5" rx="0.75" fill="#b6b6b6"/> -</g> -<defs> -<clipPath id="clip0_3043_3806"> -<rect width="40" height="40" fill="white"/> -</clipPath> -</defs> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 14C10 13.4477 10.4477 13 11 13C11.5523 13 12 13.4477 12 14C12 14.5523 11.5523 15 11 15C10.4477 15 10 14.5523 10 14Z" fill="#B6B6B6"/> +<path d="M14 19C14 18.4477 14.4477 18 15 18C15.5523 18 16 18.4477 16 19C16 19.5523 15.5523 20 15 20C14.4477 20 14 19.5523 14 19Z" fill="#B6B6B6"/> +<path d="M18 24C18 23.4477 18.4477 23 19 23C19.5523 23 20 23.4477 20 24C20 24.5523 19.5523 25 19 25C18.4477 25 18 24.5523 18 24Z" fill="#B6B6B6"/> +<path d="M14 14C14 13.5858 14.3358 13.25 14.75 13.25H37.25C37.6642 13.25 38 13.5858 38 14C38 14.4142 37.6642 14.75 37.25 14.75H14.75C14.3358 14.75 14 14.4142 14 14Z" fill="#B6B6B6"/> +<path d="M37.25 18.25H18.75C18.3358 18.25 18 18.5858 18 19C18 19.4142 18.3358 19.75 18.75 19.75H37.25C37.6642 19.75 38 19.4142 38 19C38 18.5858 37.6642 18.25 37.25 18.25Z" fill="#B6B6B6"/> +<path d="M37.25 23.25H22.75C22.3358 23.25 22 23.5858 22 24C22 24.4142 22.3358 24.75 22.75 24.75H37.25C37.6642 24.75 38 24.4142 38 24C38 23.5858 37.6642 23.25 37.25 23.25Z" fill="#B6B6B6"/> +<path d="M18 29C18 28.4477 18.4477 28 19 28C19.5523 28 20 28.4477 20 29C20 29.5523 19.5523 30 19 30C18.4477 30 18 29.5523 18 29Z" fill="#B6B6B6"/> +<path d="M37.25 28.25H22.75C22.3358 28.25 22 28.5858 22 29C22 29.4142 22.3358 29.75 22.75 29.75H37.25C37.6642 29.75 38 29.4142 38 29C38 28.5858 37.6642 28.25 37.25 28.25Z" fill="#B6B6B6"/> +<path d="M10 34C10 33.4477 10.4477 33 11 33C11.5523 33 12 33.4477 12 34C12 34.5523 11.5523 35 11 35C10.4477 35 10 34.5523 10 34Z" fill="#B6B6B6"/> +<path d="M37.25 33.25H14.75C14.3358 33.25 14 33.5858 14 34C14 34.4142 14.3358 34.75 14.75 34.75H37.25C37.6642 34.75 38 34.4142 38 34C38 33.5858 37.6642 33.25 37.25 33.25Z" fill="#B6B6B6"/> </svg> diff --git a/src/img/icon/menu/widget/view.svg b/src/img/icon/menu/widget/view.svg index 91ec1b07e2..1169ce26dc 100644 --- a/src/img/icon/menu/widget/view.svg +++ b/src/img/icon/menu/widget/view.svg @@ -1,28 +1,10 @@ -<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="1.4375" width="39" height="39" rx="5.5" stroke="#E3E3E3"/> -<rect x="6" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="9.25" width="10" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="9.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="9.5" y="14.5" width="13" height="19" rx="1.5" fill="white" stroke="#E3E3E3"/> +<rect x="25.5" y="14.5" width="13" height="19" rx="1.5" fill="white" stroke="#E3E3E3"/> +<path d="M9 16C9 14.8954 9.89543 14 11 14H21C22.1046 14 23 14.8954 23 16V24H9V16Z" fill="#E3E3E3"/> +<path d="M25 16C25 14.8954 25.8954 14 27 14H37C38.1046 14 39 14.8954 39 16V24H25V16Z" fill="#E3E3E3"/> +<rect x="12" y="26.25" width="8" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="12" y="29.25" width="8" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="28" y="26.25" width="8" height="1.5" rx="0.75" fill="#B6B6B6"/> +<rect x="28" y="29.25" width="8" height="1.5" rx="0.75" fill="#B6B6B6"/> </svg> diff --git a/src/img/icon/popup/confirm/warningInverted.svg b/src/img/icon/popup/confirm/warningInverted.svg new file mode 100644 index 0000000000..b4652fb491 --- /dev/null +++ b/src/img/icon/popup/confirm/warningInverted.svg @@ -0,0 +1,225 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="96" height="72" viewBox="0 0 96 72" fill="none"> + <path d="M59.5 -0.00927734L23.5 35.9907L59.5 71.9907L95.5 35.9907L59.5 -0.00927734Z" fill="white"/> + <rect x="63.5" y="3.99072" width="2" height="2" fill="#252525"/> + <rect x="61.5" y="1.99072" width="2" height="2" fill="#252525"/> + <rect x="59.5" y="-0.00927734" width="2" height="2" fill="#252525"/> + <rect x="69.5" y="9.99072" width="2" height="2" fill="#252525"/> + <rect x="67.5" y="7.99072" width="2" height="2" fill="#252525"/> + <rect x="65.5" y="5.99072" width="2" height="2" fill="#252525"/> + <rect x="75.5" y="15.9907" width="2" height="2" fill="#252525"/> + <rect x="73.5" y="13.9907" width="2" height="2" fill="#252525"/> + <rect x="71.5" y="11.9907" width="2" height="2" fill="#252525"/> + <rect x="81.5" y="21.9907" width="2" height="2" fill="#252525"/> + <rect x="79.5" y="19.9907" width="2" height="2" fill="#252525"/> + <rect x="77.5" y="17.9907" width="2" height="2" fill="#252525"/> + <rect x="87.5" y="27.9907" width="2" height="2" fill="#252525"/> + <rect x="85.5" y="25.9907" width="2" height="2" fill="#252525"/> + <rect x="83.5" y="23.9907" width="2" height="2" fill="#252525"/> + <rect x="93.5" y="33.9907" width="2" height="2" fill="#252525"/> + <rect x="91.5" y="31.9907" width="2" height="2" fill="#252525"/> + <rect x="89.5" y="29.9907" width="2" height="2" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 63.5 68.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 61.5 70.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 59.5 72.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 69.5 62.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 67.5 64.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 65.5 66.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 75.5 56.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 73.5 58.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 71.5 60.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 81.5 50.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 79.5 52.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 77.5 54.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 87.5 44.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 85.5 46.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 83.5 48.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 93.5 38.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 91.5 40.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 89.5 42.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 55.5 3.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 57.5 1.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 59.5 -0.00927734)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 49.5 9.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 51.5 7.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 53.5 5.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 43.5 15.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 45.5 13.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 47.5 11.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 37.5 21.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 39.5 19.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 41.5 17.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 31.5 27.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 33.5 25.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 35.5 23.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 25.5 33.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 27.5 31.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 29.5 29.9907)" fill="#252525"/> + <rect x="55.5" y="68.0093" width="2" height="2" transform="rotate(180 55.5 68.0093)" fill="#252525"/> + <rect x="57.5" y="70.0093" width="2" height="2" transform="rotate(180 57.5 70.0093)" fill="#252525"/> + <rect x="59.5" y="72.0093" width="2" height="2" transform="rotate(180 59.5 72.0093)" fill="#252525"/> + <rect x="49.5" y="62.0093" width="2" height="2" transform="rotate(180 49.5 62.0093)" fill="#252525"/> + <rect x="51.5" y="64.0093" width="2" height="2" transform="rotate(180 51.5 64.0093)" fill="#252525"/> + <rect x="53.5" y="66.0093" width="2" height="2" transform="rotate(180 53.5 66.0093)" fill="#252525"/> + <rect x="43.5" y="56.0093" width="2" height="2" transform="rotate(180 43.5 56.0093)" fill="#252525"/> + <rect x="45.5" y="58.0093" width="2" height="2" transform="rotate(180 45.5 58.0093)" fill="#252525"/> + <rect x="47.5" y="60.0093" width="2" height="2" transform="rotate(180 47.5 60.0093)" fill="#252525"/> + <rect x="37.5" y="50.0093" width="2" height="2" transform="rotate(180 37.5 50.0093)" fill="#252525"/> + <rect x="39.5" y="52.0093" width="2" height="2" transform="rotate(180 39.5 52.0093)" fill="#252525"/> + <rect x="41.5" y="54.0093" width="2" height="2" transform="rotate(180 41.5 54.0093)" fill="#252525"/> + <rect x="31.5" y="44.0093" width="2" height="2" transform="rotate(180 31.5 44.0093)" fill="#252525"/> + <rect x="33.5" y="46.0093" width="2" height="2" transform="rotate(180 33.5 46.0093)" fill="#252525"/> + <rect x="35.5" y="48.0093" width="2" height="2" transform="rotate(180 35.5 48.0093)" fill="#252525"/> + <rect x="25.5" y="38.0093" width="2" height="2" transform="rotate(180 25.5 38.0093)" fill="#252525"/> + <rect x="27.5" y="40.0093" width="2" height="2" transform="rotate(180 27.5 40.0093)" fill="#252525"/> + <rect x="29.5" y="42.0093" width="2" height="2" transform="rotate(180 29.5 42.0093)" fill="#252525"/> + <path d="M48.5 -0.00927734L12.5 35.9907L48.5 71.9907L84.5 35.9907L48.5 -0.00927734Z" fill="white"/> + <rect x="52.5" y="3.99072" width="2" height="2" fill="#252525"/> + <rect x="50.5" y="1.99072" width="2" height="2" fill="#252525"/> + <rect x="48.5" y="-0.00927734" width="2" height="2" fill="#252525"/> + <rect x="58.5" y="9.99072" width="2" height="2" fill="#252525"/> + <rect x="56.5" y="7.99072" width="2" height="2" fill="#252525"/> + <rect x="54.5" y="5.99072" width="2" height="2" fill="#252525"/> + <rect x="64.5" y="15.9907" width="2" height="2" fill="#252525"/> + <rect x="62.5" y="13.9907" width="2" height="2" fill="#252525"/> + <rect x="60.5" y="11.9907" width="2" height="2" fill="#252525"/> + <rect x="70.5" y="21.9907" width="2" height="2" fill="#252525"/> + <rect x="68.5" y="19.9907" width="2" height="2" fill="#252525"/> + <rect x="66.5" y="17.9907" width="2" height="2" fill="#252525"/> + <rect x="76.5" y="27.9907" width="2" height="2" fill="#252525"/> + <rect x="74.5" y="25.9907" width="2" height="2" fill="#252525"/> + <rect x="72.5" y="23.9907" width="2" height="2" fill="#252525"/> + <rect x="82.5" y="33.9907" width="2" height="2" fill="#252525"/> + <rect x="80.5" y="31.9907" width="2" height="2" fill="#252525"/> + <rect x="78.5" y="29.9907" width="2" height="2" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 52.5 68.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 50.5 70.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 48.5 72.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 58.5 62.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 56.5 64.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 54.5 66.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 64.5 56.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 62.5 58.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 60.5 60.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 70.5 50.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 68.5 52.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 66.5 54.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 76.5 44.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 74.5 46.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 72.5 48.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 82.5 38.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 80.5 40.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 78.5 42.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 44.5 3.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 46.5 1.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 48.5 -0.00927734)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 38.5 9.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 40.5 7.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 42.5 5.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 32.5 15.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 34.5 13.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 36.5 11.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 26.5 21.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 28.5 19.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 30.5 17.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 20.5 27.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 22.5 25.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 24.5 23.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 14.5 33.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 16.5 31.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 18.5 29.9907)" fill="#252525"/> + <rect x="44.5" y="68.0093" width="2" height="2" transform="rotate(180 44.5 68.0093)" fill="#252525"/> + <rect x="46.5" y="70.0093" width="2" height="2" transform="rotate(180 46.5 70.0093)" fill="#252525"/> + <rect x="48.5" y="72.0093" width="2" height="2" transform="rotate(180 48.5 72.0093)" fill="#252525"/> + <rect x="38.5" y="62.0093" width="2" height="2" transform="rotate(180 38.5 62.0093)" fill="#252525"/> + <rect x="40.5" y="64.0093" width="2" height="2" transform="rotate(180 40.5 64.0093)" fill="#252525"/> + <rect x="42.5" y="66.0093" width="2" height="2" transform="rotate(180 42.5 66.0093)" fill="#252525"/> + <rect x="32.5" y="56.0093" width="2" height="2" transform="rotate(180 32.5 56.0093)" fill="#252525"/> + <rect x="34.5" y="58.0093" width="2" height="2" transform="rotate(180 34.5 58.0093)" fill="#252525"/> + <rect x="36.5" y="60.0093" width="2" height="2" transform="rotate(180 36.5 60.0093)" fill="#252525"/> + <rect x="26.5" y="50.0093" width="2" height="2" transform="rotate(180 26.5 50.0093)" fill="#252525"/> + <rect x="28.5" y="52.0093" width="2" height="2" transform="rotate(180 28.5 52.0093)" fill="#252525"/> + <rect x="30.5" y="54.0093" width="2" height="2" transform="rotate(180 30.5 54.0093)" fill="#252525"/> + <rect x="20.5" y="44.0093" width="2" height="2" transform="rotate(180 20.5 44.0093)" fill="#252525"/> + <rect x="22.5" y="46.0093" width="2" height="2" transform="rotate(180 22.5 46.0093)" fill="#252525"/> + <rect x="24.5" y="48.0093" width="2" height="2" transform="rotate(180 24.5 48.0093)" fill="#252525"/> + <rect x="14.5" y="38.0093" width="2" height="2" transform="rotate(180 14.5 38.0093)" fill="#252525"/> + <rect x="16.5" y="40.0093" width="2" height="2" transform="rotate(180 16.5 40.0093)" fill="#252525"/> + <rect x="18.5" y="42.0093" width="2" height="2" transform="rotate(180 18.5 42.0093)" fill="#252525"/> + <path d="M36.5 -0.00927734L0.5 35.9907L36.5 71.9907L72.5 35.9907L36.5 -0.00927734Z" fill="white"/> + <rect x="40.5" y="3.99072" width="2" height="2" fill="#252525"/> + <rect x="38.5" y="1.99072" width="2" height="2" fill="#252525"/> + <rect x="36.5" y="-0.00927734" width="2" height="2" fill="#252525"/> + <rect x="46.5" y="9.99072" width="2" height="2" fill="#252525"/> + <rect x="44.5" y="7.99072" width="2" height="2" fill="#252525"/> + <rect x="42.5" y="5.99072" width="2" height="2" fill="#252525"/> + <rect x="52.5" y="15.9907" width="2" height="2" fill="#252525"/> + <rect x="50.5" y="13.9907" width="2" height="2" fill="#252525"/> + <rect x="48.5" y="11.9907" width="2" height="2" fill="#252525"/> + <rect x="58.5" y="21.9907" width="2" height="2" fill="#252525"/> + <rect x="56.5" y="19.9907" width="2" height="2" fill="#252525"/> + <rect x="54.5" y="17.9907" width="2" height="2" fill="#252525"/> + <rect x="64.5" y="27.9907" width="2" height="2" fill="#252525"/> + <rect x="62.5" y="25.9907" width="2" height="2" fill="#252525"/> + <rect x="60.5" y="23.9907" width="2" height="2" fill="#252525"/> + <rect x="70.5" y="33.9907" width="2" height="2" fill="#252525"/> + <rect x="68.5" y="31.9907" width="2" height="2" fill="#252525"/> + <rect x="66.5" y="29.9907" width="2" height="2" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 40.5 68.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 38.5 70.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 36.5 72.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 46.5 62.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 44.5 64.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 42.5 66.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 52.5 56.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 50.5 58.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 48.5 60.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 58.5 50.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 56.5 52.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 54.5 54.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 64.5 44.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 62.5 46.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 60.5 48.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 70.5 38.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 68.5 40.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(1 0 0 -1 66.5 42.0093)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 32.5 3.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 34.5 1.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 36.5 -0.00927734)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 26.5 9.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 28.5 7.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 30.5 5.99072)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 20.5 15.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 22.5 13.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 24.5 11.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 14.5 21.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 16.5 19.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 18.5 17.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 8.5 27.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 10.5 25.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 12.5 23.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 2.5 33.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 4.5 31.9907)" fill="#252525"/> + <rect width="2" height="2" transform="matrix(-1 0 0 1 6.5 29.9907)" fill="#252525"/> + <rect x="32.5" y="68.0093" width="2" height="2" transform="rotate(180 32.5 68.0093)" fill="#252525"/> + <rect x="34.5" y="70.0093" width="2" height="2" transform="rotate(180 34.5 70.0093)" fill="#252525"/> + <rect x="36.5" y="72.0093" width="2" height="2" transform="rotate(180 36.5 72.0093)" fill="#252525"/> + <rect x="26.5" y="62.0093" width="2" height="2" transform="rotate(180 26.5 62.0093)" fill="#252525"/> + <rect x="28.5" y="64.0093" width="2" height="2" transform="rotate(180 28.5 64.0093)" fill="#252525"/> + <rect x="30.5" y="66.0093" width="2" height="2" transform="rotate(180 30.5 66.0093)" fill="#252525"/> + <rect x="20.5" y="56.0093" width="2" height="2" transform="rotate(180 20.5 56.0093)" fill="#252525"/> + <rect x="22.5" y="58.0093" width="2" height="2" transform="rotate(180 22.5 58.0093)" fill="#252525"/> + <rect x="24.5" y="60.0093" width="2" height="2" transform="rotate(180 24.5 60.0093)" fill="#252525"/> + <rect x="14.5" y="50.0093" width="2" height="2" transform="rotate(180 14.5 50.0093)" fill="#252525"/> + <rect x="16.5" y="52.0093" width="2" height="2" transform="rotate(180 16.5 52.0093)" fill="#252525"/> + <rect x="18.5" y="54.0093" width="2" height="2" transform="rotate(180 18.5 54.0093)" fill="#252525"/> + <rect x="8.5" y="44.0093" width="2" height="2" transform="rotate(180 8.5 44.0093)" fill="#252525"/> + <rect x="10.5" y="46.0093" width="2" height="2" transform="rotate(180 10.5 46.0093)" fill="#252525"/> + <rect x="12.5" y="48.0093" width="2" height="2" transform="rotate(180 12.5 48.0093)" fill="#252525"/> + <rect x="2.5" y="38.0093" width="2" height="2" transform="rotate(180 2.5 38.0093)" fill="#252525"/> + <rect x="4.5" y="40.0093" width="2" height="2" transform="rotate(180 4.5 40.0093)" fill="#252525"/> + <rect x="6.5" y="42.0093" width="2" height="2" transform="rotate(180 6.5 42.0093)" fill="#252525"/> + <rect x="33.5" y="23.0044" width="6" height="15.9959" fill="#252525"/> + <rect x="33.5" y="42.999" width="6" height="2" fill="#252525"/> + <rect x="33.5" y="44.9985" width="6" height="2" fill="#252525"/> + <rect x="33.5" y="46.998" width="6" height="2" fill="#252525"/> +</svg> diff --git a/src/img/icon/sortArrow.svg b/src/img/icon/sortArrow.svg index 47acff2910..19114ccb9b 100644 --- a/src/img/icon/sortArrow.svg +++ b/src/img/icon/sortArrow.svg @@ -1,4 +1,4 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="9.5" y="5" width="1" height="10" fill="#949494"/> -<path d="M6 11L10 15L14 11" stroke="#949494"/> +<rect x="9.5" y="5" width="1" height="10" fill="#b6b6b6"/> +<path d="M6 11L10 15L14 11" stroke="#b6b6b6"/> </svg> \ No newline at end of file diff --git a/src/img/icon/toast/notice.svg b/src/img/icon/toast/notice.svg new file mode 100644 index 0000000000..37eff23c2f --- /dev/null +++ b/src/img/icon/toast/notice.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none"> + <path fill-rule="evenodd" clip-rule="evenodd" d="M10.5 17.5C14.6421 17.5 18 14.1421 18 10C18 5.85786 14.6421 2.5 10.5 2.5C6.35786 2.5 3 5.85786 3 10C3 14.1421 6.35786 17.5 10.5 17.5ZM10.5 19C15.4706 19 19.5 14.9706 19.5 10C19.5 5.02944 15.4706 1 10.5 1C5.52944 1 1.5 5.02944 1.5 10C1.5 14.9706 5.52944 19 10.5 19Z" fill="#F55522"/> + <path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 4.75H9.5L9.75 11.75H11.25L11.5 4.75ZM10.5 15.25C11.1904 15.25 11.75 14.6904 11.75 14C11.75 13.3096 11.1904 12.75 10.5 12.75C9.80964 12.75 9.25 13.3096 9.25 14C9.25 14.6904 9.80964 15.25 10.5 15.25Z" fill="#F55522"/> +</svg> diff --git a/src/img/icon/widget/button/export.svg b/src/img/icon/widget/button/export.svg new file mode 100644 index 0000000000..4c90c94a07 --- /dev/null +++ b/src/img/icon/widget/button/export.svg @@ -0,0 +1,3 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M14 18C16.2091 18 18 16.2091 18 14V6C18 3.79086 16.2091 2 14 2H6C3.79086 2 2 3.79086 2 6L2 14C2 16.2091 3.79086 18 6 18H14ZM9.5 14C9.5 14.2761 9.72386 14.5 10 14.5C10.2761 14.5 10.5 14.2761 10.5 14L10.5 7.11342L12.6678 9.04037C12.8742 9.22383 13.1902 9.20524 13.3737 8.99885C13.5572 8.79246 13.5386 8.47642 13.3322 8.29296L10.3322 5.6263L10 5.33102L9.66782 5.6263L6.66782 8.29296C6.46143 8.47642 6.44284 8.79246 6.6263 8.99885C6.80975 9.20524 7.12579 9.22383 7.33218 9.04037L9.5 7.11342L9.5 14Z" fill="#252525"/> +</svg> diff --git a/src/img/icon/widget/button/plus.svg b/src/img/icon/widget/button/plus.svg new file mode 100644 index 0000000000..6c6ac7de75 --- /dev/null +++ b/src/img/icon/widget/button/plus.svg @@ -0,0 +1,3 @@ +<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M10 3.75879C9.58579 3.75879 9.25 4.09458 9.25 4.50879V10.0088H3.75C3.33579 10.0088 3 10.3446 3 10.7588C3 11.173 3.33579 11.5088 3.75 11.5088H9.25V17.0088C9.25 17.423 9.58579 17.7588 10 17.7588C10.4142 17.7588 10.75 17.423 10.75 17.0088V11.5088H16.25C16.6642 11.5088 17 11.173 17 10.7588C17 10.3446 16.6642 10.0088 16.25 10.0088H10.75V4.50879C10.75 4.09458 10.4142 3.75879 10 3.75879Z" fill="#B6B6B6"/> +</svg> \ No newline at end of file diff --git a/src/img/icon/widget/button/search.svg b/src/img/icon/widget/button/search.svg new file mode 100644 index 0000000000..e7f2cbb2ae --- /dev/null +++ b/src/img/icon/widget/button/search.svg @@ -0,0 +1,4 @@ +<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3794 12.2588L16.3179 16.1972C16.5607 16.4401 16.5607 16.8338 16.3179 17.0767C16.075 17.3195 15.6813 17.3195 15.4384 17.0767L11.5 13.1382L12.3794 12.2588Z" fill="#B6B6B6"/> +<circle cx="8.75" cy="9.50879" r="4.625" stroke="#B6B6B6" stroke-width="1.25"/> +</svg> \ No newline at end of file diff --git a/src/img/icon/widget/plus.svg b/src/img/icon/widget/plus0.svg similarity index 100% rename from src/img/icon/widget/plus.svg rename to src/img/icon/widget/plus0.svg diff --git a/src/img/theme/dark/icon/widget/plus.svg b/src/img/icon/widget/plus1.svg similarity index 65% rename from src/img/theme/dark/icon/widget/plus.svg rename to src/img/icon/widget/plus1.svg index 264783962a..1170538750 100644 --- a/src/img/theme/dark/icon/widget/plus.svg +++ b/src/img/icon/widget/plus1.svg @@ -1,4 +1,4 @@ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="9.25" y="3" width="1.5" height="14" rx="0.75" fill="#dfddd3"/> -<path d="M16.25 9.25C16.6642 9.25 17 9.58579 17 10C17 10.4142 16.6642 10.75 16.25 10.75H3.75C3.33579 10.75 3 10.4142 3 10C3 9.58579 3.33579 9.25 3.75 9.25H16.25Z" fill="#dfddd3"/> +<rect x="9.25" y="3" width="1.5" height="14" rx="0.75" fill="#252525"/> +<path d="M16.25 9.25C16.6642 9.25 17 9.58579 17 10C17 10.4142 16.6642 10.75 16.25 10.75H3.75C3.33579 10.75 3 10.4142 3 10C3 9.58579 3.33579 9.25 3.75 9.25H16.25Z" fill="#252525"/> </svg> diff --git a/src/img/theme/dark/icon/chat/buttons/remove.svg b/src/img/theme/dark/icon/chat/buttons/remove.svg new file mode 100644 index 0000000000..ffe417bfb4 --- /dev/null +++ b/src/img/theme/dark/icon/chat/buttons/remove.svg @@ -0,0 +1,3 @@ +<svg width="8" height="8" viewBox="0 0 8 8" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path fill-rule="evenodd" clip-rule="evenodd" d="M6.63008 0.235269C6.94239 -0.0773428 7.44998 -0.0785774 7.76381 0.232511C8.07765 0.5436 8.07889 1.04921 7.76658 1.36182L5.13097 4L7.76658 6.63818C8.07889 6.95079 8.07765 7.4564 7.76381 7.76749C7.44998 8.07858 6.94239 8.07734 6.63008 7.76473L4 5.13208L1.36992 7.76473C1.05761 8.07734 0.550025 8.07858 0.236189 7.76749C-0.0776455 7.4564 -0.078885 6.95079 0.233421 6.63818L2.86903 4L0.233421 1.36182C-0.0788849 1.04921 -0.0776455 0.5436 0.23619 0.232511C0.550025 -0.0785774 1.05761 -0.0773428 1.36992 0.235269L4 2.86792L6.63008 0.235269Z" fill="#f8f8f8"/> +</svg> diff --git a/src/img/theme/dark/icon/menu/widget/compact.svg b/src/img/theme/dark/icon/menu/widget/compact.svg index b74d4b062e..8a1f907c20 100644 --- a/src/img/theme/dark/icon/menu/widget/compact.svg +++ b/src/img/theme/dark/icon/menu/widget/compact.svg @@ -1,15 +1,11 @@ -<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="1.4375" width="39" height="39" rx="5.5" stroke="#525148"/> -<rect x="10" y="7.25" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M6 8C6 7.44772 6.44772 7 7 7V7C7.55228 7 8 7.44772 8 8V8C8 8.55228 7.55228 9 7 9V9C6.44772 9 6 8.55228 6 8V8Z" fill="#B6B6B6"/> -<path d="M6 13C6 12.4477 6.44772 12 7 12V12C7.55228 12 8 12.4477 8 13V13C8 13.5523 7.55228 14 7 14V14C6.44772 14 6 13.5523 6 13V13Z" fill="#B6B6B6"/> -<path d="M10 13C10 12.5858 10.3358 12.25 10.75 12.25H27.25C27.6642 12.25 28 12.5858 28 13V13C28 13.4142 27.6642 13.75 27.25 13.75H10.75C10.3358 13.75 10 13.4142 10 13V13Z" fill="#B6B6B6"/> -<rect x="10" y="17.25" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M6 18C6 17.4477 6.44772 17 7 17V17C7.55228 17 8 17.4477 8 18V18C8 18.5523 7.55228 19 7 19V19C6.44772 19 6 18.5523 6 18V18Z" fill="#B6B6B6"/> -<path d="M6 23C6 22.4477 6.44772 22 7 22V22C7.55228 22 8 22.4477 8 23V23C8 23.5523 7.55228 24 7 24V24C6.44772 24 6 23.5523 6 23V23Z" fill="#B6B6B6"/> -<path d="M10 23C10 22.5858 10.3358 22.25 10.75 22.25H27.25C27.6642 22.25 28 22.5858 28 23V23C28 23.4142 27.6642 23.75 27.25 23.75H10.75C10.3358 23.75 10 23.4142 10 23V23Z" fill="#B6B6B6"/> -<path d="M6 28C6 27.4477 6.44772 27 7 27V27C7.55228 27 8 27.4477 8 28V28C8 28.5523 7.55228 29 7 29V29C6.44772 29 6 28.5523 6 28V28Z" fill="#B6B6B6"/> -<path d="M10 28C10 27.5858 10.3358 27.25 10.75 27.25H33.25C33.6642 27.25 34 27.5858 34 28V28C34 28.4142 33.6642 28.75 33.25 28.75H10.75C10.3358 28.75 10 28.4142 10 28V28Z" fill="#B6B6B6"/> -<path d="M6 33C6 32.4477 6.44772 32 7 32V32C7.55228 32 8 32.4477 8 33V33C8 33.5523 7.55228 34 7 34V34C6.44772 34 6 33.5523 6 33V33Z" fill="#B6B6B6"/> -<rect x="10" y="32.25" width="18" height="1.5" rx="0.75" fill="#B6B6B6"/> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="20" y="11.25" width="17" height="1.5" rx="0.75" fill="#737373"/> +<rect x="20" y="15.25" width="12" height="1.5" rx="0.75" fill="#737373"/> +<rect x="9" y="10" width="8" height="8" rx="2" fill="#313131"/> +<rect x="20" y="21.25" width="17" height="1.5" rx="0.75" fill="#737373"/> +<rect x="20" y="25.25" width="12" height="1.5" rx="0.75" fill="#737373"/> +<rect x="9" y="20" width="8" height="8" rx="2" fill="#313131"/> +<rect x="20" y="31.25" width="17" height="1.5" rx="0.75" fill="#737373"/> +<rect x="20" y="35.25" width="12" height="1.5" rx="0.75" fill="#737373"/> +<rect x="9" y="30" width="8" height="8" rx="2" fill="#313131"/> </svg> diff --git a/src/img/theme/dark/icon/menu/widget/link.svg b/src/img/theme/dark/icon/menu/widget/link.svg index 0495c3663c..7d088b5924 100644 --- a/src/img/theme/dark/icon/menu/widget/link.svg +++ b/src/img/theme/dark/icon/menu/widget/link.svg @@ -1,4 +1,4 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="12.4375" width="39" height="15" rx="3.5" stroke="#525148"/> -<rect x="6" y="19.25" width="28" height="1.5" rx="0.75" fill="#B6B6B6"/> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="8.5" y="19.5" width="31" height="9" rx="2.5" fill="#191919" stroke="#313131"/> +<rect x="12" y="23.5" width="24" height="1.5" rx="0.75" fill="#737373"/> </svg> diff --git a/src/img/theme/dark/icon/menu/widget/list.svg b/src/img/theme/dark/icon/menu/widget/list.svg index b86cb5362e..b1c609e605 100644 --- a/src/img/theme/dark/icon/menu/widget/list.svg +++ b/src/img/theme/dark/icon/menu/widget/list.svg @@ -1,19 +1,12 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3043_3792)"> -<rect x="0.5" y="0.4375" width="39" height="39" rx="5.5" stroke="#525148"/> -<rect x="17" y="7.25" width="17" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="17" y="11.25" width="12" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="6" width="8" height="8" rx="2" fill="#525148"/> -<rect x="17" y="17.25" width="17" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="17" y="21.25" width="12" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="16" width="8" height="8" rx="2" fill="#525148"/> -<rect x="17" y="27.25" width="17" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="17" y="31.25" width="12" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="26" width="8" height="8" rx="2" fill="#525148"/> -</g> -<defs> -<clipPath id="clip0_3043_3792"> -<rect width="40" height="40" fill="white"/> -</clipPath> -</defs> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="13.375" y="13.625" width="23.25" height="0.75" rx="0.375" stroke="#737373" stroke-width="0.75"/> +<path d="M9.5 14C9.5 13.7239 9.72386 13.5 10 13.5C10.2761 13.5 10.5 13.7239 10.5 14C10.5 14.2761 10.2761 14.5 10 14.5C9.72386 14.5 9.5 14.2761 9.5 14Z" stroke="#737373"/> +<path d="M9.5 19C9.5 18.7239 9.72386 18.5 10 18.5C10.2761 18.5 10.5 18.7239 10.5 19C10.5 19.2761 10.2761 19.5 10 19.5C9.72386 19.5 9.5 19.2761 9.5 19Z" stroke="#737373"/> +<path d="M13.5 19C13.5 18.8619 13.6119 18.75 13.75 18.75H30.25C30.3881 18.75 30.5 18.8619 30.5 19C30.5 19.1381 30.3881 19.25 30.25 19.25H13.75C13.6119 19.25 13.5 19.1381 13.5 19Z" stroke="#737373"/> +<rect x="13.375" y="23.625" width="23.25" height="0.75" rx="0.375" stroke="#737373" stroke-width="0.75"/> +<path d="M9.5 24C9.5 23.7239 9.72386 23.5 10 23.5C10.2761 23.5 10.5 23.7239 10.5 24C10.5 24.2761 10.2761 24.5 10 24.5C9.72386 24.5 9.5 24.2761 9.5 24Z" stroke="#737373"/> +<path d="M9.5 29C9.5 28.7239 9.72386 28.5 10 28.5C10.2761 28.5 10.5 28.7239 10.5 29C10.5 29.2761 10.2761 29.5 10 29.5C9.72386 29.5 9.5 29.2761 9.5 29Z" stroke="#737373"/> +<path d="M13.5 29C13.5 28.8619 13.6119 28.75 13.75 28.75H30.25C30.3881 28.75 30.5 28.8619 30.5 29C30.5 29.1381 30.3881 29.25 30.25 29.25H13.75C13.6119 29.25 13.5 29.1381 13.5 29Z" stroke="#737373"/> +<path d="M9.5 34C9.5 33.7239 9.72386 33.5 10 33.5C10.2761 33.5 10.5 33.7239 10.5 34C10.5 34.2761 10.2761 34.5 10 34.5C9.72386 34.5 9.5 34.2761 9.5 34Z" stroke="#737373"/> +<path d="M13.5 34C13.5 33.8619 13.6119 33.75 13.75 33.75H36.25C36.3881 33.75 36.5 33.8619 36.5 34C36.5 34.1381 36.3881 34.25 36.25 34.25H13.75C13.6119 34.25 13.5 34.1381 13.5 34Z" stroke="#737373"/> </svg> diff --git a/src/img/theme/dark/icon/menu/widget/tree.svg b/src/img/theme/dark/icon/menu/widget/tree.svg index 6ab682215b..d124ce5111 100644 --- a/src/img/theme/dark/icon/menu/widget/tree.svg +++ b/src/img/theme/dark/icon/menu/widget/tree.svg @@ -1,22 +1,12 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0_3043_3806)"> -<rect x="0.5" y="0.4375" width="39" height="39" rx="5.5" stroke="#525148"/> -<rect x="10" y="6.25" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M6 7C6 6.44772 6.44772 6 7 6C7.55228 6 8 6.44772 8 7C8 7.55228 7.55228 8 7 8C6.44772 8 6 7.55228 6 7Z" fill="#B6B6B6"/> -<path d="M6 12C6 11.4477 6.44772 11 7 11C7.55228 11 8 11.4477 8 12C8 12.5523 7.55228 13 7 13C6.44772 13 6 12.5523 6 12Z" fill="#B6B6B6"/> -<path d="M10 17C10 16.4477 10.4477 16 11 16C11.5523 16 12 16.4477 12 17C12 17.5523 11.5523 18 11 18C10.4477 18 10 17.5523 10 17Z" fill="#B6B6B6"/> -<path d="M14 22C14 21.4477 14.4477 21 15 21C15.5523 21 16 21.4477 16 22C16 22.5523 15.5523 23 15 23C14.4477 23 14 22.5523 14 22Z" fill="#B6B6B6"/> -<path d="M10 12C10 11.5858 10.3358 11.25 10.75 11.25H33.25C33.6642 11.25 34 11.5858 34 12C34 12.4142 33.6642 12.75 33.25 12.75H10.75C10.3358 12.75 10 12.4142 10 12Z" fill="#B6B6B6"/> -<rect x="14" y="16.25" width="20" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="21.25" width="16" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M14 27C14 26.4477 14.4477 26 15 26C15.5523 26 16 26.4477 16 27C16 27.5523 15.5523 28 15 28C14.4477 28 14 27.5523 14 27Z" fill="#B6B6B6"/> -<rect x="18" y="26.25" width="16" height="1.5" rx="0.75" fill="#B6B6B6"/> -<path d="M6 32C6 31.4477 6.44772 31 7 31C7.55228 31 8 31.4477 8 32C8 32.5523 7.55228 33 7 33C6.44772 33 6 32.5523 6 32Z" fill="#B6B6B6"/> -<rect x="10" y="31.25" width="24" height="1.5" rx="0.75" fill="#B6B6B6"/> -</g> -<defs> -<clipPath id="clip0_3043_3806"> -<rect width="40" height="40" fill="white"/> -</clipPath> -</defs> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M10 14C10 13.4477 10.4477 13 11 13C11.5523 13 12 13.4477 12 14C12 14.5523 11.5523 15 11 15C10.4477 15 10 14.5523 10 14Z" fill="#737373"/> +<path d="M14 19C14 18.4477 14.4477 18 15 18C15.5523 18 16 18.4477 16 19C16 19.5523 15.5523 20 15 20C14.4477 20 14 19.5523 14 19Z" fill="#737373"/> +<path d="M18 24C18 23.4477 18.4477 23 19 23C19.5523 23 20 23.4477 20 24C20 24.5523 19.5523 25 19 25C18.4477 25 18 24.5523 18 24Z" fill="#737373"/> +<path d="M14 14C14 13.5858 14.3358 13.25 14.75 13.25H37.25C37.6642 13.25 38 13.5858 38 14C38 14.4142 37.6642 14.75 37.25 14.75H14.75C14.3358 14.75 14 14.4142 14 14Z" fill="#737373"/> +<path d="M37.25 18.25H18.75C18.3358 18.25 18 18.5858 18 19C18 19.4142 18.3358 19.75 18.75 19.75H37.25C37.6642 19.75 38 19.4142 38 19C38 18.5858 37.6642 18.25 37.25 18.25Z" fill="#737373"/> +<path d="M37.25 23.25H22.75C22.3358 23.25 22 23.5858 22 24C22 24.4142 22.3358 24.75 22.75 24.75H37.25C37.6642 24.75 38 24.4142 38 24C38 23.5858 37.6642 23.25 37.25 23.25Z" fill="#737373"/> +<path d="M18 29C18 28.4477 18.4477 28 19 28C19.5523 28 20 28.4477 20 29C20 29.5523 19.5523 30 19 30C18.4477 30 18 29.5523 18 29Z" fill="#737373"/> +<path d="M37.25 28.25H22.75C22.3358 28.25 22 28.5858 22 29C22 29.4142 22.3358 29.75 22.75 29.75H37.25C37.6642 29.75 38 29.4142 38 29C38 28.5858 37.6642 28.25 37.25 28.25Z" fill="#737373"/> +<path d="M10 34C10 33.4477 10.4477 33 11 33C11.5523 33 12 33.4477 12 34C12 34.5523 11.5523 35 11 35C10.4477 35 10 34.5523 10 34Z" fill="#737373"/> +<path d="M37.25 33.25H14.75C14.3358 33.25 14 33.5858 14 34C14 34.4142 14.3358 34.75 14.75 34.75H37.25C37.6642 34.75 38 34.4142 38 34C38 33.5858 37.6642 33.25 37.25 33.25Z" fill="#737373"/> </svg> diff --git a/src/img/theme/dark/icon/menu/widget/view.svg b/src/img/theme/dark/icon/menu/widget/view.svg index cda70018a4..e50dcdc507 100644 --- a/src/img/theme/dark/icon/menu/widget/view.svg +++ b/src/img/theme/dark/icon/menu/widget/view.svg @@ -1,28 +1,10 @@ -<svg width="40" height="41" viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg"> -<rect x="0.5" y="1.4375" width="39" height="39" rx="5.5" stroke="#525148"/> -<rect x="6" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="31.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="23.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="27.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="19.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="12" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="18" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="24" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="15.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="6" y="9.25" width="10" height="1.5" rx="0.75" fill="#B6B6B6"/> -<rect x="30" y="9.25" width="4" height="1.5" rx="0.75" fill="#B6B6B6"/> +<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="9.5" y="14.5" width="13" height="19" rx="1.5" fill="#191919" stroke="#313131"/> +<rect x="25.5" y="14.5" width="13" height="19" rx="1.5" fill="#191919" stroke="#313131"/> +<path d="M9 16C9 14.8954 9.89543 14 11 14H21C22.1046 14 23 14.8954 23 16V24H9V16Z" fill="#313131"/> +<path d="M25 16C25 14.8954 25.8954 14 27 14H37C38.1046 14 39 14.8954 39 16V24H25V16Z" fill="#313131"/> +<rect x="12" y="26.25" width="8" height="1.5" rx="0.75" fill="#737373"/> +<rect x="12" y="29.25" width="8" height="1.5" rx="0.75" fill="#737373"/> +<rect x="28" y="26.25" width="8" height="1.5" rx="0.75" fill="#737373"/> +<rect x="28" y="29.25" width="8" height="1.5" rx="0.75" fill="#737373"/> </svg> diff --git a/src/img/theme/dark/icon/widget/plus0.svg b/src/img/theme/dark/icon/widget/plus0.svg new file mode 100644 index 0000000000..f7757402b8 --- /dev/null +++ b/src/img/theme/dark/icon/widget/plus0.svg @@ -0,0 +1,4 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="9.25" y="3" width="1.5" height="14" rx="0.75" fill="#9A9A9A"/> +<path d="M16.25 9.25C16.6642 9.25 17 9.58579 17 10C17 10.4142 16.6642 10.75 16.25 10.75H3.75C3.33579 10.75 3 10.4142 3 10C3 9.58579 3.33579 9.25 3.75 9.25H16.25Z" fill="#9A9A9A"/> +</svg> diff --git a/src/img/theme/dark/icon/widget/plus1.svg b/src/img/theme/dark/icon/widget/plus1.svg new file mode 100644 index 0000000000..802434da5f --- /dev/null +++ b/src/img/theme/dark/icon/widget/plus1.svg @@ -0,0 +1,4 @@ +<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> +<rect x="9.25" y="3" width="1.5" height="14" rx="0.75" fill="#D4D4D4"/> +<path d="M16.25 9.25C16.6642 9.25 17 9.58579 17 10C17 10.4142 16.6642 10.75 16.25 10.75H3.75C3.33579 10.75 3 10.4142 3 10C3 9.58579 3.33579 9.25 3.75 9.25H16.25Z" fill="#D4D4D4"/> +</svg> diff --git a/src/json/constant.ts b/src/json/constant.ts index 84e4728e06..b417ffe1a0 100644 --- a/src/json/constant.ts +++ b/src/json/constant.ts @@ -6,7 +6,6 @@ export default { appName: 'Anytype', blankRouteId: '_blank_', storeSpaceId: '_anytype_marketplace', - localLoversSpaceId: 'bafyreigyfkt6rbv24sbv5aq2hko3bhmv5xxlf22b4bypdu6j7hnphm3psq.23me69r569oi1', anytypeProfileId: '_anytype_profile', fontCode: 'plex', popupPinIds: [ 'search' ], @@ -26,6 +25,10 @@ export default { testing: 'N4N1wDHFpFpovXBqdbq2TDXE9tXdXbtV1eTJFpKJW4YeaJqR' }, + chatSpaceId: [ + 'bafyreiezhzb4ggnhjwejmh67pd5grilk6jn3jt7y2rnfpbkjwekilreola.1t123w9f2lgn5', + ], + platforms: { win32: 'Windows', darwin: 'Mac', @@ -38,16 +41,17 @@ export default { notification: 20, space: 50, graphDepth: 5, + listObject: 50, chat: { - messages: 30, + messages: 50, attachments: 10, files: 10, mentions: 10, text: 2000, reactions: { - self: 10, - all: 20, + self: 3, + all: 12, }, } }, diff --git a/src/json/error.ts b/src/json/error.ts index 3fb9825922..4f6ee4ba86 100644 --- a/src/json/error.ts +++ b/src/json/error.ts @@ -13,6 +13,7 @@ export default { LIMIT_OF_ROWS_OR_RELATIONS_EXCEEDED: 7, FILE_LOAD_ERROR: 8, INSUFFICIENT_PERMISSIONS: 9, + ACCOUNT_STORE_NOT_MIGRATED: 113, Import: { INTERNAL_ERROR: 3, diff --git a/src/json/index.ts b/src/json/index.ts index f0b589cd14..219b0a93e7 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -9,6 +9,7 @@ import Lang from './lang'; import Relation from './relation'; import Menu from './menu'; import Size from './size'; +import Shortcut from './shortcut'; const Emoji = require('./emoji.json'); const Latex = require('./latex.json'); @@ -27,4 +28,5 @@ export { Relation, Menu, Size, + Shortcut, }; \ No newline at end of file diff --git a/src/json/menu.ts b/src/json/menu.ts index 158d4c20dc..be6e52581f 100644 --- a/src/json/menu.ts +++ b/src/json/menu.ts @@ -33,7 +33,12 @@ export default { 'typeSuggest', 'blockAlign' ], - add: [ 'searchObject', 'blockRelationEdit', 'typeSuggest' ], + add: [ + 'searchObject', + 'blockRelationEdit', + 'typeSuggest', + 'dataviewCalendar', + ], action: [ 'blockStyle', 'blockColor', @@ -48,7 +53,6 @@ export default { relationEdit: [ 'select', 'searchObject', - 'dataviewDate', 'dataviewObjectValues', 'dataviewObjectList' ], @@ -61,6 +65,5 @@ export default { widget: [ 'searchObject', 'select' ], dataviewTemplate: [ 'previewObject' ], table: [ 'select2', 'blockColor', 'blockBackground' ], - navigation: [ 'quickCapture', 'space' ], dataviewContext: [ 'typeSuggest', 'searchObject' ] }; \ No newline at end of file diff --git a/src/json/relation.ts b/src/json/relation.ts index bdfd56a298..f5ffaccce1 100644 --- a/src/json/relation.ts +++ b/src/json/relation.ts @@ -73,7 +73,7 @@ export default { 'relationDefaultValue', 'relationFormatObjectTypes', 'sourceObject', - 'restrictions' + 'restrictions', ], cover: [ @@ -118,7 +118,8 @@ export default { 'isDeleted', 'isArchived', 'isFavorite', - 'restrictions' + 'restrictions', + 'timestamp', ], template: [ @@ -156,4 +157,8 @@ export default { ], pageCover: 'pageCover', + + key: { + mention: 'mentions', + } }; diff --git a/src/json/route.ts b/src/json/route.ts index 12e8f2d383..50941c85d5 100644 --- a/src/json/route.ts +++ b/src/json/route.ts @@ -4,6 +4,8 @@ export default [ '/:page/:action/:id?', '/:page/:action/:id?/spaceId/:spaceId?', '/:page/:action/:id?/spaceId/:spaceId?/viewId/:viewId?', + '/:page/:action/:id?/spaceId/:spaceId?/relationKey/:relationKey?', + '/:page/:action/:id?/spaceId/:spaceId?/viewId/:viewId?/relationKey/:relationKey?', '/object', '/invite', '/membership' diff --git a/src/json/shortcut.ts b/src/json/shortcut.ts new file mode 100644 index 0000000000..aada98e594 --- /dev/null +++ b/src/json/shortcut.ts @@ -0,0 +1,226 @@ +import { U, translate, keyboard } from 'Lib'; + +export default () => { + const cmd = keyboard.cmdSymbol(); + const alt = keyboard.altSymbol(); + + return [ + { + id: 'main', + name: translate('popupShortcutMain'), + children: [ + { + name: translate('popupShortcutBasics'), children: [ + { com: `${cmd} + N`, name: translate('popupShortcutMainBasics1') }, + { com: `${cmd} + ${alt} + N`, name: translate('popupShortcutMainBasics19') }, + { com: `${cmd} + Shift + N`, name: translate('popupShortcutMainBasics2') }, + { com: `${cmd} + Enter`, name: translate('popupShortcutMainBasics4') }, + { mac: `${cmd} + Ctrl + F`, com: `${cmd} + ${alt} + F`, name: translate('popupShortcutMainBasics5') }, + { com: `${cmd} + Z`, name: translate('popupShortcutMainBasics6') }, + { com: `${cmd} + Shift + Z`, name: translate('popupShortcutMainBasics7') }, + { com: `${cmd} + P`, name: translate('popupShortcutMainBasics8') }, + { com: `${cmd} + F`, name: translate('popupShortcutMainBasics9') }, + { com: `${cmd} + Q`, name: translate('popupShortcutMainBasics10') }, + { mac: `${cmd} + Y`, com: 'Ctrl + H', name: translate('popupShortcutMainBasics11') }, + { com: 'Shift + Click', name: translate('popupShortcutMainBasics12') }, + { com: `${cmd} + Click`, name: translate('popupShortcutMainBasics13') }, + { com: 'Ctrl + Space', name: translate('popupShortcutMainBasics14') }, + { com: `${cmd} + \\, ${cmd} + .`, name: translate('popupShortcutMainBasics15') }, + { com: `${cmd} + =`, name: translate('popupShortcutMainBasics16') }, + { com: `${cmd} + Minus`, name: translate('popupShortcutMainBasics17') }, + { com: `${cmd} + 0`, name: translate('popupShortcutMainBasics18') }, + { com: `Ctrl + Tab, Ctrl + Shift + Tab`, name: translate('popupShortcutMainBasics20') }, + { com: `${cmd} + Shift + M`, name: translate('popupShortcutMainBasics21') }, + { com: `${cmd} + ${alt} + L`, name: translate('popupShortcutMainBasics22') }, + ] + }, + + { + name: translate('popupShortcutMainStructuring'), children: [ + { com: 'Enter', name: translate('popupShortcutMainStructuring1') }, + { com: 'Shift + Enter', name: translate('popupShortcutMainStructuring2') }, + { com: 'Delete', name: translate('popupShortcutMainStructuring3') }, + { com: 'Tab', name: translate('popupShortcutMainStructuring4') }, + { com: 'Shift + Tab', name: translate('popupShortcutMainStructuring5') }, + ] + }, + + { + name: translate('popupShortcutMainSelection'), children: [ + { com: 'Double Click', name: translate('popupShortcutMainSelection1') }, + { com: 'Triple Click', name: translate('popupShortcutMainSelection2') }, + { com: `${cmd} + A`, name: translate('popupShortcutMainSelection3') }, + { com: 'Shift + ↑ or ↓', name: translate('popupShortcutMainSelection4') }, + { com: `${cmd} + Click`, name: translate('popupShortcutMainSelection5') }, + { com: 'Shift + Click', name: translate('popupShortcutMainSelection6') }, + ] + }, + + { + name: translate('commonActions'), children: [ + { com: '/', name: translate('popupShortcutMainActions1') }, + { com: `${cmd} + /`, name: translate('popupShortcutMainActions2') }, + { mac: `${cmd} + Delete`, com: 'Ctrl + Backspace', name: translate('popupShortcutMainActions3') }, + { com: `${cmd} + C`, name: translate('popupShortcutMainActions4') }, + { com: `${cmd} + X`, name: translate('popupShortcutMainActions5') }, + { com: `${cmd} + V`, name: translate('popupShortcutMainActions6') }, + { com: `${cmd} + D`, name: translate('popupShortcutMainActions7') }, + { com: `${cmd} + E`, name: translate('popupShortcutMainActions8') + ' 🏄‍♂' }, + ] + }, + + { + name: translate('popupShortcutMainTextStyle'), children: [ + { com: `${cmd} + B`, name: translate('popupShortcutMainTextStyle1') }, + { com: `${cmd} + I`, name: translate('popupShortcutMainTextStyle2') }, + { com: `${cmd} + U`, name: translate('popupShortcutMainTextStyle3') }, + { com: `${cmd} + Shift +S`, name: translate('popupShortcutMainTextStyle4') }, + { com: `${cmd} + K`, name: translate('popupShortcutMainTextStyle5') }, + { com: `${cmd} + L`, name: translate('popupShortcutMainTextStyle6') }, + { com: `${cmd} + Shift + C`, name: translate('popupShortcutMainTextStyle7') }, + { com: `${cmd} + Shift + H`, name: translate('popupShortcutMainTextStyle8') }, + ] + }, + ], + }, + + { + id: 'navigation', + name: translate('popupShortcutNavigation'), + children: [ + { + name: translate('popupShortcutBasics'), children: [ + { com: `${cmd} + ,(comma)`, name: translate('popupShortcutNavigationBasics1') }, + { com: `${cmd} + O`, name: translate('popupShortcutNavigationBasics2') }, + { com: `${cmd} + ${alt} + O`, name: translate('popupShortcutNavigationBasics3') }, + { com: `${cmd} + S, ${cmd} + K`, name: translate('popupShortcutNavigationBasics4') }, + { com: `${alt} + H`, name: translate('popupShortcutNavigationBasics6') }, + { mac: `${cmd} + [, ${cmd} + ←`, com: 'Alt + ←', name: translate('popupShortcutNavigationBasics7') }, + { mac: `${cmd} + ], ${cmd} + →`, com: 'Alt + →', name: translate('popupShortcutNavigationBasics8') }, + ] + }, + + { + name: translate('popupShortcutNavigationMenu'), children: [ + { com: '↓ or Tab', name: translate('popupShortcutNavigationMenu1') }, + { com: '↑ or Shift + Tab', name: translate('popupShortcutNavigationMenu2') }, + { com: '←', name: translate('popupShortcutNavigationMenu3') }, + { com: '→', name: translate('popupShortcutNavigationMenu4') }, + { com: 'Enter', name: translate('popupShortcutNavigationMenu5') }, + ] + }, + + { + name: translate('popupShortcutNavigationPage'), children: [ + { com: `${cmd} + Shift + T`, name: translate('popupShortcutNavigationPage1') }, + { com: '↓', name: translate('popupShortcutNavigationPage2') }, + { com: '↑', name: translate('popupShortcutNavigationPage3') }, + { com: `${cmd} + ←`, name: translate('popupShortcutNavigationPage4') }, + { com: `${cmd} + →`, name: translate('popupShortcutNavigationPage5') }, + { com: `${cmd} + ↑`, name: translate('popupShortcutNavigationPage6') }, + { com: `${cmd} + ↓`, name: translate('popupShortcutNavigationPage7') }, + { com: `${cmd} + Shift + ↑↓`, name: translate('popupShortcutNavigationPage8') }, + { com: `${cmd} + Shift + R`, name: translate('popupShortcutNavigationPage9') }, + { com: `${cmd} + Enter`, name: translate('popupShortcutNavigationPage10') }, + ] + }, + ], + }, + + { + id: 'markdown', + name: translate('popupShortcutMarkdown'), + children: [ + { + name: translate('popupShortcutMarkdownWhileTyping'), + children: [ + { com: '`', name: translate('popupShortcutMarkdownWhileTyping1') }, + { com: '** or __', name: translate('popupShortcutMarkdownWhileTyping2') }, + { com: '* or _', name: translate('popupShortcutMarkdownWhileTyping3') }, + { com: '~~', name: translate('popupShortcutMarkdownWhileTyping4') }, + { com: '-->', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '⟶') }, + { com: '<--', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '⟵') }, + { com: '<-->', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '⟷') }, + { com: '->', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '→') }, + { com: '<-', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '←') }, + { com: '--', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '—') }, + { com: '(r)', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '®') }, + { com: '(tm)', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '™') }, + { com: '...', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '…') }, + ] + }, + { + name: translate('popupShortcutMarkdownBeginningOfLine'), + children: [ + { com: '# + Space', name: translate('popupShortcutMarkdownBeginningOfLine1') }, + { com: '# # + Space', name: translate('popupShortcutMarkdownBeginningOfLine2') }, + { com: '# # # + Space', name: translate('popupShortcutMarkdownBeginningOfLine3') }, + { com: '" + Space', name: translate('popupShortcutMarkdownBeginningOfLine4') }, + { com: '* or + or - and Space', name: translate('popupShortcutMarkdownBeginningOfLine5') }, + { com: '[] + Space', name: translate('popupShortcutMarkdownBeginningOfLine6') }, + { com: '1. + Space', name: translate('popupShortcutMarkdownBeginningOfLine7') }, + { com: '> + Space', name: translate('popupShortcutMarkdownBeginningOfLine8') }, + { com: '``` + Space', name: translate('popupShortcutMarkdownBeginningOfLine9') }, + { com: '--- + Space', name: translate('popupShortcutMarkdownBeginningOfLine10') }, + { com: '*** + Space', name: translate('popupShortcutMarkdownBeginningOfLine11') }, + ] + }, + ], + }, + + { + id: 'command', + name: translate('popupShortcutCommand'), + children: [ + { + name: translate('commonMenu'), children: [ + { com: '/', name: translate('popupShortcutCommandMenu1') }, + { com: '↓ & ↑', name: translate('popupShortcutCommandMenu2') }, + { com: '→ & ←', name: translate('popupShortcutCommandMenu3') }, + { com: 'Esc or Clear /', name: translate('popupShortcutCommandMenu4') }, + ] + }, + + { description: translate('popupShortcutCommandDescription'), children: [], className: 'separator' }, + { + name: translate('popupShortcutCommandText'), children: [ + { com: '/text', name: translate('popupShortcutCommandText1') }, + { com: '/h1', name: translate('popupShortcutCommandText2') }, + { com: '/h2', name: translate('popupShortcutCommandText3') }, + { com: '/h3', name: translate('popupShortcutCommandText4') }, + { com: '/high', name: translate('popupShortcutCommandText5') }, + ] + }, + + { + name: translate('popupShortcutCommandLists'), children: [ + { com: '/todo', name: translate('popupShortcutCommandLists1') }, + { com: '/bullet', name: translate('popupShortcutCommandLists2') }, + { com: '/num', name: translate('popupShortcutCommandLists3') }, + { com: '/toggle', name: translate('popupShortcutCommandLists4') }, + ] + }, + + { + name: translate('popupShortcutCommandObjects'), children: [ + { com: '@today, @tomorrow', name: translate('popupShortcutCommandObjects1') }, + { com: '/page', name: translate('popupShortcutCommandObjects2') }, + { com: '/file', name: translate('popupShortcutCommandObjects3') }, + { com: '/image', name: translate('popupShortcutCommandObjects4') }, + { com: '/video', name: translate('popupShortcutCommandObjects5') }, + { com: '/bookmark', name: translate('popupShortcutCommandObjects6') }, + { com: '/link', name: translate('popupShortcutCommandObjects7') }, + ] + }, + + { + name: translate('popupShortcutCommandOther'), children: [ + { com: '/line', name: translate('popupShortcutCommandOther1') }, + { com: '/dots', name: translate('popupShortcutCommandOther2') }, + { com: '/code', name: translate('popupShortcutCommandOther3') }, + ] + }, + ], + }, + ]; +}; \ No newline at end of file diff --git a/src/json/text.json b/src/json/text.json index c177d956a1..f0de211efe 100644 --- a/src/json/text.json +++ b/src/json/text.json @@ -33,6 +33,7 @@ "commonUncategorized": "Uncategorized", "commonDashboard": "Dashboard", "commonGraph": "Graph", + "commonChat": "Chat", "commonFlow": "Flow", "commonLibrary": "Library", "commonAllContent": "All Objects", @@ -101,6 +102,7 @@ "commonMoveTo": "Move to", "commonLinkTo": "Add Link to Object", "commonUpload": "Upload", + "commonLoad": "Load", "commonDownload": "Download", "commonAscending": "Ascending", "commonDescending": "Descending", @@ -173,6 +175,8 @@ "commonMenu": "Menu", "commonSignUp": "Sign Up", "commonNotFound": "Not found", + "commonCalculate": "Calculate", + "commonRelations": "Relations", "pluralDay": "day|days", "pluralObject": "Object|Objects", @@ -217,11 +221,13 @@ "electronMenuShowMenu": "Show menu bar", "electronMenuQuit": "Quit", "electronMenuFile": "File", - "electronMenuDirectory": "Show Directory", - "electronMenuWorkDirectory": "Work", - "electronMenuDataDirectory": "Data", - "electronMenuConfigDirectory": "Config", - "electronMenuLogsDirectory": "Logs", + "electronMenuOpen": "Open", + "electronMenuApplyCustomCss": "Apply custom CSS", + "electronMenuWorkDirectory": "Work directory", + "electronMenuDataDirectory": "Data directory", + "electronMenuConfigDirectory": "Config directory", + "electronMenuLogsDirectory": "Logs directory", + "electronMenuCustomCss": "Custom CSS", "electronMenuImport": "Import to Space", "electronMenuExport": "Export Space", "electronMenuSaveAs": "Export Object", @@ -233,6 +239,7 @@ "electronMenuDebugStat": "Statistics", "electronMenuDebugReconcile": "Reconcile", "electronMenuDebugNet": "Network", + "electronMenuDebugLog": "Export log", "electronMenuClose": "Close Window", "electronMenuEdit": "Edit", "electronMenuUndo": "Undo", @@ -268,7 +275,7 @@ "electronMenuFlags": "Flags", "electronMenuDevTools": "Dev Tools", "electronMenuVersion": "Version", - "electronMenuOpen": "Open Anytype", + "electronMenuOpenApp": "Open Anytype", "electronMenuLanguage": "Interface Language", "electronMenuFlagAnalytics": "Analytics", "electronMenuFlagMw": "Middleware", @@ -333,7 +340,7 @@ "progressSaveFile": "Download in progress...", "progressMigration": "Recovering vault...", "progressRecoverAccount": "Migration in progress...", - "progressUpdateCheck": "Update...", + "progressUpdate": "Update...", "progressUpdateCheck": "Checking for update...", "spellcheckAdd": "Add to dictionary", @@ -480,6 +487,8 @@ "pageMainVoidText": "Looks like you’ve cleaned the house. Ready to start fresh?<br/>Create a new space to get things rolling!", "pageMainVoidCreateSpace": "Create space", + "pageMainDateEmptyText" : "There is nothing here for this date yet", + "pageAuthLoginInvalidPhrase": "Invalid Key", "pageAuthLoginShortPhrase": "Key is too short", @@ -542,6 +551,7 @@ "blockNameTableOfContents": "Table of Contents", "blockNameSet": "Inline set", "blockNameCollection": "Inline Collection", + "blockNameDate": "Date", "blockTextParagraph": "Write a plain text", "blockTextHeader1": "Big headline of a chapter, paragraph or section", @@ -576,6 +586,7 @@ "blockTextWrap": "Wrap", "blockTextUnwrap": "Unwrap", "blockTextPlain": "Plain Text", + "blockTextDate": "Mention a date in editor", "blockLinkSyncing": "Syncing...", "blockLinkArchived": "Deleted", @@ -787,7 +798,7 @@ "popupSettingsSpaceIndexSpaceTypePersonalTooltipText": "This space was created with end-to-end encryption, ensuring the privacy and security of user data and allowing for confident online or offline interaction.", "popupSettingsSpaceIndexInfoLabel": "Encrypted & local-first", - "popupSettingsSpaceIndexShareShareTitle": "Share space", + "popupSettingsSpaceIndexShareShareTitle": "Share Space", "popupSettingsSpaceIndexShareShareText": "Generate invite link and share the space with other members", "popupSettingsSpaceIndexShareDefaultText": "Entry space can’t be shared. Create a new one to share", "popupSettingsSpaceIndexShareInviteText": "Generate invite link and share the space with other members", @@ -1084,6 +1095,9 @@ "popupConfirmChatDeleteMessageTitle": "Delete this message?", "popupConfirmChatDeleteMessageText": "It cannot be restored after confirmation", + "popupConfirmSpeedLimitTitle": "Hold up! Turbo typing detected!", + "popupConfirmSpeedLimitText": "Looks like you're sending messages at lightning speed. Give it a sec before your next one.", + "popupInviteRequestTitle": "Join a space", "popupInviteRequestText": "You've been invited to join <b>%s</b> space, created by <b>%s</b>. Send a request so space owner can let you in.", "popupInviteRequestMessagePlaceholder": "Leave a private comment for a space owner", @@ -1172,6 +1186,7 @@ "popupShortcutMainBasics19": "Open the Quick Capture menu", "popupShortcutMainBasics20": "Switch to the next/previous Space", "popupShortcutMainBasics21": "Switch color mode (light/dark)", + "popupShortcutMainBasics22": "Lock the app with a PIN code", "popupShortcutMainStructuring": "Structuring", "popupShortcutMainStructuring1": "Create a new text block", "popupShortcutMainStructuring2": "Create a line break within a block of text", @@ -1430,11 +1445,6 @@ "menuDataviewCreateSomethingWentWrong": "Oops - something went wrong!", - "menuDataviewDateDateFormat": "Date format", - "menuDataviewDateTimeFormat": "Time format", - "menuDataviewDate12Hour": "12 hour", - "menuDataviewDate24Hour": "24 hour", - "menuDataviewFileValuesFindAFile": "Find a file...", "menuDataviewFilterValuesChecked": "Checked", @@ -1465,7 +1475,6 @@ "menuDataviewRelationEditInsertLeft": "Insert left", "menuDataviewRelationEditInsertRight": "Insert right", "menuDataviewRelationEditHideRelation": "Hide Relation", - "menuDataviewRelationEditCalculate": "Calculate", "menuDataviewRelationEditAddObjectType": "Add Object type", "menuDataviewRelationEditFilterObjectTypes": "Filter Object types...", "menuDataviewRelationEditToastOnCreate": "Relation <b>%s</b> has been created", @@ -1557,9 +1566,7 @@ "menuTypeSuggestCreateType": "Create type %s", "menuWidgetAddWidget": "Add Widget", - "menuWidgetChooseSource": "Choose a source", "menuWidgetWidgetType": "Widget type", - "menuWidgetWidgetSource": "Widget Source", "menuWidgetNumberOfObjects": "Number of Objects", "menuWidgetRemoveWidget": "Remove Widget", "menuWidgetEditWidgets": "Edit Widgets", @@ -1616,6 +1623,10 @@ "menuSyncStatusEmptyLocal": "Your Vault is currently in local-only mode, so your data isn't being synced to Anytype nodes.", "menuSyncStatusEmpty": "There are no objects to show", + "menuPublishTitle": "Publish to web", + "menuPublishLabel": "Join Space Button", + "menuPublishButton": "Publish", + "previewEdit": "Edit Link", "previewObjectTemplateIsBundled": "Template is bundled", @@ -1668,6 +1679,8 @@ "toastFileLimitReached": "Your local storage exceeds syncing limit. Locally stored files won't be synced", + "toastChatAttachmentsLimitReached": "You can upload only %s %s at a time", + "textColor-grey": "Grey", "textColor-yellow": "Yellow", "textColor-orange": "Amber", @@ -1781,8 +1794,6 @@ "phrasePlaceholder": "Please enter your Key", - "navigationAccount": "Spaces", - "spaceAccessType0": "Private Space", "spaceAccessType1": "Entry Space", "spaceAccessType2": "Shared Space", @@ -1900,7 +1911,7 @@ "onboardingTemplateSelectDescription": "Easily switch your templates when creating objects.", "onboardingSpacesTitle": "Spaces", - "onboardingSpacesText": "<b>This is your Space.</b> Click here to customize its name or icon, or to securely migrate and protect your data. ", + "onboardingSpacesText": "<b>This is your Space.</b> Click here to share it with others, customize its name and icon, or securely migrate from other apps.", "onboardingAllObjectTitle": "All Objects", "onboardingAllObjectText": "<b>Everything in Anytype is an Object.</b> All objects is where all your content lives.", "onboardingWidgetsTitle": "Widgets", @@ -1931,7 +1942,6 @@ "onboardingCollaborationTitle": "Introducing the local-first collaboration", "onboardingCollaborationText": "Check out the new Collaboration category to explore experiences that help you co-create, share and collaborate.", - "libDataviewRelations": "Relations", "libDataviewGroups": "Groups", "libDataviewView": "View", @@ -1960,7 +1970,7 @@ "libMenuCopyUrl": "Copy URL", "libActionUninstallTypeTitle": "Do you want to remove Type \"%s\"?", - "libActionUninstallTypeText": "This Type and any associated Templates will be removed. If you have created any Objects with this Type, they may become more difficult to locate.", + "libActionUninstallTypeText": "This Type will be removed. If you have created any Objects with this Type, they may become more difficult to locate.", "libActionUninstallRelationTitle": "Do you want to remove Relation \"%s\"?", "libActionUninstallRelationText": "This Relation will be removed from your Library. If you have created any Objects with which use this Relation, you will no longer be able to edit the Relation value.", @@ -2233,11 +2243,12 @@ "networkMode2Title": "Self-hosted", "networkMode2Text": "Back up to your self-hosted network", - "formulaNone": "Calculate", "formulaCount": "Count", "formulaPercentage": "Percentage", "formulaMath": "Math", "formulaDate": "Date", + "formulaValue": "Count values", + "formulaValueShort": "Values", "formulaDistinct": "Count unique values", "formulaDistinctShort": "Unique", "formulaEmpty": "Count empty", @@ -2264,6 +2275,9 @@ "formulaCheckboxNotEmpty": "Count checked", "formulaCheckboxNotEmptyShort": "Checked", "formulaCheckboxPercentEmpty": "Percentage unchecked", - "formulaCheckboxPercentNotEmpty": "Percentage checked" + "formulaCheckboxPercentNotEmpty": "Percentage checked", + + "timeFormat12": "12 hour", + "timeFormat24": "24 hour" } diff --git a/src/json/theme.ts b/src/json/theme.ts index 24fb6933c6..47e4c40d99 100644 --- a/src/json/theme.ts +++ b/src/json/theme.ts @@ -121,4 +121,4 @@ export default { } } -}; \ No newline at end of file +}; diff --git a/src/json/url.ts b/src/json/url.ts index 078c0a9b43..bd12e527f5 100644 --- a/src/json/url.ts +++ b/src/json/url.ts @@ -23,13 +23,14 @@ export default { invite: 'https://invite.any.coop/%s#%s', share: 'https://join.anytype.io/', notionFAQ: 'https://doc.anytype.io/anytype-docs/basics/space/import-export#notion-import-faq', + publish: '%s.coop', survey: { register: 'https://community.anytype.io/survey0', delete: 'https://community.anytype.io/survey1', - pmf: 'https://community.anytype.io/survey2#anytypeid=%s', + pmf: 'https://anytype.typeform.com/to/JKotDI5b#anytypeid=%s', object: 'https://community.anytype.io/survey3', shared: 'https://community.anytype.io/survey4', multiplayer: 'https://community.anytype.io/survey5' } -}; +}; \ No newline at end of file diff --git a/src/scss/block/chat/attachment.scss b/src/scss/block/chat/attachment.scss index 55217a563c..0ef7fe7627 100644 --- a/src/scss/block/chat/attachment.scss +++ b/src/scss/block/chat/attachment.scss @@ -12,8 +12,7 @@ .icon.remove { opacity: 0; transition: $transitionAllCommon; position: absolute; right: -6px; top: -6px; width: 20px; height: 20px; background-size: 8px; - background-image: url('~img/icon/chat/buttons/remove.svg'); background-color: var(--color-bg-primary); border-radius: 50%; - box-shadow: 0px 1px 4px rgba(0,0,0,0.2); + background-image: url('~img/icon/chat/buttons/remove.svg'); background-color: var(--color-shape-primary); border-radius: 50%; } .name { line-height: 20px; font-weight: 500; @include clamp1; } @@ -36,7 +35,11 @@ .side.left { .link { @include text-small; color: var(--color-text-secondary); display: flex; justify-content: flex-start; align-items: center; gap: 0px 6px; } .name { @include text-common; line-height: 20px; font-weight: 500; @include clamp2; } - .descr { @include text-small; line-height: 16px; color: var(--color-text-secondary); @include clamp3; } + .descr { @include text-small; line-height: 16px; color: var(--color-text-primary); @include clamp3; } + + .link { + .iconObject { background: none; border-radius: 2px; } + } } .side.right { position: relative; display: flex; align-items: center; border-radius: 4px; overflow: hidden; } .side.right { @@ -48,6 +51,7 @@ .attachment.isImage { width: 72px; height: 72px; min-width: unset; display: flex; align-items: center; justify-content: center; } .attachment.isImage { .image { display: block; object-fit: cover; aspect-ratio: 1; border-radius: 8px; width: 100%; height: 100%; } + .loaderWrapper { width: 72px; height: 72px; } } .attachment.isVideo { width: 72px; height: 72px; min-width: unset; display: flex; align-items: center; justify-content: center; } @@ -73,8 +77,8 @@ .attachment { width: 540px; } } -.attachments.isSingle.isBookmark { - .attachment { width: 360px; } +.attachments.isSingle { + .attachment.isBookmark { width: 360px; } } /* Layouts */ diff --git a/src/scss/block/chat/form.scss b/src/scss/block/chat/form.scss index 0bba05e831..7dea159edc 100644 --- a/src/scss/block/chat/form.scss +++ b/src/scss/block/chat/form.scss @@ -1,13 +1,13 @@ -.formWrapper.isFixed { padding: 0px 0px 90px 0px; position: sticky; z-index: 11; bottom: 0px; width: 100%; background: var(--color-bg-primary); } +.formWrapper.isFixed { padding: 0px 0px 16px 0px; position: sticky; z-index: 11; bottom: 0px; width: 100%; background: var(--color-bg-primary); } .form { padding: 0px 0px 10px 0px; border: 1px solid var(--color-shape-tertiary); background-color: var(--color-bg-primary); border-radius: 12px; position: relative; overflow: hidden; } .form { - #form-loader { display: none; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-bg-loader); z-index: 1; } + #form-loader { display: none; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-bg-loader); z-index: 10; } #form-loader.active { display: block; } .head { @@ -29,9 +29,9 @@ .side.right { flex-shrink: 0; } .name { @include text-overflow-nw; font-weight: 500; } - .descr { @include text-overflow-nw; } + .descr { @include clamp1; } .descr:empty { display: none; } - .icon.clear { width: 20px; height: 20px; background-image: url('~img/icon/chat/buttons/clear.svg'); } + .icon.clear { width: 20px; height: 20px; border-radius: 50%; background: var(--color-control-active) url('~img/icon/chat/buttons/clear.svg'); } } .editableWrap { @include text-common; max-height: 352px; overflow: scroll; background-color: transparent; } @@ -62,8 +62,10 @@ .swiper-button-next::after { z-index: 2; margin: -10px 0px 0px -9px; width: 20px; height: 20px; background-image: url('~img/arrow/chatFormAttachment.svg'); } .swiper-button-prev::after { transform: rotateZ(180deg); margin-left: -10px; } - .swiper-button-disabled { display: none; } + + .attachment { margin-top: 6px; } + .swiper-slide:last-child .attachment { margin-right: 6px; } } .buttons { display: flex; gap: 0px 8px; padding: 10px 16px 0px 16px; margin-left: -4px; } @@ -113,6 +115,9 @@ } } + .charCounter { display: none; position: absolute; bottom: 10px; right: 48px; @include text-small; line-height: 20px; color: var(--color-text-secondary); } + .charCounter.show { display: block; } + .icon.send { position: absolute; bottom: 10px; right: 16px; width: 20px; height: 20px; background: url('~img/icon/chat/buttons/send.svg'); } } diff --git a/src/scss/block/chat/message.scss b/src/scss/block/chat/message.scss index e11bdca3c3..f0bdfa9356 100644 --- a/src/scss/block/chat/message.scss +++ b/src/scss/block/chat/message.scss @@ -11,7 +11,7 @@ .iconObject { display: none; } } - > .flex > .side.right { display: flex; flex-direction: column; padding: 12px 16px; border-radius: 20px; background: rgba(242, 242, 242, 0.5); position: relative; } + > .flex > .side.right { display: flex; flex-direction: column; padding: 12px 16px; border-radius: 20px; background: var(--color-shape-tertiary); position: relative; } .icon.reactionAdd { width: 20px; height: 20px; background-image: url('~img/icon/chat/buttons/reaction0.svg'); } .icon.reactionAdd:hover, .icon.reactionAdd.hover { background-image: url('~img/icon/chat/buttons/reaction1.svg'); } @@ -27,17 +27,15 @@ } .reply { - padding: 8px 8px 8px 12px; margin: 4px 0px; border-radius: 12px; background: var(--color-shape-tertiary); position: relative; overflow: hidden; + padding: 8px 8px 8px 12px; margin: 4px 0px; border-radius: 12px; background: #e3e3e3; position: relative; overflow: hidden; display: flex; align-items: center; gap: 0px 8px; } .reply::before { content: ''; display: block; position: absolute; top: 0px; left: 0px; width: 4px; height: 100%; background-color: var(--color-text-secondary); } .reply { - .icon, - .iconObject { flex-shrink: 0; width: 32px; height: 32px; } - - .iconObject:not(.noBg, .isParticipant) { border-radius: 4px !important; background-color: var(--color-shape-highlight-medium) !important; } + > .icon, > .iconObject { flex-shrink: 0; width: 32px; height: 32px; } + > .iconObject:not(.noBg, .isParticipant) { border-radius: 4px !important; background-color: var(--color-shape-highlight-medium) !important; } .icon.isMultiple { background-image: url('~img/icon/chat/attachment/multiple.svg'); } @@ -90,10 +88,13 @@ } .message.isSelf { - .flex { flex-direction: row-reverse; } - > .flex > .side.right { background: rgba(229, 248, 214, 0.5); } - .controls { flex-direction: row-reverse; } - .reply { background-color: #e5f8d6; } + > .flex { flex-direction: row-reverse; } + > .flex { + > .side.right { background: #e5f8d6; } + > .controls { flex-direction: row-reverse; } + } + + .reply { background-color: #c5efa3; } .reply::before { background-color: #4dae00; } } @@ -103,6 +104,9 @@ } .message.canExpand.isExpanded { .text { -webkit-line-clamp: unset; } + .reply { + .text { @include clamp; -webkit-line-clamp: 10; } + } .expand::after { transform: rotateZ(180deg); margin-top: 2px; } } .message:hover, .message.hover { diff --git a/src/scss/block/common.scss b/src/scss/block/common.scss index 284d0366c5..3f70163304 100644 --- a/src/scss/block/common.scss +++ b/src/scss/block/common.scss @@ -43,7 +43,7 @@ .colResize { width: 0px; position: relative; opacity: 0; z-index: 10; flex: 0; } .colResize.active { opacity: 1; } .colResize { - .inner { position: absolute; left: 22px; top: -15px; width: 8px; height: calc(100% + 30px); cursor: col-resize; z-index: 1; } + .inner { position: absolute; left: 22px; top: -15px; width: 8px; height: calc(100% + 30px); cursor: col-resize; z-index: 2; } .line { height: 100%; width: 2px; background: var(--color-shape-secondary); position: absolute; left: 50%; top: 0px; margin-left: -1px; border-radius: 2px; @@ -81,11 +81,11 @@ } .block.isAdding.top::before { - content: ""; display: block; width: calc(100% - 48px); height: 2px; background: var(--color-system-accent-100); position: absolute; right: 0px; top: -2px; + content: ""; display: block; width: calc(100% - 48px); height: 2px; background: var(--color-control-accent); position: absolute; right: 0px; top: -2px; } .block.isAdding.bottom::after { - content: ""; display: block; width: calc(100% - 48px); height: 2px; background: var(--color-system-accent-100); position: absolute; right: 0px; bottom: -2px; + content: ""; display: block; width: calc(100% - 48px); height: 2px; background: var(--color-control-accent); position: absolute; right: 0px; bottom: -2px; } .block:hover > .wrapMenu > .icon.dnd { opacity: 1; } diff --git a/src/scss/block/dataview.scss b/src/scss/block/dataview.scss index 337e456dc0..26f8cdfad1 100644 --- a/src/scss/block/dataview.scss +++ b/src/scss/block/dataview.scss @@ -84,7 +84,7 @@ .editableWrap.isEmpty { min-width: 160px; } } - .dataviewControls { font-weight: 500; color: var(--color-control-active); position: relative; } + .dataviewControls { font-weight: 500; color: var(--color-control-active); position: relative; overflow: hidden; } .dataviewControls::after { content: ''; display: none; height: 1px; width: 100%; background: var(--color-shape-secondary); position: absolute; bottom: 0px; } .dataviewControls.viewGrid::after, .dataviewControls.viewList::after { display: block; } @@ -101,9 +101,11 @@ .filterInputWrap { overflow: hidden; width: 0px; transition: width 0.2s $easeInQuint; } .line { display: none !important; } .icon.search { background-image: url('~img/icon/dataview/button/search.svg'); } + .icon.clear { display: none; } } .filter.isActive { .icon:hover { background-color: unset; } + .icon.clear { display: block; } .filterInputWrap { width: 120px; } } @@ -150,12 +152,13 @@ .name { @include text-overflow-nw; } } - .side.left.small { + .button { padding: 0px 8px; @include text-common; } + } + .dataviewControls.small { + .side.left { .views { display: none; } .viewSelect { display: inline-block; } } - - .button { padding: 0px 8px; @include text-common; } } .dataviewControls.isCollection { .side.left { padding-left: 20px; } @@ -237,9 +240,9 @@ .dataviewControls { margin: 0px 0px 8px 0px; } .dataviewControls { - > .sides { align-items: flex-end; } + > .sides { align-items: center; } > .sides { - > .side { padding: 0px; } + > .side { padding: 0px; height: 28px; } > .side.left { display: flex; flex-direction: column; align-items: flex-start; gap: 4px 0px; } > .side.right { transition: opacity $transitionCommon; opacity: 0; } } @@ -253,6 +256,10 @@ } .dataviewControls::after { display: none !important; } + .dataviewControls.small { + .side.right { display: none; } + } + .dataviewSelection { .side.left { @include text-paragraph; } } diff --git a/src/scss/block/dataview/view/graph.scss b/src/scss/block/dataview/view/graph.scss index 72a21161fd..de6d17cdf8 100644 --- a/src/scss/block/dataview/view/graph.scss +++ b/src/scss/block/dataview/view/graph.scss @@ -4,7 +4,7 @@ .block.blockDataview { .viewContent.viewGraph { position: relative; height: 100%; } .viewContent.viewGraph { - #graphWrapper { width: 100%; height: 100%; } + .graphWrapper { width: 100%; height: 100%; } #graph { width: 100%; height: 100%; } canvas { width: 100%; height: 100%; background: var(--color-bg-primary); display: block; } diff --git a/src/scss/block/dataview/view/grid.scss b/src/scss/block/dataview/view/grid.scss index 99ff5807b5..e01b1bbb82 100644 --- a/src/scss/block/dataview/view/grid.scss +++ b/src/scss/block/dataview/view/grid.scss @@ -4,6 +4,7 @@ .block.blockDataview { .rowHead, .rowFoot, #rowHeadClone { display: grid; white-space: nowrap; height: 36px; } + .rowHead { user-select: none; } .rowHead, .rowFoot { width: calc(100% - 4px); margin-left: 2px; } .rowHead.fixed { opacity: 0; visibility: hidden; pointer-events: none; } @@ -54,12 +55,12 @@ .cellFoot { height: 48px; @include text-common; color: var(--color-text-primary); } .cellFoot { .flex { justify-content: flex-end; } - .result { display: flex; flex-direction: row; align-items: center; gap: 0px 2px; max-width: 100%; } + .result { display: flex; flex-direction: row; align-items: center; gap: 0px 4px; max-width: 100%; } + .result { + .value { @include text-overflow-nw; } + } .name { width: auto !important; color: var(--color-text-secondary); } .cellContent { height: 48px !important; } - - .select { border: 0px; padding-left: 0px; padding-top: 0px; padding-bottom: 0px; opacity: 0; pointer-events: none; } - .select:hover { background: none; } } .cellHead.isDragging { border: 0px; height: 38px; } @@ -80,10 +81,6 @@ background-color: var(--color-shape-highlight-light); } - .cellFoot.cellKeyHover, .cellFoot.hover { - .select { opacity: 1; pointer-events: all; } - } - .cellFoot.cellKeyHover::after, .cellFoot.hover::after { height: calc(100% - 1px); } .cellHead.last::after, .cellHead.hover::after { background: none; } diff --git a/src/scss/block/featured.scss b/src/scss/block/featured.scss index 5e58a3f49c..d81c85d2af 100644 --- a/src/scss/block/featured.scss +++ b/src/scss/block/featured.scss @@ -11,7 +11,7 @@ .wrap { display: flex; flex-direction: row; align-items: center; gap: 0px 2px; flex-wrap: wrap; } .bullet { width: 4px; height: 4px; border-radius: 50%; background: var(--color-text-secondary); } - .cell { white-space: nowrap; display: inline-flex; flex-direction: row; align-items: center; gap: 0px 2px; } + .cell { white-space: nowrap; display: inline-flex; flex-direction: row; align-items: center; gap: 0px 2px; height: 28px; } .cell.canEdit { .cellContent { .empty { display: inline; } @@ -19,7 +19,10 @@ } .cell:last-child .bullet { display: none; } - .cellContent { display: inline-block; border-radius: 4px; vertical-align: top; padding: 0px 6px; transition: background $transitionCommon; cursor: default !important; } + .cellContent { + height: 100% ;display: flex; border-radius: 4px; vertical-align: top; padding: 0px 6px; transition: background $transitionCommon; + cursor: default !important; + } .cellContent.disabled { opacity: 1; } .cellContent:not(.disabled):hover, .cellContent:not(.disabled).hover { background: var(--color-shape-highlight-medium); } diff --git a/src/scss/block/media.scss b/src/scss/block/media.scss index 3570fb1577..74cde17702 100644 --- a/src/scss/block/media.scss +++ b/src/scss/block/media.scss @@ -69,7 +69,8 @@ } .block.blockMedia > .wrapContent { border-radius: 8px; box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.05); } - .block.blockMedia > .wrapContent > .selectionTarget > .dropTarget { line-height: 0px; } + .block.blockMedia.isAudio > .wrapContent { border: 1px solid var(--color-shape-secondary); } + .block.blockMedia.isAudio > .wrapContent > .selectionTarget > .dropTarget > .focusable > .wrap { width: 100%; padding: 12px 12px 7px 14px; } .block.blockMedia.isReadonly { .icon.resize { display: none; } @@ -94,9 +95,6 @@ .icon.play { display: none; } } - .block.blockMedia.isAudio.withContent > .wrapContent {box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.05); background-color: var(--color-bg-primary); } - .block.blockMedia.isAudio > .wrapContent > .selectionTarget > .dropTarget > .focusable > .wrap { width: 100%; padding: 12px 12px 7px 14px; border-radius: 8px; overflow: unset; } - .block.blockMedia > .wrapContent > .selectionTarget.isSelectionSelected::after { left: 0px; width: 100%; border-radius: 8px; } .block.blockMedia.isPdf.withContent > .wrapContent > .selectionTarget.isSelectionSelected::after { border-radius: 0px; } .block.blockMedia.isImage.withContent > .wrapContent > .selectionTarget.isSelectionSelected::after { border-radius: 0px; } diff --git a/src/scss/block/table.scss b/src/scss/block/table.scss index 7b5cc92677..61fcec7510 100644 --- a/src/scss/block/table.scss +++ b/src/scss/block/table.scss @@ -68,7 +68,7 @@ .handleColumn { display: block; border-color: var(--color-system-accent-100); } } - .row { display: grid; position: relative; border-right: 1px solid var(--color-shape-primary); } + .row { display: grid; position: relative; border-right: 1px solid var(--color-shape-primary); background-color: var(--color-shape-primary); } .row:first-child { border-radius: 4px 4px 0px 0px; } .row:last-child { border-bottom: 1px solid var(--color-shape-primary); border-radius: 0px 0px 4px 4px; } diff --git a/src/scss/block/text.scss b/src/scss/block/text.scss index 098cf7bc11..4c64f3d0e1 100644 --- a/src/scss/block/text.scss +++ b/src/scss/block/text.scss @@ -41,6 +41,12 @@ img { width: 100%; height: 100%; } } } + + .flex.isRtl { + .markers { + .marker { margin-right: 0px; margin-left: 6px; } + } + } } .block.blockText { diff --git a/src/scss/common.scss b/src/scss/common.scss index cd4f1a3424..0091e798b0 100644 --- a/src/scss/common.scss +++ b/src/scss/common.scss @@ -33,7 +33,6 @@ select:-webkit-autofill:hover, select:-webkit-autofill:focus { transition: background-color 5000s ease-in-out 0s; } input, textarea, select { font-family: 'Inter'; } - #drag { -webkit-app-region: drag; position: fixed; top: 0px; left: 0px; width: 100%; height: 52px; z-index: -1; user-select: none; pointer-events: all; } #root-loader { position: fixed; width: 100%; height: 100%; left: 0px; top: 0px; background: #060606; z-index: 1000; transition: opacity 0.3s ease-in-out; } #root-loader { @@ -108,6 +107,7 @@ html.platformWindows, html.platformLinux { .isBlurred { filter: blur(7px); } .animationWord { display: inline-block; } .isRtl { direction: rtl; text-align: right; } +.isOnboardingHidden { visibility: hidden; } .fileWrap { position: relative; overflow: hidden; } .fileWrap { diff --git a/src/scss/component/common.scss b/src/scss/component/common.scss index 4503327dac..a77a2e65f1 100644 --- a/src/scss/component/common.scss +++ b/src/scss/component/common.scss @@ -24,7 +24,6 @@ @import "./title"; @import "./toast"; @import "./tooltip"; -@import "./navigation"; @import "./hightlight"; @import "./progressBar"; @import "./share"; @@ -33,3 +32,4 @@ @import "./media/common"; @import "./emailCollectionForm"; +@import "./qr"; diff --git a/src/scss/component/deleted.scss b/src/scss/component/deleted.scss index 9aefc4a0e0..819859c2be 100644 --- a/src/scss/component/deleted.scss +++ b/src/scss/component/deleted.scss @@ -1,6 +1,6 @@ @import "~scss/_mixins"; -.deleteWrapper { position: absolute; left: 0px; top: 0px; z-index: 20; background: var(--color-bg-primary); width: 100%; height: 100%; } +.deleteWrapper { position: absolute; left: 0px; top: 0px; z-index: 19; background: var(--color-bg-primary); width: 100%; height: 100%; } .deleteWrapper { .mid { position: absolute; width: 400px; height: 200px; margin: -100px 0px 0px -200px; left: 50%; top: 50%; text-align: center; } .icon.ghost { width: 64px; height: 64px; } diff --git a/src/scss/component/floater.scss b/src/scss/component/floater.scss index b6993ae5bf..5e87f93837 100644 --- a/src/scss/component/floater.scss +++ b/src/scss/component/floater.scss @@ -1,2 +1,4 @@ -.floater { position: absolute; pointer-events: none; z-index: 200; transition: opacity 0.2s ease-in-out; opacity: 0; } +@import "~scss/_mixins"; + +.floater { position: fixed; pointer-events: none; z-index: 200; transition: opacity $transitionCommon; opacity: 0; } .floater.show { opacity: 1; pointer-events: auto; } \ No newline at end of file diff --git a/src/scss/component/header.scss b/src/scss/component/header.scss index a8ff19f100..a0eecabdbf 100644 --- a/src/scss/component/header.scss +++ b/src/scss/component/header.scss @@ -11,6 +11,9 @@ .side.left { padding: 0px 16px; width: 20%; flex-shrink: 0; gap: 0px 8px; } .side.center { width: 60%; flex-grow: 1; justify-content: center; } .side.right { padding: 0px 12px; width: 20%; flex-shrink: 0; justify-content: end; gap: 0px 8px; } + .side.right { + .button.blank { font-weight: 500; border: 0px; padding: 0px 6px; color: var(--color-text-secondary); flex-shrink: 0; } + } .path { width: 66%; display: inline-block; border-radius: 6px; height: 28px; line-height: 26px; transition: $transitionAllCommon; @@ -50,13 +53,12 @@ .headerBanner.withMenu:after { content: ''; position: absolute; right: 10px; top: 12px; width: 8px; height: 8px; background-image: url('~img/arrow/button/black.svg'); } .headerBanner.withMenu.active:after { transform: rotateZ(180deg); } - .sync { -webkit-app-region: no-drag; flex-shrink: 0; } .icon { -webkit-app-region: no-drag; flex-shrink: 0; } - .icon.more { background-image: url('~img/icon/header/more.svg'); } .icon.settings { background-image: url('~img/icon/header/settings.svg'); } .icon.expand { display: none; background-image: url('~img/icon/header/expand.svg'); } .icon.relation { background-image: url('~img/icon/header/relation.svg'); } + .icon.graph { background-image: url('~img/icon/menu/action/graph0.svg'); } } .header.sidebarAnimation { @@ -64,18 +66,18 @@ } .header:not(.withSidebar) { - .side.left { padding-left: 120px; } + .side.left { padding-left: 156px; } } html:not(.platformMac) { .header:not(.withSidebar) { - .side.left { padding-left: 52px; } + .side.left { padding-left: 88px; } } } body.isFullScreen { .header:not(.withSidebar) { - .side.left { padding-left: 52px; } + .side.left { padding-left: 88px; } } } diff --git a/src/scss/component/media/audio.scss b/src/scss/component/media/audio.scss index 66851e69ea..50c4b0c38c 100644 --- a/src/scss/component/media/audio.scss +++ b/src/scss/component/media/audio.scss @@ -1,36 +1,35 @@ @import "~scss/_mixins"; -.mediaAudio { width: 100%; border: 1px solid var(--color-shape-secondary); container-type: inline-size; container-name: media-audio; } +.mediaAudio { width: 100%; container-type: inline-size; container-name: media-audio; } .mediaAudio { .controlsWrapper { width: 100%; position: relative; text-align: left; color: var(--color-text-primary); } .controlsWrapper { .name { @include text-common; display: inline-block; vertical-align: top; line-height: 20px; padding-bottom: 3px; } - .name span { display: inline-block; min-width: 100%; height: 100%; @include clamp1; } + .name { + span { display: inline-block; min-width: 100%; height: 100%; @include clamp1; } + } - .controls { display: flex; align-items: center; column-gap: 10px; } - - @container media-audio (max-width: 208px) { .controls { column-gap: 5px; }} + @container media-audio (max-width: 208px) { + .controls { column-gap: 5px; } + .timeText { display: none; } + } + .controls { display: flex; align-items: center; column-gap: 10px; } .controls { - div { flex: 0 1 auto; } + .icon { width: 20px; height: 20px; vertical-align: top; transition: none; flex-shrink: 0; } + .icon.play { background-image: url('~img/icon/audio/play.svg'); margin-left: -4px; } + .icon.play.active { background-image: url('~img/icon/audio/pause.svg'); } + .icon.volume { background-image: url('~img/icon/audio/volume.svg'); } + .icon.volume.isMuted { background-image: url('~img/icon/audio/mute.svg'); } - .timeDragWrapper { flex: 1 0 auto; } + .volumeWrap { height: 32px; display: flex; align-items: center; } + + .timeDragWrapper { flex: 1 0 auto; } + .timeText { @include text-small; text-wrap: nowrap; width: 36px; text-align: center; } - .icon { vertical-align: top; transition: none; } - .icon.play { width: 20px; height: 20px; min-width: 20px; min-height: 20px; background-image: url('~img/icon/audio/play.svg'); } - .icon.play.active { background-image: url('~img/icon/audio/pause.svg'); } - - .input-drag-horizontal .icon { cursor: default; } - - .icon.volume { width: 20px; height: 20px; min-width: 20px; min-height: 20px; background-image: url('~img/icon/audio/volume.svg'); } - .icon.volume.muted { background-image: url('~img/icon/audio/mute.svg'); } - .time { @include text-small; text-wrap: nowrap; width: 36px; text-align: center; } - - @container media-audio (max-width: 208px) { .time { display: none; }} - .input-drag-horizontal { display: inline-block; vertical-align: top; height: 20px; } .input-drag-horizontal { - .icon { width: 6px; height: 6px; border: 0px; background: none; } + .icon { width: 6px; height: 6px; border: 0px; background: none; cursor: default; } .bullet { width: 12px; height: 12px; border-radius: 6px; background: var(--color-control-accent); } .fill { height: 4px; background: var(--color-control-accent); transform: translateY(-50%); margin-top: 0px; } .back { width: 100%; height: 4px; background: var(--color-shape-secondary); transform: translateY(-50%); margin-top: 0px; } @@ -38,7 +37,7 @@ } - #time { margin: 6px 0px; width: 100%; display: block; } + #timeDrag { margin: 6px 0px; width: 100%; display: block; } } } diff --git a/src/scss/component/navigation.scss b/src/scss/component/navigation.scss deleted file mode 100644 index 2529e9a193..0000000000 --- a/src/scss/component/navigation.scss +++ /dev/null @@ -1,27 +0,0 @@ -@import "~scss/_mixins"; - -.navigationPanel { - background: rgba(134, 134, 134, 0.7); backdrop-filter: blur(32px); border-radius: 16px; position: fixed; left: 0px; padding: 12px 16px; bottom: 24px; - z-index: 105; transition-property: opacity, visibility; transition-duration: 0.16s; transition-timing-function: $easeInQuint; -} -.navigationPanel.hide { visibility: hidden; z-index: 0; opacity: 0; } -.navigationPanel.hide * { pointer-events: none; } - -.navigationPanel.sidebarAnimation { transition-property: left; } - -.navigationPanel { - .inner { display: flex; flex-direction: row; gap: 0px 20px; align-items: center; justify-content: center; position: relative; z-index: 1; } - - .iconWrap { - width: 28px; height: 28px; border-radius: 6px; display: flex; align-items: center; justify-content: center; - position: relative; transition: $transitionAllCommon; - } - .iconWrap:not(.disabled):hover, .iconWrap.active { background-color: rgba(37, 37, 37, 0.15); } - - .icon { width: 20px; height: 20px; z-index: 1; } - .icon.back, .icon.forward { background-image: url('~img/icon/navigation/back.svg'); } - .icon.forward { transform: rotateZ('180deg'); } - .icon.plus { background-image: url('~img/icon/navigation/plus.svg'); } - .icon.graph { background-image: url('~img/icon/navigation/graph.svg'); } - .icon.search { background-image: url('~img/icon/navigation/search.svg'); } -} diff --git a/src/scss/component/preview/object.scss b/src/scss/component/preview/object.scss index ec7c1459d3..8beb376d9d 100644 --- a/src/scss/component/preview/object.scss +++ b/src/scss/component/preview/object.scss @@ -10,7 +10,7 @@ align-items: center; justify-content: center; background: var(--color-shape-tertiary); opacity: 0; transition: $transitionAllCommon; } .moreWrapper { - .icon { width: 20px; height: 20px; background-image: url('~img/icon/menu/action/more0.svg'); } + .icon { width: 20px; height: 20px; background-image: url('~img/icon/menu/action/more0.svg'); margin: 0px !important; } } .scroller { padding: 32px 40px 16px 40px; position: relative; z-index: 1; overflow: hidden; height: calc(100% - 1px); } diff --git a/src/scss/component/qr.scss b/src/scss/component/qr.scss new file mode 100644 index 0000000000..7c66b2f5d8 --- /dev/null +++ b/src/scss/component/qr.scss @@ -0,0 +1,4 @@ +.qrInner { width: 130px; height: 130px; border-radius: 4px; padding: 4px; background-color: #fff; } +.qrInner { + canvas { width: 100%; height: 100%; } +} diff --git a/src/scss/component/share.scss b/src/scss/component/share.scss index bb5965a95d..3d174909d5 100644 --- a/src/scss/component/share.scss +++ b/src/scss/component/share.scss @@ -19,6 +19,7 @@ .icon.close { box-shadow: none; background-color: var(--color-shape-primary); background-image: url('~img/icon/widget/remove.svg'); z-index: 1; -webkit-app-region: no-drag; } .icon.close:hover { background-color: var(--color-text-tertiary); } } +.shareBanner.isOnboardingHidden { display: none; } .shareTooltip, .shareBanner { diff --git a/src/scss/component/sidebar.scss b/src/scss/component/sidebar.scss index 2c2d70dd1e..11bb40ab21 100644 --- a/src/scss/component/sidebar.scss +++ b/src/scss/component/sidebar.scss @@ -8,13 +8,14 @@ .sidebarAnimation { transition: width $transitionSidebarTime linear; } -#sidebarToggle { - position: fixed; left: 84px; top: 12px; backdrop-filter: blur(20px); - background-image: url('~img/icon/widget/toggle0.svg'); z-index: 22; -webkit-app-region: no-drag; transition: none; -} -#sidebarToggle.sidebarAnimation { transition: left $transitionSidebarTime linear; } +#sidebarToggle, +#sidebarSync { position: fixed; top: 12px; z-index: 22; -webkit-app-region: no-drag; transition: none; } + +#sidebarToggle { backdrop-filter: blur(20px); left: 84px; background-image: url('~img/icon/widget/toggle0.svg'); } +#sidebarToggle.sidebarAnimation, #sidebarSync.sidebarAnimation { transition: left $transitionSidebarTime linear; } #sidebarToggle:hover, #sidebarToggle.hover { background-color: var(--color-shape-highlight-medium) !important; background-image: url('~img/icon/widget/toggle1.svg'); } + .sidebar { position: fixed; z-index: 21; user-select: none; transition: none; top: 0px; left: 0px; height: 100%; } .sidebar.anim { transition-property: width; transition-duration: $transitionSidebarTime; transition-timing-function: linear; } .sidebar.withVault { left: $vaultWidthCollapsed; } diff --git a/src/scss/component/sidebar/object.scss b/src/scss/component/sidebar/object.scss index 021a1ba34f..609254c7df 100644 --- a/src/scss/component/sidebar/object.scss +++ b/src/scss/component/sidebar/object.scss @@ -10,6 +10,7 @@ .titleWrap { .side { height: 32px; } .side.left { display: flex; flex-direction: row; align-items: center; gap: 0px 8px; } + .side.right { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; } .title { @include text-paragraph; font-weight: 600; flex-grow: 1; } .icon.back { flex-shrink: 0; background-image: url('~img/icon/widget/back.svg'); } diff --git a/src/scss/component/sidebar/widget.scss b/src/scss/component/sidebar/widget.scss index 45f2d3ada3..bdb958bf1c 100644 --- a/src/scss/component/sidebar/widget.scss +++ b/src/scss/component/sidebar/widget.scss @@ -106,5 +106,7 @@ .list.isListPreview { overflow: hidden; height: 100%; } } - > .body.withShareBanner { margin-top: -10px; } + + > .body.isOnboardingHidden { visibility: visible; } + > .body.withShareBanner:not(.isOnboardingHidden) { margin-top: -10px; } } diff --git a/src/scss/component/toast.scss b/src/scss/component/toast.scss index 089e28c45e..c52a69f992 100644 --- a/src/scss/component/toast.scss +++ b/src/scss/component/toast.scss @@ -3,21 +3,26 @@ .toast { position: fixed; left: 0px; top: 0px; border-radius: 8px; background: var(--color-control-accent); padding: 11px 16px; text-transform: none; @include text-common; color: var(--color-bg-primary); display: none; z-index: 1000; white-space: nowrap; transition-duration: 0.25s; - transition-property: opacity, transform; transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); + transition-property: opacity, transform; transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); max-width: 50%; } .toast { - .inner { display: flex; justify-content: space-between; } + .inner { display: flex; justify-content: space-between; gap: 0px 8px; } } .toast { .inner { - .message { display: flex; gap: 0px 4px; } + .icon { width: 20px; height: 20px; } + .icon.notice { background: url('~img/icon/toast/notice.svg'); } + + .message { display: flex; gap: 0px 4px; max-width: 100%; } .message { .chunk { @include text-overflow-nw; display: flex; align-items: center; gap: 0px 4px; } .chunk { .name { max-width: 140px; @include text-overflow-nw; } } + span { @include text-overflow-nw; } + b { font-weight: 500; } } diff --git a/src/scss/form/filter.scss b/src/scss/form/filter.scss index c09391bbe6..faba589c1e 100644 --- a/src/scss/form/filter.scss +++ b/src/scss/form/filter.scss @@ -4,7 +4,6 @@ .filter { .inner { position: relative; width: 100%; height: 35px; display: flex; align-items: center; gap: 0px 8px; } .filterInputWrap { position: relative; width: 100%; } - .input { padding: 0px !important; height: 28px; line-height: 28px; vertical-align: top; border: 0px !important; background: none; } .icon { flex-shrink: 0; cursor: default; } diff --git a/src/scss/form/inputWithFile.scss b/src/scss/form/inputWithFile.scss index f80990785b..246331891a 100644 --- a/src/scss/form/inputWithFile.scss +++ b/src/scss/form/inputWithFile.scss @@ -2,22 +2,24 @@ .inputWithFile { padding: 11px 13px; border-radius: 8px; border: solid 1px var(--color-shape-secondary); color: var(--color-control-active); @include text-common; - transition: background 0.05s ease-in-out, border $transitionCommon; white-space: nowrap; overflow: hidden; line-height: 20px; text-align: left; + transition: border $transitionCommon; overflow: hidden; line-height: 20px; text-align: left; display: flex; align-items: center; gap: 0px 6px; + white-space: nowrap; } .inputWithFile:hover { border-color: var(--color-shape-primary); } .inputWithFile { - .txt { line-height: 20px; height: 20px; overflow: hidden; width: calc(100% - 26px); vertical-align: top; } + .inputWithFile-inner { display: flex; align-items: center; flex-wrap: wrap; flex-grow: 1; width: calc(100% - 26px); } + .fileWrap { display: inline-block; vertical-align: top; } .fileWrap .border { border-bottom: 0.05em solid var(--color-control-active); display: inline-block; line-height: 1; transition: $transitionAllCommon; } .fileWrap:hover .border { color: var(--color-text-primary); } + .input-text { height: 20px; line-height: 20px; vertical-align: top; padding: 0px; border: 0px; color: var(--color-text-primary); } .input::placeholder { color: var(--color-control-active); } - .urlToggle { cursor: text; display: inline-block; } - #form { display: inline-block; vertical-align: top; } - #url { height: 20px; line-height: 20px; vertical-align: top; padding: 0px; border: 0px; color: var(--color-text-primary); } + .urlToggle { cursor: text; display: inline-block; } + .form { display: inline-block; vertical-align: top; } - .icon { width: 20px; height: 20px; margin: 0px 6px 0px 0px; transition: none; vertical-align: top; } + .icon { width: 20px; height: 20px; transition: none; flex-shrink: 0; } .icon.image { background-image: url('~img/icon/menu/action/block/media/image0.svg'); } .icon.video { background-image: url('~img/icon/menu/action/block/media/video0.svg'); } .icon.file { background-image: url('~img/icon/menu/action/block/media/file0.svg'); } @@ -39,13 +41,13 @@ .inputWithFile.noFile { .urlToggle { width: 100%; } - #form { width: 100%; } + .form { width: 100%; } } .inputWithFile.noFile.isSmall .txt { height: 20px; } .inputWithFile.isFocused { .fileWrap { display: none; } - #form { width: 100%; } + .form { width: 100%; } } .inputWithFile.isSmall.isFocused { @@ -53,7 +55,6 @@ .fileWrap { display: block; } } -.inputWithFile.isIcon { } .inputWithFile.isIcon { - #text { display: none; } + .inputWithFile-inner { display: none; } } \ No newline at end of file diff --git a/src/scss/list/object.scss b/src/scss/list/object.scss index 76d5354598..ab1a389b5b 100644 --- a/src/scss/list/object.scss +++ b/src/scss/list/object.scss @@ -3,9 +3,9 @@ .listObject { .table { display: grid; margin: 0px 0px 10px 0px; } .table { - .selectionTarget { display: grid; grid-template-columns: minmax(0, 1fr) 20% 20%; } + .selectionTarget { display: grid; grid-template-columns: minmax(0, 1fr) 30% 30%; } - .row.isHead { display: grid; grid-template-columns: minmax(0, 1fr) 20% 20%; color: var(--color-control-active); } + .row.isHead { display: grid; grid-template-columns: minmax(0, 1fr) 30% 30%; color: var(--color-control-active); } .row.isHead { .cell { text-align: left; padding: 9px 0px 9px 14px; white-space: nowrap; font-weight: 400; line-height: 20px; position: relative; @@ -27,6 +27,9 @@ .row { .cell { padding: 9px 14px; vertical-align: top; position: relative; word-break: break-word; } + .cell:first-child { padding-left: 0px; } + .cell:last-child { padding-right: 0px; } + .cell.empty { line-height: 20px; } .cellContent { width: 100%; overflow: hidden; height: 20px; line-height: 20px; } .cellContent { diff --git a/src/scss/media/print.scss b/src/scss/media/print.scss index f348e5ad2e..8893aa21db 100644 --- a/src/scss/media/print.scss +++ b/src/scss/media/print.scss @@ -14,14 +14,14 @@ html.printMedia { .header, .footer, .notifications, - #navigationPanel, #sidebar, .sidebarDummy, .progress, .toast, .focusable.isFocused::before, #vault, - #sidebarToggle + #sidebarToggle, + #sidebarSync { display: none !important; } #page.isFull { width: 100% !important; } @@ -95,18 +95,14 @@ html.printMedia { } .block.blockText.textCode > .wrapContent > .selectionTarget > .dropTarget #value { white-space: pre-wrap; overflow-x: visible; } + .block.blockMedia.isAudio > .wrapContent { box-shadow: 0px 0px; } + .block.blockDataview.isInline { .content { .scroll { width: 100% !important; margin: 0px !important; padding-left: 0px !important; } } } - .block.blockTable { width: 100% !important; margin: 0px !important; } - .block.blockTable { - .scrollWrap > .inner { width: 100% !important; } - .row { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)) !important; } - } - .block.blockDataview { .viewContent.viewGallery { .card { box-shadow: 0px 0px; } @@ -121,9 +117,17 @@ html.printMedia { .inner { box-shadow: 0px 0px; } } + .block.blockBookmark { + .inner { box-shadow: 0px 0px; } + } + .block.blockRelation { .info { width: auto; } } + + .block.blockTable { + .handle { display: none; } + } } .editor { @@ -137,6 +141,14 @@ html.printMedia.themeDark { html.printMedia.print { #editorWrapper { width: 100% !important; } + + .blocks { + .block.blockTable { width: 100% !important; margin: 0px !important; } + .block.blockTable { + .scrollWrap > .inner { width: 100% !important; } + .row { grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)) !important; } + } + } } html.printMedia.withPopup { diff --git a/src/scss/menu/block/add.scss b/src/scss/menu/block/add.scss index 4e0731e649..3518341271 100644 --- a/src/scss/menu/block/add.scss +++ b/src/scss/menu/block/add.scss @@ -12,14 +12,14 @@ .sectionName.first { padding-top: 4px; } .sectionName.first::before { display: none; } - .info { - width: 154px; @include text-overflow-nw; line-height: 20px; border-radius: 4px; transition: background $transitionCommon; - flex-shrink: 0; margin-right: 6px; - } + .info { @include text-overflow-nw; line-height: 20px; border-radius: 4px; transition: background $transitionCommon; flex-shrink: 0; } .item { padding: 4px 16px; } .item.add { padding: 6px 16px; } - .item.sides { display: flex; padding: 6px 16px; } + .item.sides { display: flex; padding: 6px 16px; flex-direction: row; align-items: center; gap: 0px 6px; } + .item.sides { + .info { width: 154px; } + } .item.empty { padding: 16px; @include text-small; } .item { @@ -83,6 +83,8 @@ .icon.relation { background-image: url('~img/icon/menu/action/block/relation1.svg'); } .icon.set { background-image: url('~img/icon/menu/action/block/set1.svg'); } .icon.collection { background-image: url('~img/icon/menu/action/block/collection1.svg'); } + + .icon.date { background-image: url('~img/icon/menu/action/date0.svg'); } } .cell { width: calc(100% - 160px); white-space: nowrap; } diff --git a/src/scss/menu/block/relation.scss b/src/scss/menu/block/relation.scss index 958b202b06..7c39a4964b 100644 --- a/src/scss/menu/block/relation.scss +++ b/src/scss/menu/block/relation.scss @@ -167,13 +167,6 @@ .info { width: 100%; transition: $transitionAllCommon; } } } - -} - -body.isDragging { - .menus { - .menu.menuBlockRelationView { opacity: 0.5; } - } } html.platformWindows { diff --git a/src/scss/menu/common.scss b/src/scss/menu/common.scss index 47d0b13fef..540f7396b3 100644 --- a/src/scss/menu/common.scss +++ b/src/scss/menu/common.scss @@ -5,7 +5,6 @@ .menuWrap.fromPopup, .menuWrap.fromHeader, .menuWrap.fromSidebar, - .menuWrap.fromNavigation { z-index: 103; } .menuWrap.fromOnboarding { z-index: 1001; } .menuWrap.fixed { position: fixed; } @@ -124,7 +123,7 @@ .buttons { flex-shrink: 0; } .buttons { - .icon { position: static; margin: 0px; } + .icon { position: static; margin: 0px; flex-shrink: 0; } } .iconObject { margin-right: 6px; vertical-align: top; flex-shrink: 0; } @@ -150,7 +149,7 @@ .descr { @include text-small; @include text-overflow-nw; line-height: 20px; height: 20px; color: var(--color-text-secondary); } .descr:empty { display: none; } - .info { line-height: 40px; width: calc(100% - 16px); } + .info { line-height: 40px; } .txt { width: 100%; } } @@ -273,6 +272,7 @@ html.platformWindows { @import "./syncStatus"; @import "./graph"; @import "./participant"; +@import "./publish"; @import "./search/object"; @import "./search/text"; diff --git a/src/scss/menu/dataview/calendar.scss b/src/scss/menu/dataview/calendar.scss index ae60b5bff1..5d400c3cd4 100644 --- a/src/scss/menu/dataview/calendar.scss +++ b/src/scss/menu/dataview/calendar.scss @@ -18,7 +18,10 @@ .sides { margin-bottom: 10px; } .side.right { display: flex; gap: 0px 2px; justify-content: flex-end; } .side.right { - .btn { width: 24px; height: 24px; background-size: 20px; background-image: url('~img/arrow/calendarNav.svg'); background-position: 50% 50%; border-radius: 4px; } + .btn { + width: 24px; height: 24px; background-size: 20px; background-image: url('~img/arrow/calendarNav.svg'); background-position: 50% 50%; + border-radius: 4px; + } .btn:hover { background-color: var(--color-shape-highlight-medium); } .btn.prevMonth { transform: rotateZ(180deg); } } @@ -35,15 +38,29 @@ .btn:hover { color: var(--color-system-accent-100); } } - .day { display: flex; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 4px; align-items: center; justify-content: center; } - .day:not(.th):not(.active):hover { background: var(--color-shape-highlight-medium); } + .day { + display: flex; width: 28px; height: 28px; line-height: 28px; text-align: center; border-radius: 4px; align-items: center; justify-content: center; + position: relative; + } .day.th { color: var(--color-control-active); @include text-small; } + .day { + .inner { width: 100%; max-width: 28px; height: 28px; border-radius: 4px; transition: $transitionAllCommon; position: relative; } + .bullet { width: 3px; height: 3px; border-radius: 50%; position: absolute; bottom: 2px; left: 50%; margin-left: -1.5px; background: var(--color-control-active); } + } + .day.today, .day.active { font-weight: 600; } .day.today { color: var(--color-system-accent-125); } - .day.active { background: var(--color-system-accent-100); color: var(--color-text-inversion); } .day.other { color: var(--color-control-active); } + .day.active { background: var(--color-system-accent-100); color: var(--color-text-inversion); } + .day.active { + .bullet { background-color: var(--color-text-inversion); } + } + + .day.selected::before { + content: ''; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; border-radius: inherit; background-color: var(--color-shape-highlight-medium); + } .line { margin: 8px 16px 11px 16px; } diff --git a/src/scss/menu/dataview/option.scss b/src/scss/menu/dataview/option.scss index 0b4061872b..3647787e42 100644 --- a/src/scss/menu/dataview/option.scss +++ b/src/scss/menu/dataview/option.scss @@ -13,8 +13,11 @@ .items { height: calc(100% - 38px); } } - .item { display: flex; align-items: center; padding-right: 10px; gap: 0px 4px; } + .item { display: flex; align-items: center; } + .item:not(.add, .empty) { padding: 0px 10px 0px 0px; gap: 0px 4px; } .item { + .clickable { padding: 4px 14px; } + .buttons { display: flex; flex-direction: row; align-items: center; gap: 0px 2px; flex-shrink: 0; } .buttons { .icon.more { display: none; } diff --git a/src/scss/menu/icon.scss b/src/scss/menu/icon.scss index b00f2af1ba..3a375ec27e 100644 --- a/src/scss/menu/icon.scss +++ b/src/scss/menu/icon.scss @@ -124,12 +124,6 @@ .icon.table-insert-top { background-image: url('~img/icon/menu/table/insert-v.svg'); } .icon.table-insert-bottom { background-image: url('~img/icon/menu/table/insert-v.svg'); transform: rotateZ(180deg); } - .icon.widget-0 { background-image: url('~img/icon/menu/widget/link.svg'); } - .icon.widget-1 { background-image: url('~img/icon/menu/widget/tree.svg'); } - .icon.widget-2 { background-image: url('~img/icon/menu/widget/list.svg'); } - .icon.widget-3 { background-image: url('~img/icon/menu/widget/compact.svg'); } - .icon.widget-4 { background-image: url('~img/icon/menu/widget/view.svg'); } - .icon.sidebar-all { background-image: url('~img/icon/menu/sidebar/all.svg'); } .icon.sidebar-sidebar { background-image: url('~img/icon/menu/sidebar/sidebar.svg'); } .icon.sidebar-focus { background-image: url('~img/icon/menu/sidebar/focus.svg'); } diff --git a/src/scss/menu/publish.scss b/src/scss/menu/publish.scss new file mode 100644 index 0000000000..19497c16db --- /dev/null +++ b/src/scss/menu/publish.scss @@ -0,0 +1,43 @@ +@import "~scss/_mixins"; + +.menus { + .menu.menuPublish { width: var(--menu-width-large); } + .menu.menuPublish { + .content { padding: 16px; display: flex; flex-direction: column; gap: 8px 0px; overflow: visible; max-height: unset; } + .title { padding: 0px; @include text-paragraph; font-weight: 600; margin: 0px 0px 8px 0px; color: var(--color-text-primary); } + + .input { padding: 7px 9px; height: auto; border-radius: 7px; border: 1px solid var(--color-shape-secondary); } + .input.isReadonly { background: rgba(0, 0, 0, 0.03); } + .input.isFocused { box-shadow: 0px 0px 0px 1px var(--color-system-accent-50); border-color: var(--color-system-accent-50); } + + .label.small { @include text-small; @include text-overflow-nw; color: var(--color-text-secondary); } + + .flex { padding: 3px 0px; align-items: center; gap: 0px 16px; justify-content: space-between; } + .flex { + .value { display: flex; flex-direction: row; align-items: center; justify-content: flex-end; flex-shrink: 0; } + } + + .outer { + position: absolute; left: 0px; bottom: 0px; transform: translateY(calc(100% + 8px)); width: 100%; box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.2); + border-radius: 8px; overflow: hidden; background: var(--color-bg-primary); + } + .outer { + .item { + display: flex; flex-direction: row; align-items: center; padding: 18px 16px; @include text-paragraph; font-weight: 600; gap: 0px 12px; + border-bottom: 1px solid var(--color-shape-secondary); + } + .item:last-child { border-bottom: none; } + .item::before { + content: ""; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: rgba(79,79,79,0); z-index: 1; pointer-events: none; + } + .item:hover::before { background: var(--color-shape-highlight-medium); } + + .icon { flex-shrink: 0; } + .icon.space { background-image: url('~img/icon/widget/button/member.svg'); } + .icon.export { background-image: url('~img/icon/widget/button/export.svg'); } + .icon.arrow { background-size: 6px 10px; background-image: url('~img/icon/popup/settings/forward.svg'); right: 12px; } + + .name { flex-grow: 1; } + } + } +} \ No newline at end of file diff --git a/src/scss/menu/syncStatus.scss b/src/scss/menu/syncStatus.scss index 9a54fe4a10..435fe06e06 100644 --- a/src/scss/menu/syncStatus.scss +++ b/src/scss/menu/syncStatus.scss @@ -76,8 +76,8 @@ } } - .item:hover, .item.hover { background: var(--color-shape-highlight-medium); } .item:hover, .item.hover { + &::before { background: var(--color-shape-highlight-medium); } .side.right { .icon:not(.more) { display: none; } .icon.more { opacity: 1; } diff --git a/src/scss/menu/widget.scss b/src/scss/menu/widget.scss index 86c881104c..9c68c4c7d8 100644 --- a/src/scss/menu/widget.scss +++ b/src/scss/menu/widget.scss @@ -1,18 +1,32 @@ @import "~scss/_mixins"; .menus { - .menu.menuWidget { width: var(--menu-width-icon); } + .menu.menuWidget { width: unset; } .menu.menuWidget { - .section:last-child::after { display: none; } + .section::after { display: none; } + .section:last-child::before { content: ''; display: block; height: 1px; margin: 0px 14px 8px; background-color: var(--color-shape-secondary); } .item { .name { @include text-overflow-nw; width: calc(100% - 36px); } } - .item.withCaption { - .caption { @include text-common; line-height: 20px; padding-right: 16px; } + + .options { display: flex; gap: 0px 8px; padding: 0px 14px; margin-bottom: 4px; } + .options { + .option { display: flex; align-items: center; justify-content: center; width: 48px; height: 32px; border-radius: 8px; border: 1px solid var(--color-shape-tertiary); } + .option.withIcon { height: 48px; } + .option { + .icon { width: 48px; height: 48px; flex-shrink: 0; } + .icon.widget-0 { background-image: url('~img/icon/menu/widget/link.svg'); } + .icon.widget-1 { background-image: url('~img/icon/menu/widget/tree.svg'); } + .icon.widget-2 { background-image: url('~img/icon/menu/widget/list.svg'); } + .icon.widget-3 { background-image: url('~img/icon/menu/widget/compact.svg'); } + .icon.widget-4 { background-image: url('~img/icon/menu/widget/view.svg'); } + } + .option:hover { background-color: var(--color-shape-highlight-light); } + .option.active { background-color: var(--color-shape-highlight-light); border-color: var(--color-shape-primary); } } .buttons { padding: 8px 16px 0px 16px; } .button { width: 100%; } } -} \ No newline at end of file +} diff --git a/src/scss/page/auth.scss b/src/scss/page/auth.scss index 83bd02b7b8..16717893b9 100644 --- a/src/scss/page/auth.scss +++ b/src/scss/page/auth.scss @@ -25,7 +25,7 @@ html.bodyIndex, html.bodyAuth { --shadow: 0px 0px 0px 1px var(--color-button-stroke) !important; body { background: var(--color-bg-primary); color: var(--color-text-secondary); overflow: hidden; } - #navigationPanel, #notifications, #sidebar, #vault, #sidebarToggle { display: none !important; } + #notifications, #sidebar, #vault, #sidebarToggle, #sidebarSync, .shareTooltip { display: none !important; } .popup { .innerWrap { background: var(--color-popup) !important;; box-shadow: var(--shadow) !important; color: var(--color-text-primary) !important; } @@ -59,6 +59,7 @@ html.bodyIndex, html.bodyAuth { .menu.vertical { background: var(--color-popup) !important; box-shadow: var(--shadow) !important; } .menu.vertical { .item { background: var(--color-popup) !important; } + .item.hover::before { background: rgba(255, 255, 255, 0.05) !important; } } .menu.menuAccountPath { .label { color: var(--color-text-secondary) !important; } @@ -81,7 +82,7 @@ html.bodyIndex, html.bodyAuth { ul { list-style-position: inside; padding-left: 0.75em; } } - .tooltip.menuNote { white-space: nowrap; background-color: var(--color-bg-secondary); color: var(--color-text-inversion); padding: 4px 8px; @include text-small; } + .tooltip.menuNote { white-space: nowrap; background-color: var(--color-bg-secondary); color: var(--color-text-primary); padding: 4px 8px; @include text-small; } .tooltip.menuNote { .txt { line-height: 18px; } } @@ -275,3 +276,7 @@ html.bodyAuthDeleted { .button { width: 320px; } .remove { color: var(--color-red) !important; } } + +.pageAuthMigrate { + .frame { width: 480px; } +} \ No newline at end of file diff --git a/src/scss/page/main/date.scss b/src/scss/page/main/date.scss index 91995c6aa7..c9ca443249 100644 --- a/src/scss/page/main/date.scss +++ b/src/scss/page/main/date.scss @@ -1,9 +1,17 @@ @import "~scss/_mixins"; +.pageMainDate { min-height: 100vh; } .pageMainDate { - .wrapper { width: 704px; margin: 0px auto; padding: 40px 0px 80px 0px; user-select: none; } + .wrapper { width: 704px; margin: 0px auto; padding: 52px 0px 80px 0px; user-select: none; } - .headSimple { align-items: center; height: 32px; } + .dayName { @include text-common; color: var(--color-text-secondary); margin: 0px 0px 8px 0px; display: flex; flex-direction: row; gap: 8px; align-items: center; } + .dayName { + div { display: flex; flex-direction: row; gap: 8px; align-items: center; } + div::after { content: ''; width: 3px; height: 3px; border-radius: 50%; background: var(--color-control-active); } + div:last-child::after { display: none; } + } + + .headSimple { align-items: center; height: 32px; margin-bottom: 20px; } .headSimple { .side.right { gap: 0px; } .side.right { @@ -14,15 +22,19 @@ } } - .categories { - display: flex; flex-direction: row; gap: 8px; margin: 0px 0px 12px 0px; align-items: center; justify-content: flex-start; flex-wrap: wrap; - } + .categories { display: flex; flex-direction: row; gap: 8px; margin: 0px 0px 12px 0px; align-items: center; justify-content: flex-start; flex-wrap: wrap; } .categories { + .button { border-radius: 8px; } .icon.mention { width: 20px; height: 20px; margin: 0px 6px 0px 0px; background-image: url('~img/icon/mention.svg'); } - .separator { content: ''; background-color: var(--color-shape-secondary); width: 1px; height: 24px; } } .cell.c-type { .iconObject { display: none; } } + + .emptyContainer { + text-align: center; align-content: center; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); color: var(--color-text-secondary); + } + + #loader { position: fixed; top: 50% !important; transform: translateY(-50%); } } \ No newline at end of file diff --git a/src/scss/page/main/graph.scss b/src/scss/page/main/graph.scss index f875d95c10..a1acb98a56 100644 --- a/src/scss/page/main/graph.scss +++ b/src/scss/page/main/graph.scss @@ -10,7 +10,7 @@ .wrapper { display: flex; height: 100%; overflow: hidden; } - #graphWrapper { width: 100%; height: 100%; } + .graphWrapper { width: 100%; height: 100%; } #graph { width: 100%; height: 100%; } canvas { width: 100%; height: 100%; background: var(--color-bg-primary); display: block; } diff --git a/src/scss/page/main/onboarding.scss b/src/scss/page/main/onboarding.scss index e38d23465f..ec91b0ed15 100644 --- a/src/scss/page/main/onboarding.scss +++ b/src/scss/page/main/onboarding.scss @@ -4,7 +4,7 @@ #vault, #sidebar, #sidebarToggle, - #navigationPanel, + #sidebarSync, #sidebarDummy { display: none; } body { background: #000; } diff --git a/src/scss/page/main/void.scss b/src/scss/page/main/void.scss index ab84b7ef63..639ac0f2d3 100644 --- a/src/scss/page/main/void.scss +++ b/src/scss/page/main/void.scss @@ -1,7 +1,7 @@ @import "~scss/_mixins"; .bodyMainVoid { - .navigationPanel, #sidebar { display: none; } + #sidebar { display: none; } } .pageMainVoid { @@ -10,7 +10,7 @@ .container { text-align: center; } .iconWrapper { display: flex; width: 320px; height: 104px; border-radius: 320px; margin: 0px auto 10px; align-items: center; justify-content: space-around; - background: radial-gradient(50% 50% at 50% 50%, #FFBCBC 26.04%, rgba(255, 230, 230, 0.00) 100%); + background: radial-gradient(50% 50% at 50% 50%, #ffbcbc 26.04%, rgba(255, 230, 230, 0.00) 100%); } .iconWrapper { .icon { width: 72px; height: 72px; background-image: url('~img/icon/popup/confirm/error.svg'); } diff --git a/src/scss/popup/confirm.scss b/src/scss/popup/confirm.scss index d9fac0f887..0796549bb9 100644 --- a/src/scss/popup/confirm.scss +++ b/src/scss/popup/confirm.scss @@ -17,9 +17,9 @@ .iconObject { margin-bottom: 12px; } .iconWrapper { display: flex; width: 320px; height: 104px; border-radius: 320px; margin: -16px auto 10px; align-items: center; justify-content: space-around; } - .iconWrapper.green { background: radial-gradient(50% 50% at 50% 50%, #A9F496 26.04%, rgba(188, 242, 175, 0.00) 100%); } - .iconWrapper.red { background: radial-gradient(50% 50% at 50% 50%, #FFBCBC 26.04%, rgba(255, 230, 230, 0.00) 100%); } - .iconWrapper.yellow { background: radial-gradient(50% 50% at 50% 50%, #FFF0C8 0%, rgba(255, 240, 200, 0.00) 100%); } + .iconWrapper.green { background: radial-gradient(50% 50% at 50% 50%, #a9f496 26.04%, rgba(188, 242, 175, 0.00) 100%); } + .iconWrapper.red { background: radial-gradient(50% 50% at 50% 50%, #ffbcbc 26.04%, rgba(255, 230, 230, 0.00) 100%); } + .iconWrapper.yellow { background: radial-gradient(50% 50% at 50% 50%, #fff0c8 0%, rgba(255, 240, 200, 0.00) 100%); } .iconWrapper.blue { background: radial-gradient(50% 50% at 50% 50%, #80d1ff 0%, rgba(187, 231, 255, 0.00) 100%); } .iconWrapper { @@ -31,6 +31,7 @@ .icon.invite { width: 68px; background-image: url('~img/icon/popup/confirm/invite.svg'); } .icon.sad { width: 68px; background-image: url('~img/icon/popup/confirm/sad.svg'); } .icon.warning { width: 96px; background-image: url('~img/icon/popup/confirm/warning.svg'); } + .icon.warningInverted { width: 96px; background-image: url('~img/icon/popup/confirm/warningInverted.svg'); } } .checkboxWrapper { @@ -41,6 +42,8 @@ } ul { padding-left: 1.25em; } + + .error { margin-bottom: 0px; } } .popup.popupConfirm.isWide { diff --git a/src/scss/popup/settings.scss b/src/scss/popup/settings.scss index f6d3b1b46d..3fbc511c37 100644 --- a/src/scss/popup/settings.scss +++ b/src/scss/popup/settings.scss @@ -344,10 +344,7 @@ > .side.right.tabPhrase { .inputs { margin: 0px 0px 54px 0px; } - .qrWrap { padding: 8px; border-radius: 4px; width: 132px; height: 132px; background: var(--color-shape-tertiary); overflow: hidden; } - .qrWrap { - canvas { width: 100%; height: 100%; } - } + .qrWrap { width: 130px; overflow: hidden; } } > .side.right.tabDataManagement { diff --git a/src/scss/theme/dark/block.scss b/src/scss/theme/dark/block.scss index 5db423fe16..5fa5c7c80f 100644 --- a/src/scss/theme/dark/block.scss +++ b/src/scss/theme/dark/block.scss @@ -194,8 +194,12 @@ /* BlockChat */ .block.blockChat { + .emptyState { + .img { background: none; } + } .message { .side.right { background: var(--color-shape-tertiary); } + .reply { background: #3c3c3c; } .icon.reactionAdd:hover, .icon.reactionAdd.hover { background-image: url('#{$themePath}/icon/chat/buttons/reaction1.svg'); } @@ -203,11 +207,10 @@ .icon.messageReply:hover, .icon.messageReply.hover { background-image: url('#{$themePath}/icon/chat/buttons/reply1.svg'); } .icon.more:hover, .icon.more.hover { background-image: url('#{$themePath}/icon/menu/action/more1.svg'); } } - - .reply { color: var(--color-text-inversion); } } .message.isSelf { - .side.right { background: #162908; } + .side.right { background: #202919; } + .reply { background: #36551d; } } .form { @@ -220,6 +223,10 @@ .swiper-button-next::before { box-shadow: none; } .swiper-button-prev::after, .swiper-button-next::after { background-image: url('#{$themePath}/arrow/chatFormAttachment.svg'); } + + .attachment { + .icon.remove { background-image: url('#{$themePath}/icon/chat/buttons/remove.svg'); } + } } } } diff --git a/src/scss/theme/dark/common.scss b/src/scss/theme/dark/common.scss index abd3b79782..61d629d1b0 100644 --- a/src/scss/theme/dark/common.scss +++ b/src/scss/theme/dark/common.scss @@ -38,7 +38,7 @@ html.themeDark { --color-system-accent-125: #ffc83c; --color-system-accent-50: #9f6b00; --color-system-accent-25: #694500; - --color-system-selection: rgba(24, 163, 241, 0.15); + --color-system-selection: rgba(24, 163, 241, 0.3); /* Common */ @@ -322,13 +322,6 @@ html.themeDark { } } - /* Navigation */ - - .navigationPanel { background-color: rgba(0, 0, 0, 0.6); } - .navigationPanel { - .iconWrap:not(.disabled):hover, .iconWrap.active { background-color: rgba(255, 255, 255, 0.1); } - } - /* Notifications */ .notifications { diff --git a/src/scss/theme/dark/menu.scss b/src/scss/theme/dark/menu.scss index 9860ef6404..1fea8b8d12 100644 --- a/src/scss/theme/dark/menu.scss +++ b/src/scss/theme/dark/menu.scss @@ -18,12 +18,6 @@ .icon.textNumbered { background-image: url('#{$themePath}/icon/menu/action/block/text/numbered0.svg'); } .icon.textToggle { background-image: url('#{$themePath}/icon/menu/action/block/text/toggle0.svg'); } - .icon.widget-0 { background-image: url('#{$themePath}/icon/menu/widget/link.svg'); } - .icon.widget-1 { background-image: url('#{$themePath}/icon/menu/widget/tree.svg'); } - .icon.widget-2 { background-image: url('#{$themePath}/icon/menu/widget/list.svg'); } - .icon.widget-3 { background-image: url('#{$themePath}/icon/menu/widget/compact.svg'); } - .icon.widget-4 { background-image: url('#{$themePath}/icon/menu/widget/view.svg'); } - .icon.more { background-image: url('#{$themePath}/icon/menu/action/more0.svg'); } .icon.color { @@ -311,4 +305,18 @@ } } + /* Widget */ + + .menu.menuWidget { + .options { + .option { + .icon.widget-0 { background-image: url('#{$themePath}/icon/menu/widget/link.svg'); } + .icon.widget-1 { background-image: url('#{$themePath}/icon/menu/widget/tree.svg'); } + .icon.widget-2 { background-image: url('#{$themePath}/icon/menu/widget/list.svg'); } + .icon.widget-3 { background-image: url('#{$themePath}/icon/menu/widget/compact.svg'); } + .icon.widget-4 { background-image: url('#{$themePath}/icon/menu/widget/view.svg'); } + } + } + } + } diff --git a/src/scss/theme/dark/widget.scss b/src/scss/theme/dark/widget.scss index 2d30b59395..7b5c633486 100644 --- a/src/scss/theme/dark/widget.scss +++ b/src/scss/theme/dark/widget.scss @@ -2,7 +2,7 @@ .icon.back { background-image: url('#{$themePath}/icon/widget/back.svg'); } .icon.collapse { background-image: url('#{$themePath}/icon/widget/collapse.svg'); } .icon.options { background-image: url('#{$themePath}/icon/widget/options.svg'); } - .icon.plus { background-image: url('#{$themePath}/icon/widget/plus.svg'); } + .icon.plus { background-image: url('#{$themePath}/icon/widget/plus0.svg'); } .icon.remove .inner { background-image: url('#{$themePath}/icon/widget/remove.svg'); } @@ -51,6 +51,8 @@ .group { .clickable { .icon.arrow { background-image: url('~img/arrow/select/light.svg'); } + .icon.plus { background-image: url('#{$themePath}/icon/widget/plus0.svg'); } + .icon.plus:hover { background-image: url('#{$themePath}/icon/widget/plus1.svg'); } } } } diff --git a/src/scss/widget/buttons.scss b/src/scss/widget/buttons.scss deleted file mode 100644 index 2a022f48c7..0000000000 --- a/src/scss/widget/buttons.scss +++ /dev/null @@ -1,42 +0,0 @@ -@import "~scss/_mixins"; - -.widget.widgetButtons { padding: 8px; } -.widget.widgetButtons { - .icon.remove { display: none !important; } - - .body { - .item { padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; position: relative; } - .item::before { - content: ""; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-shape-highlight-medium); z-index: 1; pointer-events: none; - border-radius: 6px; opacity: 0; - } - .item:hover::before, .item.hover::before { opacity: 1; } - - .item { - .side { display: flex; flex-direction: row; align-items: center; } - .side.left { gap: 0px 6px; } - .side.right { justify-content: flex-end; padding-left: 6px; overflow: hidden; } - .side.right { - .btn { background-image: linear-gradient(90deg, #2aa7ee, #27c941); background-clip: text; -webkit-text-fill-color: transparent; font-weight: 500; } - } - - .name { display: flex; gap: 0px 8px; @include text-overflow-nw; } - - .icon { width: 20px; height: 20px; flex-shrink: 0; } - .icon.member { background-image: url('~img/icon/widget/button/member.svg'); } - .icon.all { background-image: url('~img/icon/widget/button/all.svg'); } - .icon.chat { background-image: url('~img/icon/widget/button/chat.svg'); } - - .cnt { color: var(--color-text-secondary); } - } - .item:hover { - .side.right { - .icon.more { opacity: 1; } - } - } - } - - .body.withCnt { - .side.left { width: calc(100% - 30px); } - } -} \ No newline at end of file diff --git a/src/scss/widget/common.scss b/src/scss/widget/common.scss index be7dfe5d5a..4979969d07 100644 --- a/src/scss/widget/common.scss +++ b/src/scss/widget/common.scss @@ -24,7 +24,7 @@ .icon.back { background-image: url('~img/icon/widget/back.svg'); } .icon.options { background-image: url('~img/icon/widget/options.svg'); } .icon.collapse { background-image: url('~img/icon/widget/collapse.svg'); } - .icon.plus { background-image: url('~img/icon/widget/plus.svg'); } + .icon.plus { background-image: url('~img/icon/widget/plus0.svg'); } .icon.collapse.isClosed { transform: rotateZ(-90deg); } .buttons { flex-shrink: 0; flex-direction: row; align-items: center; gap: 0px 6px; display: none; position: relative; z-index: 2; } @@ -116,6 +116,5 @@ } @import "./space"; -@import "./buttons"; @import "./tree"; @import "./view/common"; \ No newline at end of file diff --git a/src/scss/widget/space.scss b/src/scss/widget/space.scss index 5a84e72bb6..d4ea3eb753 100644 --- a/src/scss/widget/space.scss +++ b/src/scss/widget/space.scss @@ -1,30 +1,69 @@ @import "~scss/_mixins"; -.widget.widgetSpace { padding: 16px; } +.widget.widgetSpace { padding: 8px; } .widget.widgetSpace { .icon.remove { display: none !important; } - .body { display: flex; flex-direction: row; align-items: center; gap: 0px 12px; justify-content: stretch; } .body { - .side.left { display: flex; flex-direction: row; align-items: center; gap: 0px 12px; width: 100%; } - .side.left { - .iconObject { flex-shrink: 0; } - .iconObject:not(.withOption) { background-color: var(--color-shape-tertiary); } + .sides { display: flex; flex-direction: row; align-items: center; gap: 0px 12px; justify-content: stretch; padding: 4px 4px 4px 8px; } + .sides { + .side.left { display: flex; flex-direction: row; align-items: center; gap: 0px 6px; width: 100%; flex-grow: 1; overflow: hidden; } + .side.left { + .iconObject { flex-shrink: 0; } + .iconObject:not(.withOption) { background-color: var(--color-shape-tertiary); } - .txt { flex-grow: 1; width: calc(100% - 52px) } - .name { @include text-paragraph; @include text-overflow-nw; font-weight: 600; } + .txt { flex-grow: 1; width: calc(100% - 52px) } + .name { @include text-paragraph; @include text-overflow-nw; font-weight: 600; } + } + + .side.right { flex-shrink: 0; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 0px 4px; } + .side.right { + .cnt { + @include text-very-small; background-color: var(--color-control-active); color: var(--color-control-bg); border-radius: 50%; min-width: 18px; + text-align: center; font-weight: 500; height: 18px; line-height: 18px; display: none; padding: 0px 2px; flex-shrink: 0; + } + + .icon { width: 24px; height: 24px; flex-shrink: 0; } + .icon.search { background-image: url('~img/icon/widget/button/search.svg'); } + .icon.plus { background-image: url('~img/icon/widget/button/plus.svg'); } + } } - .side.right { flex-shrink: 0; } - .side.right { - .cnt { - @include text-very-small; background-color: var(--color-control-accent); color: var(--color-control-bg); border-radius: 50%; min-width: 18px; - text-align: center; font-weight: 500; height: 18px; line-height: 18px; + .buttons { + .item { padding: 4px 8px; display: flex; align-items: center; justify-content: space-between; position: relative; } + .item::before { + content: ""; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-shape-highlight-medium); z-index: 1; pointer-events: none; + border-radius: 6px; opacity: 0; + } + .item:hover::before, .item.hover::before { opacity: 1; } + + .item { + .side { display: flex; flex-direction: row; align-items: center; } + .side.left { gap: 0px 6px; } + + .name { display: flex; gap: 0px 8px; @include text-overflow-nw; } + + .icon { width: 20px; height: 20px; flex-shrink: 0; } + .icon.member { background-image: url('~img/icon/widget/button/member.svg'); } + .icon.all { background-image: url('~img/icon/widget/button/all.svg'); } + .icon.chat { background-image: url('~img/icon/widget/button/chat.svg'); } + + .cnt { color: var(--color-text-secondary); } + } + .item:hover { + .side.right { + .icon.more { opacity: 1; } + } } } } .body.withCnt { - .side.left { width: calc(100% - 30px); } + .sides { + .side.left { width: calc(100% - 30px); } + .side.right { + .cnt { display: block; } + } + } } } \ No newline at end of file diff --git a/src/scss/widget/view/board.scss b/src/scss/widget/view/board.scss index 3867317675..914e2d70ca 100644 --- a/src/scss/widget/view/board.scss +++ b/src/scss/widget/view/board.scss @@ -4,7 +4,7 @@ .body { padding: 0px 8px; } .group { - .clickable { display: flex; flex-direction: row; align-items: center; height: 28px; padding: 0px 8px 0px 4px; gap: 0px 4px; position: relative; } + .clickable { display: flex; flex-direction: row; align-items: center; height: 28px; padding: 0px 8px 0px 4px; gap: 0px 2px; position: relative; } .clickable::before { content: ""; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: var(--color-shape-highlight-medium); z-index: 1; pointer-events: none; opacity: 0; border-radius: 4px; transition: $transitionAllCommon; @@ -12,11 +12,11 @@ .clickable:hover::before { opacity: 1; } .clickable { - .icon { width: 24px; height: 24px; background-size: 20px; flex-shrink: 0; transition: $transitionAllCommon; border-radius: 4px; } + .icon { width: 20px; height: 20px; flex-shrink: 0; transition: $transitionAllCommon; } .icon.arrow { background-image: url('~img/arrow/select/dark.svg'); transform: rotate(-90deg); } - .icon.plus { background-image: url('~img/icon/widget/plus.svg'); opacity: 0; } - .icon.plus:hover { background-color: var(--color-shape-highlight-medium); } + .icon.plus { width: 24px; height: 24px; background-size: 20px; background-image: url('~img/icon/widget/plus0.svg'); opacity: 0; } + .icon.plus:hover { background-image: url('~img/icon/widget/plus1.svg'); } .cellContent { flex-grow: 1; width: calc(100% - 24px); } .cellContent { diff --git a/src/scss/widget/view/graph.scss b/src/scss/widget/view/graph.scss index 4d5d9e75a5..f4521122f7 100644 --- a/src/scss/widget/view/graph.scss +++ b/src/scss/widget/view/graph.scss @@ -2,6 +2,6 @@ .viewGraph { padding: 0px; } .viewGraph { - #graphWrapper { height: 240px; } + .graphWrapper { height: 240px; } canvas { width: 100%; height: 100%; } } \ No newline at end of file diff --git a/src/ts/app.tsx b/src/ts/app.tsx index ea0709d831..6013e58e2b 100644 --- a/src/ts/app.tsx +++ b/src/ts/app.tsx @@ -8,8 +8,8 @@ import { Router, Route, Switch } from 'react-router-dom'; import { Provider } from 'mobx-react'; import { configure, spy } from 'mobx'; import { enableLogging } from 'mobx-logger'; -import { Page, SelectionProvider, DragProvider, Progress, Toast, Preview as PreviewIndex, Navigation, ListPopup, ListMenu, ListNotification, Sidebar, Vault, ShareTooltip, Loader } from 'Component'; -import { I, C, S, U, J, keyboard, Storage, analytics, dispatcher, translate, Renderer, focus, Preview, Mark, Animation, Onboarding, Survey, Encode, Decode, sidebar } from 'Lib'; +import { Page, SelectionProvider, DragProvider, Progress, Toast, Preview as PreviewIndex, ListPopup, ListMenu, ListNotification, Sidebar, Vault, Loader } from 'Component'; +import { I, C, S, U, J, M, keyboard, Storage, analytics, dispatcher, translate, Renderer, focus, Preview, Mark, Animation, Onboarding, Survey, Encode, Decode, sidebar } from 'Lib'; require('pdfjs-dist/build/pdf.worker.entry.js'); @@ -75,6 +75,7 @@ if (!isPackaged) { C, S, U, + M, analytics, dispatcher, keyboard, @@ -138,9 +139,8 @@ class RoutePage extends React.Component<RouteComponentProps> { <ListPopup key="listPopup" {...this.props} /> <ListMenu key="listMenu" {...this.props} /> - <Navigation ref={ref => S.Common.refSet('navigation', ref)} key="navigation" {...this.props} /> <Sidebar key="sidebar" {...this.props} /> - <Page {...this.props} /> + <Page {...this.props} isPopup={false} /> </DragProvider> </SelectionProvider> ); @@ -204,7 +204,6 @@ class App extends React.Component<object, State> { <Progress /> <Toast /> <ListNotification key="listNotification" /> - <ShareTooltip showOnce={true} /> <Vault ref={ref => S.Common.refSet('vault', ref)} /> <Switch> @@ -239,16 +238,6 @@ class App extends React.Component<object, State> { console.log('[App] version:', version.app, 'isPackaged', isPackaged); }; - initStorage () { - const lastSurveyTime = Number(Storage.get('lastSurveyTime')) || 0; - - if (!lastSurveyTime) { - Storage.set('lastSurveyTime', U.Date.now()); - }; - - Storage.delete('lastSurveyCanceled'); - }; - registerIpcEvents () { Renderer.on('init', this.onInit); Renderer.on('route', (e: any, route: string) => this.onRoute(route)); @@ -318,13 +307,12 @@ class App extends React.Component<object, State> { S.Common.dataPathSet(dataPath); analytics.init(); - this.initStorage(); if (redirect) { Storage.delete('redirect'); }; - if (css) { + if (css && !config.disableCss) { U.Common.injectCss('anytype-custom-css', css); }; diff --git a/src/ts/component/block/chat.tsx b/src/ts/component/block/chat.tsx index 32128fb068..5d03da07bb 100644 --- a/src/ts/component/block/chat.tsx +++ b/src/ts/component/block/chat.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import $ from 'jquery'; -import raf from 'raf'; import { observer } from 'mobx-react'; import { Label, Icon } from 'Component'; import { I, C, S, U, J, keyboard, translate, Storage, Preview, Mark } from 'Lib'; @@ -9,7 +8,6 @@ import Message from './chat/message'; import Form from './chat/form'; interface State { - threadId: string; isLoading: boolean; }; @@ -23,18 +21,20 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon refForm = null; deps: string[] = null; replies: string[] = null; + isLoaded = false; + isLoading = false; + isBottom = true; messageRefs: any = {}; timeoutInterface = 0; + timeoutScroll = 0; top = 0; state = { - threadId: '', isLoading: false, }; constructor (props: I.BlockComponent) { super(props); - this.onThread = this.onThread.bind(this); this.onScroll = this.onScroll.bind(this); this.onDragOver = this.onDragOver.bind(this); this.onDragLeave = this.onDragLeave.bind(this); @@ -42,13 +42,13 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon this.onContextMenu = this.onContextMenu.bind(this); this.scrollToMessage = this.scrollToMessage.bind(this); this.scrollToBottom = this.scrollToBottom.bind(this); + this.scrollToBottomCheck = this.scrollToBottomCheck.bind(this); this.getMessages = this.getMessages.bind(this); this.getReplyContent = this.getReplyContent.bind(this); }; render () { const { showRelativeDates } = S.Common; - const { threadId } = this.state; const rootId = this.getRootId(); const blockId = this.getBlockId(); const messages = this.getMessages(); @@ -75,9 +75,9 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon id={item.id} rootId={rootId} blockId={blockId} + subId={subId} isNew={item.id == lastId} - isThread={!!threadId} - onThread={this.onThread} + scrollToBottom={this.scrollToBottomCheck} onContextMenu={e => this.onContextMenu(e, item)} onMore={e => this.onContextMenu(e, item, true)} onReplyEdit={e => this.onReplyEdit(e, item)} @@ -116,7 +116,7 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon rootId={rootId} blockId={blockId} subId={subId} - scrollToBottom={this.scrollToBottom} + scrollToBottom={this.scrollToBottomCheck} scrollToMessage={this.scrollToMessage} getMessages={this.getMessages} getReplyContent={this.getReplyContent} @@ -133,7 +133,7 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon this.rebind(); this.setState({ isLoading: true }); - this.loadMessages(true, () => { + this.loadMessages(1, true, () => { this.loadReplies(() => { this.replies = this.getReplies(); @@ -181,7 +181,7 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon unbind () { const { isPopup, block } = this.props; - const events = [ 'messageAdd' ]; + const events = [ 'messageAdd', 'messageUpdate', 'reactionUpdate' ]; const ns = block.id + U.Common.getEventNamespace(isPopup); $(window).off(events.map(it => `${it}.${ns}`).join(' ')); @@ -190,52 +190,108 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon rebind () { const { isPopup, block } = this.props; - const { account } = S.Auth; const win = $(window); const ns = block.id + U.Common.getEventNamespace(isPopup); this.unbind(); - win.on(`messageAdd.${ns}`, (e, message: I.ChatMessage) => { - if (message.creator != account.id) { - this.scrollToMessage(message.id); - }; - }); + win.on(`messageAdd.${ns}`, () => this.scrollToBottomCheck()); + win.on(`messageUpdate.${ns}`, () => this.scrollToBottomCheck()); + win.on(`reactionUpdate.${ns}`, () => this.scrollToBottomCheck()); U.Common.getScrollContainer(isPopup).on(`scroll.${ns}`, e => this.onScroll(e)); }; - loadMessages (clear: boolean, callBack?: () => void) { + subscribeMessages (clear: boolean, callBack?: () => void) { + const rootId = this.getRootId(); + + C.ChatSubscribeLastMessages(rootId, J.Constant.limit.chat.messages, (message: any) => { + if (message.error.code) { + if (callBack) { + callBack(); + }; + return; + }; + + const messages = message.messages || []; + + if (messages.length && clear) { + S.Chat.set(rootId, messages); + this.forceUpdate(); + }; + + if (callBack) { + callBack(); + }; + }); + }; + + loadMessages (dir: number, clear: boolean, callBack?: () => void) { const rootId = this.getRootId(); - const list = this.getMessages(); - if (!rootId) { + if (!rootId || this.isLoading) { return; }; + if (!clear && (dir > 0) && this.isLoaded) { + this.setIsBottom(true); + return; + }; + + this.isLoading = true; + if (clear) { - C.ChatSubscribeLastMessages(rootId, J.Constant.limit.chat.messages, (message: any) => { - if (!message.error.code) { - S.Chat.set(rootId, message.messages); - this.forceUpdate(); - }; + this.subscribeMessages(clear, () => { + this.isLoading = false; if (callBack) { callBack(); }; }); } else { + const list = this.getMessages(); if (!list.length) { return; }; - const first = list[0]; + const before = dir < 0 ? list[0].orderId : ''; + const after = dir > 0 ? list[list.length - 1].orderId : ''; - C.ChatGetMessages(rootId, first.orderId, J.Constant.limit.chat.messages, (message: any) => { - if (!message.error.code && message.messages.length) { - S.Chat.prepend(rootId, message.messages); + if (!before && !after) { + return; + }; + + C.ChatGetMessages(rootId, before, after, J.Constant.limit.chat.messages, (message: any) => { + this.isLoading = false; + + if (message.error.code) { + this.isLoaded = true; - this.scrollToMessage(first.id); + if (callBack) { + callBack(); + }; + return; + }; + + const messages = message.messages || []; + + if (dir > 0) { + if (!messages.length) { + this.isLoaded = true; + this.setIsBottom(true); + this.subscribeMessages(false); + } else { + this.setIsBottom(false); + }; + } else { + this.setIsBottom(false); + }; + + if (messages.length) { + const scrollTo = dir < 0 ? messages[0].id : messages[messages.length - 1].id; + + S.Chat[(dir < 0 ? 'prepend' : 'append')](rootId, messages); + this.scrollToMessage(scrollTo); }; if (callBack) { @@ -339,7 +395,7 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon }; getBlockId () { - return this.state.threadId || this.props.block.id; + return this.props.block.id; }; getSections () { @@ -392,15 +448,17 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon onContextMenu (e: React.MouseEvent, item: any, onMore?: boolean) { const { readonly } = this.props; - const { account } = S.Auth; - const blockId = this.getBlockId(); - const message = `#block-${blockId} #item-${item.id}`; - const isSelf = item.creator == account.id; - if (readonly) { return; }; + + const { isPopup } = this.props; + const { account } = S.Auth; + const blockId = this.getBlockId(); + const message = `#block-${blockId} #item-${item.id}`; + const container = isPopup ? U.Common.getScrollContainer(isPopup) : $('body'); + const isSelf = item.creator == account.id; const options: any[] = [ { id: 'reply', name: translate('blockChatReply') }, isSelf ? { id: 'edit', name: translate('commonEdit') } : null, @@ -409,9 +467,15 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon const menuParam: Partial<I.MenuParam> = { vertical: I.MenuDirection.Bottom, - horizontal: I.MenuDirection.Left, - onOpen: () => $(message).addClass('hover'), - onClose: () => $(message).removeClass('hover'), + horizontal: onMore ? I.MenuDirection.Center : I.MenuDirection.Left, + onOpen: () => { + $(message).addClass('hover'); + container.addClass('over'); + }, + onClose: () => { + $(message).removeClass('hover'); + container.removeClass('over'); + }, data: { options, onSelect: (e, option) => { @@ -447,13 +511,16 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon onScroll (e: any) { const { isPopup } = this.props; const node = $(this.node); + const scrollWrapper = node.find('#scrollWrapper'); + const formWrapper = node.find('#formWrapper'); const rootId = this.getRootId(); const container = U.Common.getScrollContainer(isPopup); const st = container.scrollTop(); const co = isPopup ? container.offset().top : 0; - const ch = container.outerHeight(); const messages = this.getMessages(); const dates = node.find('.section > .date'); + const fh = formWrapper.outerHeight(); + const ch = container.outerHeight(); const hh = J.Size.header; const lastId = Storage.getChat(rootId).lastId; @@ -475,8 +542,14 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon }); }; + this.setIsBottom(false); + if (st <= 0) { - this.loadMessages(false); + this.loadMessages(-1, false); + }; + + if (st - fh >= scrollWrapper.outerHeight() - ch) { + this.loadMessages(1, false); }; dates.each((i, item: any) => { @@ -512,51 +585,87 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon }; scrollToMessage (id: string) { - window.setTimeout(() => { - const container = U.Common.getScrollContainer(this.props.isPopup); - const top = this.getMessageScrollOffset(id); + if (!id) { + return; + }; + + const container = U.Common.getScrollContainer(this.props.isPopup); + const top = this.getMessageScrollOffset(id); - container.get(0).scrollTo({ top }); - }, 50); + container.scrollTop(top - container.height() / 2); }; scrollToBottom () { - window.setTimeout(() => { - const { isPopup } = this.props; - const container = U.Common.getScrollContainer(isPopup); - const height = isPopup ? container.get(0).scrollHeight : document.body.scrollHeight; + const { isPopup } = this.props; + const container = U.Common.getScrollContainer(isPopup); + const node = $(this.node); + const wrapper = node.find('#scrollWrapper'); - container.get(0).scrollTo({ top: height + 10000 }); - }, 50); + container.scrollTop(wrapper.outerHeight()); }; - onThread (id: string) { - this.setState({ threadId: id }, () => { - this.scrollToBottom(); - }); + scrollToBottomCheck () { + if (this.isBottom) { + window.clearTimeout(this.timeoutScroll); + this.timeoutScroll = window.setTimeout(() => this.scrollToBottom(), 10); + }; }; onReplyEdit (e: React.MouseEvent, message: any) { this.refForm.onReply(message); + this.scrollToBottomCheck(); }; - onReplyClick (e: React.MouseEvent, message: any) { + onReplyClick (e: React.MouseEvent, item: any) { if (!S.Common.config.experimental) { return; }; + this.isLoaded = false; + this.setIsBottom(false); + const rootId = this.getRootId(); - const reply = S.Chat.getReply(rootId, message.replyToMessageId); const limit = Math.ceil(J.Constant.limit.chat.messages / 2); - let messages = []; + C.ChatGetMessagesByIds(rootId, [ item.replyToMessageId ], (message: any) => { + if (message.error.code || !message.messages.length) { + return; + }; - C.ChatGetMessages(rootId, reply.orderId, limit, (message: any) => { - if (!message.error.code && message.messages.length) { - messages = messages.concat(message.messages); + const reply = message.messages[0]; + if (!reply) { + return; }; - S.Chat.set(rootId, messages); + let list = []; + + S.Chat.clear(rootId); + + C.ChatGetMessages(rootId, reply.orderId, '', limit, (message: any) => { + if (!message.error.code && message.messages.length) { + list = list.concat(message.messages); + }; + + list.push(reply); + + C.ChatGetMessages(rootId, '', reply.orderId, limit, (message: any) => { + if (!message.error.code && message.messages.length) { + list = list.concat(message.messages); + }; + + S.Chat.set(rootId, list); + + this.loadReplies(() => { + this.replies = this.getReplies(); + + this.loadDeps(() => { + this.deps = this.getDeps(); + + this.scrollToMessage(reply.id); + }); + }); + }); + }); }); }; @@ -590,19 +699,20 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon } else { let attachmentLayout = I.ObjectLayout[first.layout]; - attachment = first; + attachment = null; attachments.forEach((el) => { if ((I.ObjectLayout[el.layout] != attachmentLayout) || !layouts.includes(el.layout)) { isMultiple = true; - attachment = null; + attachment = first; attachmentLayout = 'Attachment'; }; }); - attachmentText = `${U.Common.plural(l, translate(`plural${attachmentLayout}`))} (${l})`; + attachmentText = text.length ? `${U.Common.plural(l, translate(`plural${attachmentLayout}`))} (${l})` : `${l} ${U.Common.plural(l, translate(`plural${attachmentLayout}`)).toLowerCase()}`; }; if (!text) { text = attachmentText; + attachment = first; }; return { title, text, attachment, isMultiple }; @@ -620,6 +730,10 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon this.refForm?.onDrop(e); }; + setIsBottom (v: boolean) { + this.isBottom = v; + }; + }); export default BlockChat; diff --git a/src/ts/component/block/chat/attachment/index.tsx b/src/ts/component/block/chat/attachment/index.tsx index 341291ea87..29eabe946c 100644 --- a/src/ts/component/block/chat/attachment/index.tsx +++ b/src/ts/component/block/chat/attachment/index.tsx @@ -1,26 +1,36 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { IconObject, Icon, ObjectName, ObjectDescription, ObjectType, MediaVideo, MediaAudio } from 'Component'; -import { I, U, S, J, Action } from 'Lib'; +import { IconObject, Icon, ObjectName, ObjectDescription, ObjectType, MediaVideo, MediaAudio, Loader } from 'Component'; +import { I, U, S, J, Action, analytics, keyboard } from 'Lib'; interface Props { object: any; showAsFile?: boolean; bookmarkAsDefault?: boolean; + subId?: string; + scrollToBottom?: () => void; onRemove: (id: string) => void; onPreview?: (data: any) => void; }; -const ChatAttachment = observer(class ChatAttachment extends React.Component<Props> { +interface State { + isLoaded: boolean; +}; + +const ChatAttachment = observer(class ChatAttachment extends React.Component<Props, State> { node = null; src = ''; previewItem: any = null; + state = { + isLoaded: false, + }; constructor (props: Props) { super(props); this.onOpen = this.onOpen.bind(this); + this.onContextMenu = this.onContextMenu.bind(this); this.onOpenBookmark = this.onOpenBookmark.bind(this); this.onPreview = this.onPreview.bind(this); this.onRemove = this.onRemove.bind(this); @@ -105,6 +115,7 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro <div ref={node => this.node = node} className={cn.join(' ')} + onContextMenu={this.onContextMenu} > {content} <Icon className="remove" onClick={this.onRemove} /> @@ -168,23 +179,24 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro <ObjectName object={object} /> <ObjectDescription object={object} /> </div> - <div className="side right"> - {picture ? <img src={S.Common.imageUrl(picture, 500)} className="img" /> : ''} - </div> + + {picture ? ( + <div className="side right"> + <img src={S.Common.imageUrl(picture, 500)} className="img" /> + </div> + ) : ''} </div> ); }; renderImage () { - const { object } = this.props; - - this.previewItem = { type: I.FileType.Image, object }; + const { object, scrollToBottom } = this.props; + const { isLoaded } = this.state; if (!this.src) { if (object.isTmp && object.file) { - U.File.loadPreviewBase64(object.file, { type: 'jpg', quality: 99, maxWidth: J.Size.image }, (image: string, param: any) => { + U.File.loadPreviewBase64(object.file, { type: 'jpg', quality: 99, maxWidth: J.Size.image }, (image: string) => { this.src = image; - this.previewItem.src = image; $(this.node).find('#image').attr({ 'src': image }); }); this.src = './img/space.svg'; @@ -193,25 +205,29 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro }; }; - this.previewItem.src = this.src; + if (!isLoaded) { + const img = new Image(); + img.onload = () => this.setState({ isLoaded: true }); + img.src = this.src; + }; - return ( + return isLoaded ? ( <img id="image" className="image" src={this.src} onClick={this.onPreview} + onLoad={scrollToBottom} onDragStart={e => e.preventDefault()} + style={{ aspectRatio: `${object.widthInPixels} / ${object.heightInPixels}` }} /> - ); + ) : <Loader />; }; renderVideo () { const { object } = this.props; const src = S.Common.fileUrl(object.id); - this.previewItem = { type: I.FileType.Video, src, object }; - return ( <MediaVideo src={src} @@ -230,32 +246,76 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro return <MediaAudio playlist={playlist} />; }; + componentDidUpdate (prevProps: Readonly<Props>, prevState: Readonly<State>): void { + const { scrollToBottom } = this.props; + + if (!prevState.isLoaded && this.state.isLoaded && scrollToBottom) { + scrollToBottom(); + }; + }; + onOpen () { const { object } = this.props; - if (!object.isTmp) { - U.Object.openPopup(object); + switch (object.layout) { + case I.ObjectLayout.Bookmark: { + this.onOpenBookmark(); + break; + }; + + case I.ObjectLayout.Video: + case I.ObjectLayout.Image: { + this.onPreview(); + break; + }; + + case I.ObjectLayout.File: + case I.ObjectLayout.Pdf: + case I.ObjectLayout.Audio: { + Action.openFile(object.id, analytics.route.chat); + break; + }; + + default: { + if (!object.isTmp) { + U.Object.openPopup(object); + }; + break; + }; }; }; - onOpenBookmark () { - const { object } = this.props; - const { source } = object; + onContextMenu (e: any) { + e.stopPropagation(); + + const { object, subId } = this.props; + + S.Menu.open('dataviewContext', { + recalcRect: () => { + const { x, y } = keyboard.mouse.page; + return { width: 0, height: 0, x: x + 4, y: y }; + }, + data: { + objectIds: [ object.id ], + subId, + allowedLinkTo: true, + allowedOpen: true, + } + }); + }; - Action.openUrl(source); + onOpenBookmark () { + Action.openUrl(this.props.object.source); }; onPreview () { const { onPreview } = this.props; - - if (!this.previewItem) { - return; - }; + const item = this.getPreviewItem(); if (onPreview) { - onPreview(this.previewItem); + onPreview(item); } else { - S.Popup.open('preview', { data: { gallery: [ this.previewItem ] } }); + S.Popup.open('preview', { data: { gallery: [ item ] } }); }; }; @@ -267,7 +327,24 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro }; getPreviewItem () { - return this.previewItem; + const { object } = this.props; + const ret: any = { object }; + + switch (object.layout) { + case I.ObjectLayout.Image: { + ret.type = I.FileType.Image; + ret.src = this.src || S.Common.imageUrl(object.id, J.Size.image); + break; + }; + + case I.ObjectLayout.Video: { + ret.type = I.FileType.Video; + ret.src = S.Common.fileUrl(object.id); + break; + }; + + }; + return ret; }; }); diff --git a/src/ts/component/block/chat/form.tsx b/src/ts/component/block/chat/form.tsx index 2796eed2e2..f663b71812 100644 --- a/src/ts/component/block/chat/form.tsx +++ b/src/ts/component/block/chat/form.tsx @@ -4,7 +4,7 @@ import sha1 from 'sha1'; import raf from 'raf'; import { observer } from 'mobx-react'; import { Editable, Icon, IconObject, Loader } from 'Component'; -import { I, C, S, U, J, keyboard, Mark, translate, Storage } from 'Lib'; +import { I, C, S, U, J, keyboard, Mark, translate, Storage, Preview } from 'Lib'; import { Swiper, SwiperSlide } from 'swiper/react'; import { Navigation } from 'swiper/modules'; @@ -22,6 +22,7 @@ interface Props extends I.BlockComponent { interface State { attachments: any[]; + charCounter: number; }; const ChatForm = observer(class ChatForm extends React.Component<Props, State> { @@ -30,14 +31,18 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { node = null; refEditable = null; refButtons = null; + refCounter = null; + isLoading = []; marks: I.Mark[] = []; range: I.TextRange = { from: 0, to: 0 }; timeoutFilter = 0; editingId: string = ''; replyingId: string = ''; swiper = null; + speedLimit = { last: 0, counter: 0 } state = { attachments: [], + charCounter: 0, }; constructor (props: Props) { @@ -76,7 +81,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { render () { const { rootId, readonly, getReplyContent } = this.props; - const { attachments } = this.state; + const { attachments, charCounter } = this.state; const { space } = S.Common; const value = this.getTextValue(); @@ -115,7 +120,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { icon = <IconObject className={iconSize ? 'noBg' : ''} object={object} size={32} iconSize={iconSize} />; }; - if (reply.isMultiple) { + if (reply.isMultiple && !reply.attachment) { icon = <Icon className="isMultiple" />; }; onClear = this.onReplyClear; @@ -149,6 +154,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { <Editable ref={ref => this.refEditable = ref} id="messageBox" + classNameWrap="customScrollbar" maxLength={J.Constant.limit.chat.text} placeholder={translate('blockChatPlaceholder')} onSelect={this.onSelect} @@ -201,6 +207,8 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { removeBookmark={this.removeBookmark} /> + <div ref={ref => this.refCounter = ref} className="charCounter">{charCounter} / {J.Constant.limit.chat.text}</div> + <Icon id="send" className="send" onClick={this.onSend} /> </div> </div> @@ -226,6 +234,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { this.marks = marks; this.updateMarkup(text, length, length); + this.updateCounter(text); if (attachments.length) { this.setAttachments(attachments); @@ -245,17 +254,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { componentWillUnmount () { this._isMounted = false; window.clearTimeout(this.timeoutFilter); - - const { rootId } = this.props; - const { attachments } = this.state; - keyboard.disableSelection(false); - - Storage.setChat(rootId, { - text: this.getTextValue(), - marks: this.marks, - attachments, - }); }; checkSendButton () { @@ -285,8 +284,17 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { }; onBlurInput () { + const { rootId } = this.props; + const { attachments } = this.state; + keyboard.disableSelection(false); this.refEditable?.placeholderCheck(); + + Storage.setChat(rootId, { + text: this.getTextValue(), + marks: this.marks, + attachments, + }); }; onKeyDownInput (e: any) { @@ -370,7 +378,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { }; onKeyUpInput (e: any) { - this.range = this.refEditable.getRange(); + this.range = this.refEditable.getRange() || { from: 0, to: 0 }; const { attachments } = this.state; const { to } = this.range; @@ -442,6 +450,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { this.checkSendButton(); this.updateButtons(); this.removeBookmarks(); + this.updateCounter(); }; onInput () { @@ -455,25 +464,39 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { e.preventDefault(); const { from, to } = this.range; + const limit = J.Constant.limit.chat.text; + const current = this.getTextValue(); const cb = e.clipboardData || e.originalEvent.clipboardData; - const text = U.Common.normalizeLineEndings(String(cb.getData('text/plain') || '')); const electron = U.Common.getElectron(); const list = U.Common.getDataTransferFiles((e.clipboardData || e.originalEvent.clipboardData).items).map((it: File) => this.getObjectFromFile(it)).filter(it => { return !electron.isDirectory(it.path); }); - const value = U.Common.stringInsert(this.getTextValue(), text, from, to); + + let text = U.Common.normalizeLineEndings(String(cb.getData('text/plain') || '')); + let value = U.Common.stringInsert(current, text, from, to); + if (value.length >= limit) { + const excess = value.length - limit; + const keep = text.length - excess; + + text = text.substring(0, keep); + value = U.Common.stringInsert(current, text, from, to); + }; this.range = { from: to, to: to + text.length }; - this.refEditable.setValue(value); + this.refEditable.setValue(Mark.toHtml(value, this.marks)); this.refEditable.setRange(this.range); this.refEditable.placeholderCheck(); + this.renderMarkup(); if (list.length) { - this.addAttachments(list); + U.Common.saveClipboardFiles(list, {}, data => { + this.addAttachments(data.files); + }); }; this.checkUrls(); this.onInput(); + this.updateCounter(); }; checkUrls () { @@ -485,17 +508,16 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { this.removeBookmarks(); - window.setTimeout(() => { - for (const url of urls) { - const { from, to, isLocal, value } = url; - const param = isLocal ? `file://${value}` : value; + for (const url of urls) { + const { from, to, isLocal, value } = url; + const param = isLocal ? `file://${value}` : value; - this.marks = Mark.adjust(this.marks, from - 1, value.length + 1); - this.marks.push({ type: I.MarkType.Link, range: { from, to }, param}); - this.addBookmark(param, true); - }; - this.updateMarkup(text, this.range.to + 1, this.range.to + 1); - }, 150); + this.marks = Mark.adjust(this.marks, from - 1, value.length + 1); + this.marks.push({ type: I.MarkType.Link, range: { from, to }, param}); + this.addBookmark(param, true); + }; + + this.updateMarkup(text, this.range.to + 1, this.range.to + 1); }; canDrop (e: any): boolean { @@ -568,7 +590,11 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { this.addAttachments([ item ]); }; + this.isLoading.push(url); + C.LinkPreview(url, (message: any) => { + this.isLoading = this.isLoading.filter(it => it != url); + if (message.error.code) { add({ title: url, url }); } else { @@ -618,6 +644,8 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { const clear = () => { this.onEditClear(); this.onReplyClear(); + this.clearCounter(); + this.checkSpeedLimit(); loader.removeClass('active'); }; @@ -716,6 +744,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { this.editingId = message.id; this.replyingId = ''; this.updateMarkup(text, l, l); + this.updateCounter(); this.setAttachments(attachments, () => { this.refEditable.setRange(this.range); @@ -727,6 +756,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { this.marks = []; this.updateMarkup('', 0, 0); this.setState({ attachments: [] }, () => this.refEditable.setRange(this.range)); + this.clearCounter(); this.refButtons.setButtons(); }; @@ -743,6 +773,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { onReplyClear () { this.replyingId = ''; this.forceUpdate(); + this.props.scrollToBottom(); }; onDelete (id: string) { @@ -977,8 +1008,8 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { }; }; - canSend () { - return this.editingId || this.getTextValue() || this.state.attachments.length || this.marks.length; + canSend (): boolean { + return !this.isLoading.length && Boolean(this.editingId || this.getTextValue() || this.state.attachments.length || this.marks.length); }; hasSelection (): boolean { @@ -992,6 +1023,13 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { if (list.length > limit[type]) { list = list.slice(0, limit[type]); + + if (type == 'attachments') { + Preview.toastShow({ + icon: 'notice', + text: U.Common.sprintf(translate('toastChatAttachmentsLimitReached'), limit[type], U.Common.plural(limit[type], translate('pluralFile')).toLowerCase()) + }); + }; }; return list; @@ -1000,6 +1038,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { updateMarkup (value: string, from: number, to: number) { this.range = { from, to }; this.refEditable.setValue(Mark.toHtml(value, this.marks)); + this.refEditable.setRange({ from, to }); this.refEditable.placeholderCheck(); this.renderMarkup(); @@ -1008,7 +1047,7 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { renderMarkup () { const { rootId, renderLinks, renderMentions, renderObjects, renderEmoji } = this.props; - const node = this.refEditable.node; + const node = this.refEditable.getNode(); const value = this.refEditable.getTextValue(); renderMentions(rootId, node, this.marks, () => value); @@ -1017,6 +1056,56 @@ const ChatForm = observer(class ChatForm extends React.Component<Props, State> { renderEmoji(node); }; + updateCounter (v?: string) { + const value = v || this.getTextValue(); + const l = value.length; + + this.setState({ charCounter: l }); + $(this.refCounter).toggleClass('show', l >= J.Constant.limit.chat.text - 50); + }; + + clearCounter () { + this.setState({ charCounter: 0 }); + $(this.refCounter).removeClass('show'); + }; + + checkSpeedLimit () { + const { last, counter } = this.speedLimit; + const now = U.Date.now(); + + if (now - last >= 5 ) { + this.speedLimit = { + last: now, + counter: 1 + }; + return; + }; + + this.speedLimit = { + last: now, + counter: counter + 1, + }; + + if (counter >= 5) { + this.speedLimit = { + last: now, + counter: 1 + }; + + S.Popup.open('confirm', { + data: { + icon: 'warningInverted', + bgColor: 'red', + title: translate('popupConfirmSpeedLimitTitle'), + text: translate('popupConfirmSpeedLimitText'), + textConfirm: translate('commonOkay'), + colorConfirm: 'blank', + canCancel: false, + } + }); + }; + }; + }); export default ChatForm; diff --git a/src/ts/component/block/chat/message/index.tsx b/src/ts/component/block/chat/message/index.tsx index 177e842a92..29dcc98923 100644 --- a/src/ts/component/block/chat/message/index.tsx +++ b/src/ts/component/block/chat/message/index.tsx @@ -27,7 +27,7 @@ const ChatMessage = observer(class ChatMessage extends React.Component<I.ChatMes }; render () { - const { rootId, id, isThread, isNew, readonly, onThread, onContextMenu, onMore, onReplyEdit } = this.props; + const { rootId, id, isNew, readonly, subId, scrollToBottom, onContextMenu, onMore, onReplyEdit } = this.props; const { space } = S.Common; const { account } = S.Auth; const message = S.Chat.getMessage(rootId, id); @@ -121,10 +121,13 @@ const ChatMessage = observer(class ChatMessage extends React.Component<I.ChatMes > <div className="flex"> <div className="side left"> - <IconObject object={author} size={40} onClick={e => U.Object.openConfig(author)} /> + <IconObject + object={{ ...author, layout: I.ObjectLayout.Participant }} + size={40} + onClick={e => U.Object.openConfig(author)} + /> </div> <div className="side right"> - <div className="author" onClick={e => U.Object.openConfig(author)}> <ObjectName object={author} /> <div className="time">{U.Date.date('H:i', createdAt)}</div> @@ -151,6 +154,8 @@ const ChatMessage = observer(class ChatMessage extends React.Component<I.ChatMes ref={ref => this.attachmentRefs[item.id] = ref} key={i} object={item} + subId={subId} + scrollToBottom={scrollToBottom} onRemove={() => this.onAttachmentRemove(item.id)} onPreview={(preview) => this.onPreview(preview)} showAsFile={!attachmentsLayout} @@ -170,10 +175,6 @@ const ChatMessage = observer(class ChatMessage extends React.Component<I.ChatMes ) : ''} </div> ) : ''} - - <div className="sub" onClick={() => onThread(id)}> - {!isThread ? <div className="item">0 replies</div> : ''} - </div> </div> {!readonly ? ( @@ -253,14 +254,22 @@ const ChatMessage = observer(class ChatMessage extends React.Component<I.ChatMes }; onReactionAdd () { + const { isPopup } = this.props; const node = $(this.node); + const container = isPopup ? U.Common.getScrollContainer(isPopup) : $('body'); S.Menu.open('smile', { element: node.find('#reaction-add'), horizontal: I.MenuDirection.Center, noFlipX: true, - onOpen: () => node.addClass('hover'), - onClose: () => node.removeClass('hover'), + onOpen: () => { + node.addClass('hover'); + container.addClass('over'); + }, + onClose: () => { + node.removeClass('hover'); + container.removeClass('over'); + }, data: { noHead: true, noUpload: true, diff --git a/src/ts/component/block/dataview.tsx b/src/ts/component/block/dataview.tsx index 993b6a25c3..ec5253f6a9 100644 --- a/src/ts/component/block/dataview.tsx +++ b/src/ts/component/block/dataview.tsx @@ -632,6 +632,8 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props const details = this.getDetails(groupId); const flags: I.ObjectFlag[] = []; + const isViewGraph = view.type == I.ViewType.Graph; + const isViewCalendar = view.type == I.ViewType.Calendar; let typeId = ''; let templateId = ''; @@ -649,12 +651,14 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props if (!typeId) { typeId = this.getTypeId(); }; + if (!templateId) { templateId = this.getDefaultTemplateId(typeId); }; const type = S.Record.getTypeById(typeId); if (!type) { + console.error('[BlockDataview.recordCreate] No type'); return; }; @@ -701,16 +705,16 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props S.Record.recordsSet(subId, '', records); }; - if ([ I.ViewType.Graph ].includes(view.type)) { + if (isViewGraph) { const refGraph = this.refView?.refGraph; if (refGraph) { refGraph.addNewNode(object.id, '', null, () => { - refGraph.setRootId(object.id); + $(window).trigger('updateGraphRoot', { id: object.id }); }); }; }; - if ([ I.ViewType.Calendar ].includes(view.type)) { + if (isViewGraph || isViewCalendar) { U.Object.openConfig(object); } else { if (U.Object.isNoteLayout(object.layout)) { @@ -1437,9 +1441,7 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props }; setSelected (ids: string[]) { - if (this.refSelect) { - this.refSelect.setIds(ids); - }; + this.refSelect?.setIds(ids); }; multiSelectAction (id: string) { diff --git a/src/ts/component/block/dataview/cell/index.tsx b/src/ts/component/block/dataview/cell/index.tsx index fe7253e63d..6c554825a8 100644 --- a/src/ts/component/block/dataview/cell/index.tsx +++ b/src/ts/component/block/dataview/cell/index.tsx @@ -186,9 +186,16 @@ const Cell = observer(class Cell extends React.Component<Props> { const canEdit = this.canCellEdit(relation, record); if (!canEdit) { - if (Relation.isUrl(relation.format) && value) { - Action.openUrl(Relation.checkUrlScheme(relation.format, value)); - return; + if (value) { + if (Relation.isUrl(relation.format)) { + Action.openUrl(Relation.checkUrlScheme(relation.format, value)); + return; + }; + + if (Relation.isDate(relation.format)) { + U.Object.openDateByTimestamp(relation.relationKey, value, 'config'); + return; + }; }; if (relation.format == I.RelationType.Checkbox) { @@ -275,6 +282,7 @@ const Cell = observer(class Cell extends React.Component<Props> { blockId: block.id, value, relation: observable.box(relation), + relationKey: relation.relationKey, record, placeholder, canEdit, @@ -292,6 +300,7 @@ const Cell = observer(class Cell extends React.Component<Props> { case I.RelationType.Date: { param.data = Object.assign(param.data, { value: param.data.value || U.Date.now(), + noKeyboard: true, }); menuId = 'dataviewCalendar'; diff --git a/src/ts/component/block/dataview/controls.tsx b/src/ts/component/block/dataview/controls.tsx index 88330feaab..042c8c269f 100644 --- a/src/ts/component/block/dataview/controls.tsx +++ b/src/ts/component/block/dataview/controls.tsx @@ -121,10 +121,9 @@ const Controls = observer(class Controls extends React.Component<Props> { id="dataviewControls" className={cn.join(' ')} > + {head} <div className="sides"> <div id="dataviewControlsSideLeft" className="side left"> - {head} - <div id="view-selector" className="viewSelect viewItem select" @@ -556,8 +555,8 @@ const Controls = observer(class Controls extends React.Component<Props> { let add = false; - if (sideLeft.hasClass('small')) { - sideLeft.removeClass('small'); + if (node.hasClass('small')) { + node.removeClass('small'); }; const width = sideLeft.outerWidth() + sideRight.outerWidth(); @@ -571,7 +570,7 @@ const Controls = observer(class Controls extends React.Component<Props> { }; if (add) { - sideLeft.addClass('small'); + node.addClass('small'); } else { S.Menu.closeAll([ 'dataviewViewList' ]); }; diff --git a/src/ts/component/block/dataview/view/board.tsx b/src/ts/component/block/dataview/view/board.tsx index 7c07154d36..39465b4b8f 100644 --- a/src/ts/component/block/dataview/view/board.tsx +++ b/src/ts/component/block/dataview/view/board.tsx @@ -252,6 +252,13 @@ const ViewBoard = observer(class ViewBoard extends React.Component<I.ViewCompone }; onDragStartColumn (e: any, groupId: string) { + const { readonly } = this.props; + if (readonly) { + e.preventDefault(); + e.stopPropagation(); + return; + }; + const win = $(window); const node = $(this.node); @@ -335,6 +342,13 @@ const ViewBoard = observer(class ViewBoard extends React.Component<I.ViewCompone }; onDragStartCard (e: any, groupId: any, record: any) { + const { readonly } = this.props; + if (readonly) { + e.preventDefault(); + e.stopPropagation(); + return; + }; + const win = $(window); this.onDragStartCommon(e, $(e.currentTarget)); diff --git a/src/ts/component/block/dataview/view/calendar.tsx b/src/ts/component/block/dataview/view/calendar.tsx index 17931dacdd..194e2837b6 100644 --- a/src/ts/component/block/dataview/view/calendar.tsx +++ b/src/ts/component/block/dataview/view/calendar.tsx @@ -238,12 +238,11 @@ const ViewCalendar = observer(class ViewCalendar extends React.Component<I.ViewC const objectId = getTarget().id; const flags: I.ObjectFlag[] = [ I.ObjectFlag.SelectTemplate ]; const type = S.Record.getTypeById(getTypeId()); - const typeKey = type.uniqueKey; const templateId = getTemplateId(); details = Object.assign(Dataview.getDetails(rootId, J.Constant.blockId.dataview, objectId, view.id), details); - C.ObjectCreate(details, flags, templateId, typeKey, S.Common.space, (message: any) => { + C.ObjectCreate(details, flags, templateId, type?.uniqueKey, S.Common.space, (message: any) => { if (message.error.code) { return; }; diff --git a/src/ts/component/block/dataview/view/calendar/item.tsx b/src/ts/component/block/dataview/view/calendar/item.tsx index e42b334c00..0e5f6bf742 100644 --- a/src/ts/component/block/dataview/view/calendar/item.tsx +++ b/src/ts/component/block/dataview/view/calendar/item.tsx @@ -174,9 +174,10 @@ const Item = observer(class Item extends React.Component<Props> { }; onOpenDate () { - const { d, m, y } = this.props; + const { d, m, y, getView } = this.props; + const view = getView(); - U.Object.openDateByTimestamp(U.Date.timestamp(y, m, d, 12, 0, 0), 'config'); + U.Object.openDateByTimestamp(view.groupRelationKey, U.Date.timestamp(y, m, d, 12, 0, 0), 'config'); }; canCreate (): boolean { diff --git a/src/ts/component/block/dataview/view/grid.tsx b/src/ts/component/block/dataview/view/grid.tsx index 68b1a43607..07586e12ee 100644 --- a/src/ts/component/block/dataview/view/grid.tsx +++ b/src/ts/component/block/dataview/view/grid.tsx @@ -278,6 +278,7 @@ const ViewGrid = observer(class ViewGrid extends React.Component<I.ViewComponent }); node.find('.rowHead').css({ gridTemplateColumns: str }); + node.find('.rowFoot').css({ gridTemplateColumns: str }); node.find('.row .selectionTarget').css({ gridTemplateColumns: str }); }; diff --git a/src/ts/component/block/dataview/view/grid/foot/cell.tsx b/src/ts/component/block/dataview/view/grid/foot/cell.tsx index 4e506e1f55..7a5a942bea 100644 --- a/src/ts/component/block/dataview/view/grid/foot/cell.tsx +++ b/src/ts/component/block/dataview/view/grid/foot/cell.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Select } from 'Component'; -import { I, S, C, U, keyboard, Relation, Dataview, analytics } from 'Lib'; +import { I, S, C, U, keyboard, Relation, Dataview, analytics, Preview } from 'Lib'; interface Props extends I.ViewComponent, I.ViewRelation { rootId?: string; @@ -9,7 +8,6 @@ interface Props extends I.ViewComponent, I.ViewRelation { }; interface State { - isEditing: boolean; result: any; }; @@ -17,44 +15,35 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { node = null; menuContext = null; - refSelect = null; state = { - isEditing: false, result: null, }; constructor (props: Props) { super(props); - this.onClick = this.onClick.bind(this); this.onOpen = this.onOpen.bind(this); - this.onClose = this.onClose.bind(this); this.onOver = this.onOver.bind(this); this.onChange = this.onChange.bind(this); this.onMouseEnter = this.onMouseEnter.bind(this); this.onMouseLeave = this.onMouseLeave.bind(this); + this.onSelect = this.onSelect.bind(this); }; render () { - const { relationKey, rootId, block, getView } = this.props; - const { isEditing, result } = this.state; + const { relationKey, rootId, block } = this.props; + const { result } = this.state; const relation = S.Record.getRelationByKey(relationKey); - const view = getView(); - if (!relation || !view) { + if (!relation) { return <div />; }; - // Subscriptions - const viewRelation = view.getRelation(relationKey); - if (!viewRelation || (viewRelation.formulaType == I.FormulaType.None)) { - return <div />; - }; - - const cn = [ 'cellFoot', `cell-key-${relationKey}` ]; - const sections = U.Menu.getFormulaSections(relationKey); - const option = Relation.formulaByType(relation.format).find(it => it.id == String(viewRelation.formulaType)); + const cn = [ 'cellFoot' ]; + const formulaType = this.getFormulaType(); + const option: any = this.getOption() || {}; + const name = option.short || option.name || ''; const subId = S.Record.getSubId(rootId, block.id); const records = S.Record.getRecords(subId, [ relationKey ], true); @@ -67,36 +56,15 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { ref={ref => this.node = ref} id={Relation.cellId('foot', relationKey, '')} className={cn.join(' ')} - onClick={this.onClick} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} > - <div className="cellContent"> + <div className="cellContent" onClick={this.onSelect}> <div className="flex"> - {isEditing || (result === null) ? ( - <Select - ref={ref => this.refSelect = ref} - id={`grid-foot-select-${relationKey}-${block.id}`} - value="" - onChange={() => this.refSelect.setValue('')} - options={sections} - arrowClassName="light" - menuParam={{ - onOpen: this.onOpen, - onClose: this.onClose, - subIds: [ 'select2' ], - data: { - noScroll: true, - noVirtualisation: true, - onOver: this.onOver, - }, - }} - /> - ) : ''} - {!isEditing && option && (result !== null) ? ( + {formulaType != I.FormulaType.None ? ( <div className="result"> - <span className="name">{option.short || option.name}</span> - {result} + <span className="name">{name}</span> + <span className="value">{result}</span> </div> ) : ''} </div> @@ -116,7 +84,15 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { calculate () { const { rootId, block, relationKey, getView, isInline } = this.props; const view = getView(); + if (!view) { + return; + }; + const viewRelation = view.getRelation(relationKey); + if (!viewRelation) { + return; + }; + const subId = isInline ? [ rootId, block.id, 'total' ].join('-') : S.Record.getSubId(rootId, block.id); const result = Dataview.getFormulaResult(subId, viewRelation); @@ -126,14 +102,53 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { }; }; - setEditing (v: boolean): void { - this.setState({ isEditing: v }); + getOption (): any { + const { relationKey, getView } = this.props; + const view = getView(); + + if (!view) { + return null; + }; + + const formulaType = this.getFormulaType(); + const relation = S.Record.getRelationByKey(relationKey); + + if (!relation) { + return null; + }; + + return Relation.formulaByType(relationKey, relation.format).find(it => it.id == String(formulaType)); }; - onClick (e: any) { - this.setState({ isEditing: true }, () => { - window.setTimeout(() => this.refSelect.show(e), 10); + onSelect (e: any) { + const { relationKey, getView } = this.props; + const id = Relation.cellId('foot', relationKey, ''); + const options = U.Menu.getFormulaSections(relationKey); + const formulaType = this.getFormulaType(); + + if (formulaType == I.FormulaType.None) { + return; + }; + + S.Menu.closeAll([], () => { + S.Menu.open('select', { + element: `#${id}`, + horizontal: I.MenuDirection.Center, + onOpen: this.onOpen, + subIds: [ 'select2' ], + data: { + options: U.Menu.prepareForSelect(options), + noScroll: true, + noVirtualisation: true, + onOver: this.onOver, + onSelect: (e: any, item: any) => { + this.onChange(item.id); + }, + } + }); }); + + Preview.tooltipHide(); }; onOpen (context: any): void { @@ -147,10 +162,6 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { analytics.event('ClickGridFormula', { format: relation.format, objectType: object.type }); }; - onClose () { - $(`.cellKeyHover`).removeClass('cellKeyHover'); - }; - onOver (e: any, item: any) { if (!this.menuContext) { return; @@ -163,7 +174,7 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { const { rootId, relationKey } = this.props; const relation = S.Record.getRelationByKey(relationKey); - const options = Relation.formulaByType(relation.format).filter(it => it.section == item.id); + const options = Relation.formulaByType(relationKey, relation.format).filter(it => it.section == item.id); S.Menu.closeAll([ 'select2' ], () => { S.Menu.open('select2', { @@ -179,7 +190,6 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { onSelect: (e: any, item: any) => { this.onChange(item.id); this.menuContext.close(); - this.setEditing(false); }, } }); @@ -202,15 +212,49 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> { }; onMouseEnter (): void { - const { block, relationKey } = this.props; + if (keyboard.isDragging) { + return; + }; + + const formulaType = this.getFormulaType(); + + if (formulaType == I.FormulaType.None) { + return; + }; - if (!keyboard.isDragging) { - $(`#block-${block.id} .cell-key-${relationKey}`).addClass('cellKeyHover'); + const node = $(this.node); + + node.addClass('hover'); + + const { result } = this.state; + if ((result === null) || S.Menu.isOpen()) { + return; + }; + + const option: any = this.getOption() || {}; + const name = option.short || option.name || ''; + + const t = Preview.tooltipCaption(name, result); + if (t) { + Preview.tooltipShow({ text: t, element: node, typeY: I.MenuDirection.Top }); }; }; onMouseLeave () { - $('.cellKeyHover').removeClass('cellKeyHover'); + $(this.node).removeClass('hover'); + Preview.tooltipHide(); + }; + + getFormulaType (): I.FormulaType { + const { relationKey, getView } = this.props; + const view = getView(); + + if (!view) { + return I.FormulaType.None; + }; + + const viewRelation = view.getRelation(relationKey); + return viewRelation ? viewRelation.formulaType : I.FormulaType.None; }; }); diff --git a/src/ts/component/block/dataview/view/grid/head/cell.tsx b/src/ts/component/block/dataview/view/grid/head/cell.tsx index dc8cd8a1aa..077c68342e 100644 --- a/src/ts/component/block/dataview/view/grid/head/cell.tsx +++ b/src/ts/component/block/dataview/view/grid/head/cell.tsx @@ -58,13 +58,15 @@ const HeadCell = observer(class HeadCell extends React.Component<Props> { onMouseEnter (): void { const { block, relationKey } = this.props; - if (!keyboard.isDragging) { + if (!keyboard.isDragging && !keyboard.isResizing) { $(`#block-${block.id} .cell-key-${relationKey}`).addClass('cellKeyHover'); }; }; onMouseLeave () { - $('.cellKeyHover').removeClass('cellKeyHover'); + if (!keyboard.isDragging && !keyboard.isResizing) { + $('.cellKeyHover').removeClass('cellKeyHover'); + }; }; onEdit (e: any) { diff --git a/src/ts/component/block/div.tsx b/src/ts/component/block/div.tsx index f6043a7dce..de3ada1873 100644 --- a/src/ts/component/block/div.tsx +++ b/src/ts/component/block/div.tsx @@ -1,80 +1,59 @@ -import * as React from 'react'; +import React, { forwardRef, KeyboardEvent } from 'react'; import { I, focus } from 'Lib'; import { observer } from 'mobx-react'; -const BlockDiv = observer(class BlockDiv extends React.Component<I.BlockComponent> { +const BlockDiv = observer(forwardRef<{}, I.BlockComponent>((props, ref) => { - _isMounted = false; + const { block, onKeyDown, onKeyUp } = props; + const { id, content } = block; + const { style } = content; + const cn = [ 'wrap', 'focusable', `c${id}` ]; - constructor (props: I.BlockComponent) { - super(props); - - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onFocus = this.onFocus.bind(this); - }; - - render () { - const { block } = this.props; - const { id, content } = block; - const { style } = content; - - const cn = [ 'wrap', 'focusable', 'c' + id ]; - let inner: any = null; - - switch (content.style) { - case I.DivStyle.Line: - inner = ( - <div className="line" /> - ); - break; - - case I.DivStyle.Dot: - inner = ( - <div className="dots"> - <div className="dot" /> - <div className="dot" /> - <div className="dot" /> - </div> - ); - break; + const onKeyDownHandler = (e: KeyboardEvent) => { + if (onKeyDown) { + onKeyDown(e, '', [], { from: 0, to: 0 }, props); }; - - return ( - <div className={cn.join(' ')} tabIndex={0} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus}> - {inner} - </div> - ); }; - componentDidMount () { - this._isMounted = true; + const onKeyUpHandler = (e: KeyboardEvent) => { + if (onKeyUp) { + onKeyUp(e, '', [], { from: 0, to: 0 }, props); + }; }; - - componentWillUnmount () { - this._isMounted = false; + + const onFocus = () => { + focus.set(block.id, { from: 0, to: 0 }); }; - - onKeyDown (e: any) { - const { onKeyDown } = this.props; - if (onKeyDown) { - onKeyDown(e, '', [], { from: 0, to: 0 }, this.props); + let inner: any = null; + switch (style) { + case I.DivStyle.Line: { + inner = <div className="line" />; + break; }; - }; - - onKeyUp (e: any) { - const { onKeyUp } = this.props; - if (onKeyUp) { - onKeyUp(e, '', [], { from: 0, to: 0 }, this.props); + case I.DivStyle.Dot: { + inner = ( + <div className="dots"> + {Array(3).fill(null).map((_, i) => <div key={i} className="dot" />)} + </div> + ); + break; }; }; - onFocus () { - focus.set(this.props.block.id, { from: 0, to: 0 }); - }; - -}); + return ( + <div + className={cn.join(' ')} + tabIndex={0} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUpHandler} + onFocus={onFocus} + > + {inner} + </div> + ); + +})); export default BlockDiv; \ No newline at end of file diff --git a/src/ts/component/block/featured.tsx b/src/ts/component/block/featured.tsx index 3f9a3309f7..c3ff1d04c7 100644 --- a/src/ts/component/block/featured.tsx +++ b/src/ts/component/block/featured.tsx @@ -83,10 +83,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props <span key={i} className={cn.join(' ')} - onClick={(e: any) => { - e.persist(); - this.onRelation(e, relationKey); - }} + onClick={e => this.onRelation(e, relationKey)} > <Cell ref={ref => this.cellRefs.set(id, ref)} @@ -648,6 +645,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props }; onRelation (e: any, relationKey: string) { + e.persist(); e.stopPropagation(); if (S.Menu.isOpen()) { @@ -667,6 +665,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props let menuId = ''; let menuParam: any = {}; let menuData: any = {}; + let ret = false; switch (relation.format) { case I.RelationType.Object: { @@ -691,6 +690,12 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props isEmpty = true; }; + if (!this.canEdit(relation)) { + U.Object.openDateByTimestamp(relationKey, value, 'config'); + ret = true; + break; + }; + menuId = 'dataviewCalendar'; menuData = { value, @@ -737,6 +742,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props case I.RelationType.Checkbox: { if (!this.canEdit(relation)) { + ret = true; break; }; @@ -749,6 +755,10 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props }; }; + if (ret) { + return; + }; + if (menuId) { this.onCellMenu(relationKey, menuId, menuParam, menuData); } else { diff --git a/src/ts/component/block/help/icon.tsx b/src/ts/component/block/help/icon.tsx index f3fff47acf..baa0cfac64 100644 --- a/src/ts/component/block/help/icon.tsx +++ b/src/ts/component/block/help/icon.tsx @@ -1,17 +1,14 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { IconObject } from 'Component'; interface Props { icon?: string; }; -class ContentIcon extends React.Component<Props> { +const ContentIcon = forwardRef<HTMLDivElement, Props>(({ icon = '' }, ref) => { - render () { - const { icon } = this.props; - return <IconObject size={96} object={{ iconEmoji: icon }} />; - }; - -}; + return <IconObject size={96} object={{ iconEmoji: icon }} />; + +}); export default ContentIcon; \ No newline at end of file diff --git a/src/ts/component/block/help/index.tsx b/src/ts/component/block/help/index.tsx index a2bdd5c389..5da288078c 100644 --- a/src/ts/component/block/help/index.tsx +++ b/src/ts/component/block/help/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { I, U } from 'Lib'; import ContentIcon from './icon'; import ContentText from './text'; @@ -11,74 +11,70 @@ interface Props { align?: I.BlockHAlign; }; -class Block extends React.Component<Props> { +const Block = forwardRef<HTMLDivElement, Props>((props, ref) => { - public static defaultProps = { - type: I.BlockType.Text, - style: I.TextStyle.Paragraph, - align: I.BlockHAlign.Left, - }; - - render () { - const { type, style, align } = this.props; - const cn = [ 'block', U.Data.blockClass({ type: type, content: { style: style } }), 'align' + align ]; + const { + type = I.BlockType.Text, + style = I.TextStyle.Paragraph, + align = I.BlockHAlign.Left, + } = props; + const cn = [ 'block', U.Data.blockClass({ type: type, content: { style: style } }), `align${align}` ]; - let content = null; + let content = null; - switch (type) { - case I.BlockType.IconPage: { - content = <ContentIcon {...this.props} />; - break; - }; - - case I.BlockType.Text: { - content = <ContentText {...this.props} />; - break; - }; - - case I.BlockType.Link: { - content = <ContentLink {...this.props} />; - break; - }; + switch (type) { + case I.BlockType.IconPage: { + content = <ContentIcon {...props} />; + break; + }; + + case I.BlockType.Text: { + content = <ContentText {...props} />; + break; + }; + + case I.BlockType.Link: { + content = <ContentLink {...props} />; + break; + }; - case I.BlockType.Div: { - let inner: any = null; - switch (style) { - case I.DivStyle.Line: - inner = ( - <div className="line" /> - ); - break; + case I.BlockType.Div: { + let inner: any = null; + switch (style) { + case I.DivStyle.Line: + inner = ( + <div className="line" /> + ); + break; - case I.DivStyle.Dot: - inner = ( - <div className="dots"> - <div className="dot" /> - <div className="dot" /> - <div className="dot" /> - </div> - ); - break; - }; - - content = <div className="wrap">{inner}</div>; - break; + case I.DivStyle.Dot: + inner = ( + <div className="dots"> + <div className="dot" /> + <div className="dot" /> + <div className="dot" /> + </div> + ); + break; }; + + content = <div className="wrap">{inner}</div>; + break; }; - - return ( - <div className={cn.join(' ')}> - <div className="wrapContent"> - <div className="selectionTarget"> - <div className="dropTarget"> - {content} - </div> + }; + + return ( + <div className={cn.join(' ')}> + <div className="wrapContent"> + <div className="selectionTarget"> + <div className="dropTarget"> + {content} </div> </div> </div> - ); - }; - -}; + </div> + ); + +}); export default Block; \ No newline at end of file diff --git a/src/ts/component/block/help/link.tsx b/src/ts/component/block/help/link.tsx index 3141cf5927..246016e80f 100644 --- a/src/ts/component/block/help/link.tsx +++ b/src/ts/component/block/help/link.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { IconObject } from 'Component'; import { U } from 'Lib'; @@ -8,29 +8,17 @@ interface Props { contentId?: string; }; -class ContentLink extends React.Component<Props> { +const ContentLink = forwardRef<HTMLDivElement, Props>(({ icon = '', name = '', contentId = '' }, ref) => { - constructor (props: Props) { - super(props); - - this.onClick = this.onClick.bind(this); - }; + return ( + <> + <IconObject object={{ iconEmoji: icon }} /> + <div className="name" onClick={() => U.Router.go(`/help/${contentId}`, {})}> + {name} + </div> + </> + ); - render () { - const { icon, name } = this.props; - - return ( - <React.Fragment> - <IconObject object={{ iconEmoji: icon }} /> - <div className="name" onClick={this.onClick}>{name}</div> - </React.Fragment> - ); - }; - - onClick (e: any) { - U.Router.go(`/help/${this.props.contentId}`, {}); - }; - -}; +}); export default ContentLink; \ No newline at end of file diff --git a/src/ts/component/block/help/text.tsx b/src/ts/component/block/help/text.tsx index 3dc1579655..978ba203ec 100644 --- a/src/ts/component/block/help/text.tsx +++ b/src/ts/component/block/help/text.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { Marker, IconObject } from 'Component'; import { I, U } from 'Lib'; @@ -10,62 +10,59 @@ interface Props { icon?: string; }; -class ContentText extends React.Component<Props> { +const ContentText = forwardRef<HTMLDivElement, Props>(({ + text = ' ', + style = I.TextStyle.Paragraph, + checked = false, + color = 'default', + icon = '', +}, ref) => { - public static defaultProps = { - text: ' ', - color: 'default', - }; - - render () { - const { text, style, checked, color, icon } = this.props; - - let marker = null; - let additional = null; + let marker = null; + let additional = null; - switch (style) { - case I.TextStyle.Quote: { - additional = <div className="line" />; - break; - }; + switch (style) { + case I.TextStyle.Quote: { + additional = <div className="line" />; + break; + }; - case I.TextStyle.Callout: { - additional = <IconObject object={{ iconEmoji: icon, layout: I.ObjectLayout.Page }} />; - break; - }; - - case I.TextStyle.Bulleted: { - marker = { type: I.MarkerType.Bulleted, className: 'bullet', active: false }; - break; - }; - - case I.TextStyle.Numbered: { - marker = { type: I.MarkerType.Numbered, className: 'number', active: false }; - break; - }; - - case I.TextStyle.Toggle: { - marker = { type: I.MarkerType.Toggle, className: 'toggle', active: false }; - break; - }; - - case I.TextStyle.Checkbox: { - marker = { type: I.MarkerType.Checkbox, className: 'check', active: checked }; - break; - }; + case I.TextStyle.Callout: { + additional = <IconObject object={{ iconEmoji: icon, layout: I.ObjectLayout.Page }} />; + break; + }; + + case I.TextStyle.Bulleted: { + marker = { type: I.MarkerType.Bulleted, className: 'bullet', active: false }; + break; + }; + + case I.TextStyle.Numbered: { + marker = { type: I.MarkerType.Numbered, className: 'number', active: false }; + break; + }; + + case I.TextStyle.Toggle: { + marker = { type: I.MarkerType.Toggle, className: 'toggle', active: false }; + break; + }; + + case I.TextStyle.Checkbox: { + marker = { type: I.MarkerType.Checkbox, className: 'check', active: checked }; + break; }; - - return ( - <div className="flex"> - <div className="markers"> - {marker ? <Marker {...marker} color={color} /> : ''} - </div> - {additional} - <div className="wrap" dangerouslySetInnerHTML={{ __html: U.Common.sanitize(text) }} /> - </div> - ); }; -}; + return ( + <div className="flex"> + <div className="markers"> + {marker ? <Marker {...marker} color={color} /> : ''} + </div> + {additional} + <div className="wrap" dangerouslySetInnerHTML={{ __html: U.Common.sanitize(text) }} /> + </div> + ); + +}); export default ContentText; \ No newline at end of file diff --git a/src/ts/component/block/iconPage.tsx b/src/ts/component/block/iconPage.tsx index 73209eff7d..f4574f48c1 100644 --- a/src/ts/component/block/iconPage.tsx +++ b/src/ts/component/block/iconPage.tsx @@ -1,23 +1,22 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; import { IconObject } from 'Component'; import { I, S } from 'Lib'; -const BlockIconPage = observer(class BlockIconPage extends React.Component<I.BlockComponent> { - - render (): any { - const { rootId, readonly } = this.props; +const BlockIconPage = observer(forwardRef<{}, I.BlockComponent>(({ + rootId = '', + readonly = false, +}, ref) => { - return ( - <IconObject - id={`block-icon-${rootId}`} - canEdit={!readonly} - getObject={() => S.Detail.get(rootId, rootId, [])} - size={96} - /> - ); - }; + return ( + <IconObject + id={`block-icon-${rootId}`} + canEdit={!readonly} + getObject={() => S.Detail.get(rootId, rootId, [])} + size={96} + /> + ); -}); +})); export default BlockIconPage; \ No newline at end of file diff --git a/src/ts/component/block/iconUser.tsx b/src/ts/component/block/iconUser.tsx index 4afca07976..13c49b0190 100644 --- a/src/ts/component/block/iconUser.tsx +++ b/src/ts/component/block/iconUser.tsx @@ -1,59 +1,47 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useImperativeHandle } from 'react'; import { observer } from 'mobx-react'; import { IconObject, Loader } from 'Component'; import { I, S, U } from 'Lib'; -interface State { - isLoading: boolean; +interface BlockIconUserRefProps { + setLoading: (v: boolean) => void; }; -const BlockIconUser = observer(class BlockIconUser extends React.Component<I.BlockComponent, State> { +const BlockIconUser = observer(forwardRef<BlockIconUserRefProps, I.BlockComponent>(({ + rootId = '', + readonly = false, +}, ref) => { - state = { - isLoading: false, - }; - - constructor (props: I.BlockComponent) { - super(props); - - this.onSelect = this.onSelect.bind(this); - this.onUpload = this.onUpload.bind(this); - }; + const [ isLoading, setIsLoading ] = useState(false); - render (): any { - const { isLoading } = this.state; - const { rootId, readonly } = this.props; - - return ( - <div className="wrap"> - {isLoading ? <Loader /> : ''} - <IconObject - id={`block-icon-${rootId}`} - getObject={() => S.Detail.get(rootId, rootId, [])} - className={readonly ? 'isReadonly' : ''} - canEdit={!readonly} - onSelect={this.onSelect} - onUpload={this.onUpload} - size={128} - /> - </div> - ); + const onSelect = () => { + setIsLoading(true); + U.Object.setIcon(rootId, '', '', () => setIsLoading(false)); }; - onSelect () { - this.setLoading(true); - U.Object.setIcon(this.props.rootId, '', '', () => this.setLoading(false)); + const onUpload = (objectId: string) => { + setIsLoading(true); + U.Object.setIcon(rootId, '', objectId, () => setIsLoading(false)); }; - onUpload (objectId: string) { - this.setLoading(true); - U.Object.setIcon(this.props.rootId, '', objectId, () => this.setLoading(false)); - }; - - setLoading (v: boolean) { - this.setState({ isLoading: v }); - }; - -}); + useImperativeHandle(ref, () => ({ + setLoading: (v: boolean) => setIsLoading(v), + })); + + return ( + <div className="wrap"> + {isLoading ? <Loader /> : ''} + <IconObject + id={`block-icon-${rootId}`} + getObject={() => S.Detail.get(rootId, rootId, [])} + className={readonly ? 'isReadonly' : ''} + canEdit={!readonly} + onSelect={onSelect} + onUpload={onUpload} + size={128} + /> + </div> + ); +})); export default BlockIconUser; \ No newline at end of file diff --git a/src/ts/component/block/index.tsx b/src/ts/component/block/index.tsx index 14187a2cbe..9c83aa7f00 100644 --- a/src/ts/component/block/index.tsx +++ b/src/ts/component/block/index.tsx @@ -468,7 +468,7 @@ const Block = observer(class Block extends React.Component<Props> { onDragStart (e: any) { e.stopPropagation(); - if (!this._isMounted) { + if (!this._isMounted || keyboard.isResizing) { return; }; @@ -484,7 +484,7 @@ const Block = observer(class Block extends React.Component<Props> { keyboard.disableSelection(true); if (selection) { - if (selection.isSelecting) { + if (selection.isSelecting()) { selection.setIsSelecting(false); }; @@ -512,15 +512,14 @@ const Block = observer(class Block extends React.Component<Props> { return; }; + const offset = element.offset(); + selection.set(I.SelectType.Block, this.ids); this.menuOpen({ horizontal: I.MenuDirection.Right, offsetX: element.outerWidth(), - recalcRect: () => { - const offset = element.offset(); - return { x: offset.left, y: keyboard.mouse.page.y, width: element.width(), height: 0 }; - }, + rect: { x: offset.left, y: keyboard.mouse.page.y, width: element.width(), height: 0 }, }); }; @@ -533,8 +532,7 @@ const Block = observer(class Block extends React.Component<Props> { isContextMenuDisabled || readonly || (block.isText() && (focused == block.id)) || - block.isTable() || - block.isDataview() + !block.canContextMenu() ) { return; }; @@ -559,7 +557,7 @@ const Block = observer(class Block extends React.Component<Props> { }; this.menuOpen({ - recalcRect: () => ({ x: keyboard.mouse.page.x, y: keyboard.mouse.page.y, width: 0, height: 0 }) + rect: { x: keyboard.mouse.page.x, y: keyboard.mouse.page.y, width: 0, height: 0 }, }); }); }; @@ -1048,13 +1046,13 @@ const Block = observer(class Block extends React.Component<Props> { node = $(node); const items = node.find(Mark.getTag(I.MarkType.Emoji)); - const { block } = this.props; - const size = U.Data.emojiParam(block.content.style); - if (!items.length) { return; }; + const { block } = this.props; + const size = U.Data.emojiParam(block.content.style); + items.each((i: number, item: any) => { item = $(item); diff --git a/src/ts/component/block/media/file.tsx b/src/ts/component/block/media/file.tsx index 0f9c638093..8ba48e99d5 100644 --- a/src/ts/component/block/media/file.tsx +++ b/src/ts/component/block/media/file.tsx @@ -1,136 +1,107 @@ -import * as React from 'react'; +import React, { forwardRef, KeyboardEvent } from 'react'; import { InputWithFile, Loader, IconObject, Error, ObjectName, Icon } from 'Component'; import { I, S, U, focus, translate, Action, analytics } from 'Lib'; import { observer } from 'mobx-react'; -const BlockFile = observer(class BlockFile extends React.Component<I.BlockComponent> { +const BlockFile = observer(forwardRef<{}, I.BlockComponent>((props, ref) => { - _isMounted = false; + const { rootId, block, readonly, onKeyDown, onKeyUp } = props; + const { id, content } = block; + const { state, style, targetObjectId } = content; + const object = S.Detail.get(rootId, targetObjectId, []); - constructor (props: I.BlockComponent) { - super(props); - - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onClick = this.onClick.bind(this); - this.onChangeUrl = this.onChangeUrl.bind(this); - this.onChangeFile = this.onChangeFile.bind(this); - }; - - render () { - const { rootId, block, readonly } = this.props; - const { id, content } = block; - const { state, style, targetObjectId } = content; - const object = S.Detail.get(rootId, targetObjectId, []); - - let element = null; - if (object.isDeleted) { - element = ( - <div className="deleted"> - <Icon className="ghost" /> - <div className="name">{translate('commonDeletedObject')}</div> - </div> - ); - } else { - switch (state) { - default: - case I.FileState.Error: - case I.FileState.Empty: { - element = ( - <React.Fragment> - {state == I.FileState.Error ? <Error text={translate('blockFileError')} /> : ''} - <InputWithFile - block={block} - icon="file" - textFile={translate('blockFileUpload')} - onChangeUrl={this.onChangeUrl} - onChangeFile={this.onChangeFile} - readonly={readonly} - /> - </React.Fragment> - ); - break; - }; - - case I.FileState.Uploading: { - element = <Loader />; - break; - }; - - case I.FileState.Done: { - element = ( - <div - className="inner" - onMouseDown={this.onClick} - > - <IconObject object={object} size={24} /> - <ObjectName object={object} /> - <span className="size">{U.File.size(object.sizeInBytes)}</span> - </div> - ); - break; - }; - }; - }; - - return ( - <div - className={[ 'focusable', 'c' + id ].join(' ')} - tabIndex={0} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onFocus={this.onFocus} - > - {element} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentWillUnmount () { - this._isMounted = false; - }; - - onKeyDown (e: any) { - const { onKeyDown } = this.props; - + const onKeyDownHandler = (e: KeyboardEvent) => { if (onKeyDown) { - onKeyDown(e, '', [], { from: 0, to: 0 }, this.props); + onKeyDown(e, '', [], { from: 0, to: 0 }, props); }; }; - onKeyUp (e: any) { - const { onKeyUp } = this.props; - + const onKeyUpHandler = (e: KeyboardEvent) => { if (onKeyUp) { - onKeyUp(e, '', [], { from: 0, to: 0 }, this.props); + onKeyUp(e, '', [], { from: 0, to: 0 }, props); }; }; - onFocus () { - focus.set(this.props.block.id, { from: 0, to: 0 }); + const onFocus = () => { + focus.set(block.id, { from: 0, to: 0 }); }; - onChangeUrl (e: any, url: string) { - const { rootId, block } = this.props; + const onChangeUrl = (e: any, url: string) => { Action.upload(I.FileType.File, rootId, block.id, url, ''); }; - onChangeFile (e: any, path: string) { - const { rootId, block } = this.props; + const onChangeFile = (e: any, path: string) => { Action.upload(I.FileType.File, rootId, block.id, '', path); }; - onClick (e: any) { + const onClick = (e: any) => { if (!e.button) { - Action.openFile(this.props.block.getTargetObjectId(), analytics.route.block); + Action.openFile(block.getTargetObjectId(), analytics.route.block); + }; + }; + + let element = null; + if (object.isDeleted) { + element = ( + <div className="deleted"> + <Icon className="ghost" /> + <div className="name">{translate('commonDeletedObject')}</div> + </div> + ); + } else { + switch (state) { + default: + case I.FileState.Error: + case I.FileState.Empty: { + element = ( + <> + {state == I.FileState.Error ? <Error text={translate('blockFileError')} /> : ''} + <InputWithFile + block={block} + icon="file" + textFile={translate('blockFileUpload')} + onChangeUrl={onChangeUrl} + onChangeFile={onChangeFile} + readonly={readonly} + /> + </> + ); + break; + }; + + case I.FileState.Uploading: { + element = <Loader />; + break; + }; + + case I.FileState.Done: { + element = ( + <div + className="inner" + onMouseDown={onClick} + > + <IconObject object={object} size={24} /> + <ObjectName object={object} /> + <span className="size">{U.File.size(object.sizeInBytes)}</span> + </div> + ); + break; + }; }; }; -}); + return ( + <div + className={[ 'focusable', `c${id}` ].join(' ')} + tabIndex={0} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUpHandler} + onFocus={onFocus} + > + {element} + </div> + ); + +})); export default BlockFile; \ No newline at end of file diff --git a/src/ts/component/block/table/cell.tsx b/src/ts/component/block/table/cell.tsx index f9344424f7..0b8698e8e4 100644 --- a/src/ts/component/block/table/cell.tsx +++ b/src/ts/component/block/table/cell.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; import { I, U, J, keyboard } from 'Lib'; import { Icon, Block } from 'Component'; @@ -10,164 +10,152 @@ interface Props extends I.BlockComponentTable { column: I.Block; }; -const BlockTableCell = observer(class BlockTableCell extends React.Component<Props> { +const BlockTableCell = observer(forwardRef<{}, Props>((props, ref) => { - constructor (props: Props) { - super(props); + const { + readonly, block, rowIdx, columnIdx, row, column, onHandleRow, onHandleColumn, onOptions, onCellFocus, onCellBlur, onCellClick, onCellEnter, + onCellLeave, onCellKeyDown, onCellKeyUp, onResizeStart, onDragStartColumn, onDragStartRow, onEnterHandle, onLeaveHandle, onCellUpdate + } = props; - this.onMouseDown = this.onMouseDown.bind(this); - }; + const { isHeader } = row.content; + const cn = [ 'cell', 'column' + column.id ]; + const cellId = [ row.id, column.id ].join('-'); + const inner = <div className="inner" />; + const cnm = [ 'menu' ]; - render () { - const { - readonly, block, rowIdx, columnIdx, row, column, onHandleRow, onHandleColumn, onOptions, onCellFocus, onCellBlur, onCellClick, onCellEnter, - onCellLeave, onCellKeyDown, onCellKeyUp, onResizeStart, onDragStartColumn, onDragStartRow, onEnterHandle, onLeaveHandle, onCellUpdate - } = this.props; + if (block) { + cn.push('align-v' + block.vAlign); - if (!row || !column) { - return null; + if (block.bgColor) { + cnm.push(`bgColor bgColor-${block.bgColor}`); }; + }; + + const Handle = (item: any) => { + const cn = [ 'handle' ]; - const { isHeader } = row.content; - const cn = [ 'cell', 'column' + column.id ]; - const cellId = [ row.id, column.id ].join('-'); - const inner = <div className="inner" />; - const cnm = [ 'menu' ]; + let onDragStart = null; + let onClick = null; + let canDrag = true; - if (block) { - cn.push('align-v' + block.vAlign); + switch (item.type) { + case I.BlockType.TableColumn: + cn.push('handleColumn canDrag'); - if (block.bgColor) { - cnm.push(`bgColor bgColor-${block.bgColor}`); - }; + onDragStart = e => onDragStartColumn(e, column.id); + onClick = e => onHandleColumn(e, item.type, row.id, column.id, cellId); + break; + + case I.BlockType.TableRow: + cn.push('handleRow'); + canDrag = !isHeader; + + if (canDrag) { + onDragStart = e => onDragStartRow(e, row.id); + }; + onClick = e => onHandleRow(e, item.type, row.id, column.id, cellId); + break; }; - const Handle = (item: any) => { - const cn = [ 'handle' ]; - - let onDragStart = null; - let onClick = null; - let canDrag = true; - - switch (item.type) { - case I.BlockType.TableColumn: - cn.push('handleColumn canDrag'); - - onDragStart = e => onDragStartColumn(e, column.id); - onClick = e => onHandleColumn(e, item.type, row.id, column.id, cellId); - break; - - case I.BlockType.TableRow: - cn.push('handleRow'); - canDrag = !isHeader; - - if (canDrag) { - onDragStart = e => onDragStartRow(e, row.id); - }; - onClick = e => onHandleRow(e, item.type, row.id, column.id, cellId); - break; - }; - - if (canDrag) { - cn.push('canDrag'); - }; - - return ( - <div - className={cn.join(' ')} - draggable={canDrag} - onMouseEnter={e => onEnterHandle(e, item.type, row.id, column.id)} - onMouseLeave={e => onLeaveHandle(e)} - onClick={onClick} - onDragStart={onDragStart} - onContextMenu={onClick} - > - <div className="inner" /> - </div> - ); + if (canDrag) { + cn.push('canDrag'); + }; + + return ( + <div + className={cn.join(' ')} + draggable={canDrag} + onMouseEnter={e => onEnterHandle(e, item.type, row.id, column.id)} + onMouseLeave={e => onLeaveHandle(e)} + onClick={onClick} + onDragStart={onDragStart} + onContextMenu={onClick} + > + <div className="inner" /> + </div> + ); + }; + + const EmptyBlock = () => { + const cn = [ 'block', 'blockText', 'noPlus', 'align0' ]; + const cv = [ 'value' ]; + + if (readonly) { + cn.push('isReadonly'); + cv.push('isReadonly'); }; - const EmptyBlock = () => { - const cn = [ 'block', 'blockText', 'noPlus', 'align0' ]; - const cv = [ 'value' ]; - - if (readonly) { - cn.push('isReadonly'); - cv.push('isReadonly'); - }; - - return ( - <div className={cn.join(' ')}> - <div className="wrapContent"> - <div className="selectionTarget"> - <div className="dropTarget"> - <div className="flex"> - <div className="markers" /> - <div className="editableWrap"> - <div - id="value" - className={cv.join(' ')} - contentEditable={!readonly} - suppressContentEditableWarning={true} - onFocus={e => onCellFocus(e, row.id, column.id, cellId)} - onBlur={e => onCellBlur(e, row.id, column.id, cellId)} - onDragStart={e => e.preventDefault()} - /> - </div> + return ( + <div className={cn.join(' ')}> + <div className="wrapContent"> + <div className="selectionTarget"> + <div className="dropTarget"> + <div className="flex"> + <div className="markers" /> + <div className="editableWrap"> + <div + id="value" + className={cv.join(' ')} + contentEditable={!readonly} + suppressContentEditableWarning={true} + onFocus={e => onCellFocus(e, row.id, column.id, cellId)} + onBlur={e => onCellBlur(e, row.id, column.id, cellId)} + onDragStart={e => e.preventDefault()} + /> </div> </div> </div> </div> </div> - ); - }; - - return ( - <div - id={`cell-${cellId}`} - className={cn.join(' ')} - onClick={e => onCellClick(e, row.id, column.id, cellId)} - onMouseEnter={e => onCellEnter(e, row.id, column.id, cellId)} - onMouseLeave={e => onCellLeave(e, row.id, column.id, cellId)} - onMouseDown={this.onMouseDown} - {...U.Common.dataProps({ 'column-id': column.id })} - > - {!rowIdx ? <Handle key={'handle-column-' + cellId} type={I.BlockType.TableColumn} {...column} /> : ''} - {!columnIdx ? <Handle key={'handle-row-' + cellId} type={I.BlockType.TableRow} {...row} /> : ''} - - {block ? ( - <Block - key={`block-${cellId}`} - {...this.props} - block={block} - isInsideTable={true} - className="noPlus" - onKeyDown={(e: any, text: string, marks: I.Mark[], range: I.TextRange, props: any) => { - onCellKeyDown(e, row.id, column.id, cellId, text, marks, range, props); - }} - onKeyUp={(e: any, text: string, marks: I.Mark[], range: I.TextRange, props: any) => { - onCellKeyUp(e, row.id, column.id, cellId, text, marks, range, props); - }} - onUpdate={() => onCellUpdate(cellId)} - onFocus={e => onCellFocus(e, row.id, column.id, cellId)} - onBlur={e => onCellBlur(e, row.id, column.id, cellId)} - getWrapperWidth={() => J.Size.editor} - /> - ) : ( - <EmptyBlock /> - )} - - {!readonly ? <div className="resize" onMouseDown={e => onResizeStart(e, column.id)} /> : ''} - <Icon className={cnm.join(' ')} inner={inner} onClick={e => onOptions(e, I.BlockType.Text, row.id, column.id, cellId)} /> </div> ); }; - onMouseDown (e: any) { + const onMouseDown = () => { keyboard.disableSelection(true); $(window).off('mousedown.table-cell').on('mousedown.table-cell', () => keyboard.disableSelection(false)); }; -}); + return ( + <div + id={`cell-${cellId}`} + className={cn.join(' ')} + onClick={e => onCellClick(e, row.id, column.id, cellId)} + onMouseEnter={e => onCellEnter(e, row.id, column.id, cellId)} + onMouseLeave={e => onCellLeave(e, row.id, column.id, cellId)} + onMouseDown={onMouseDown} + {...U.Common.dataProps({ 'column-id': column.id })} + > + {!rowIdx ? <Handle key={'handle-column-' + cellId} type={I.BlockType.TableColumn} {...column} /> : ''} + {!columnIdx ? <Handle key={'handle-row-' + cellId} type={I.BlockType.TableRow} {...row} /> : ''} + + {block ? ( + <Block + key={`block-${cellId}`} + {...props} + block={block} + isInsideTable={true} + className="noPlus" + onKeyDown={(e: any, text: string, marks: I.Mark[], range: I.TextRange, props: any) => { + onCellKeyDown(e, row.id, column.id, cellId, text, marks, range, props); + }} + onKeyUp={(e: any, text: string, marks: I.Mark[], range: I.TextRange, props: any) => { + onCellKeyUp(e, row.id, column.id, cellId, text, marks, range, props); + }} + onUpdate={() => onCellUpdate(cellId)} + onFocus={e => onCellFocus(e, row.id, column.id, cellId)} + onBlur={e => onCellBlur(e, row.id, column.id, cellId)} + getWrapperWidth={() => J.Size.editor} + /> + ) : ( + <EmptyBlock /> + )} + + {!readonly ? <div className="resize" onMouseDown={e => onResizeStart(e, column.id)} /> : ''} + <Icon className={cnm.join(' ')} inner={inner} onClick={e => onOptions(e, I.BlockType.Text, row.id, column.id, cellId)} /> + </div> + ); + +})); export default BlockTableCell; \ No newline at end of file diff --git a/src/ts/component/block/table/row.tsx b/src/ts/component/block/table/row.tsx index b6e949787a..2299c97c2b 100644 --- a/src/ts/component/block/table/row.tsx +++ b/src/ts/component/block/table/row.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { I, S } from 'Lib'; import { observer } from 'mobx-react'; import Cell from './cell'; @@ -7,49 +7,45 @@ interface Props extends I.BlockComponentTable { onRowUpdate: (rowId: string) => void; }; -const BlockTableRow = observer(class BlockTableRow extends React.Component<Props> { +const BlockTableRow = observer(forwardRef<{}, Props>((props, ref) => { - render () { - const { rootId, block, index, getData } = this.props; - const { columns } = getData(); - const childrenIds = S.Block.getChildrenIds(rootId, block.id); - const children = S.Block.getChildren(rootId, block.id); - const length = childrenIds.length; - const cn = [ 'row' ]; + const { rootId, block, index, getData, onRowUpdate } = props; + const { columns } = getData(); + const childrenIds = S.Block.getChildrenIds(rootId, block.id); + const children = S.Block.getChildren(rootId, block.id); + const length = childrenIds.length; + const cn = [ 'row' ]; - if (block.content.isHeader) { - cn.push('isHeader'); - }; - - return ( - <div id={`row-${block.id}`} className={cn.join(' ')}> - {columns.map((column: any, i: number) => { - const child = children.find(it => it.id == [ block.id, column.id ].join('-')); - return ( - <Cell - key={`cell-${block.id}-${column.id}`} - {...this.props} - block={child} - index={i} - rowIdx={index} - row={block} - columnIdx={i} - column={columns[i]} - /> - ); - })} - </div> - ); + if (block.content.isHeader) { + cn.push('isHeader'); }; - componentDidUpdate () { - const { onRowUpdate, block } = this.props; - + useEffect(() => { if (onRowUpdate) { onRowUpdate(block.id); }; - }; - -}); + }); + + return ( + <div id={`row-${block.id}`} className={cn.join(' ')}> + {columns.map((column: any, i: number) => { + const child = children.find(it => it.id == [ block.id, column.id ].join('-')); + return ( + <Cell + key={`cell-${block.id}-${column.id}`} + {...props} + block={child} + index={i} + rowIdx={index} + row={block} + columnIdx={i} + column={columns[i]} + /> + ); + })} + </div> + ); + +})); export default BlockTableRow; \ No newline at end of file diff --git a/src/ts/component/block/tableOfContents.tsx b/src/ts/component/block/tableOfContents.tsx index a642cf6421..d81cdb4944 100644 --- a/src/ts/component/block/tableOfContents.tsx +++ b/src/ts/component/block/tableOfContents.tsx @@ -1,81 +1,29 @@ -import * as React from 'react'; +import React, { forwardRef, KeyboardEvent } from 'react'; import { I, S, U, J, focus, translate } from 'Lib'; import { observer } from 'mobx-react'; -const BlockTableOfContents = observer(class BlockTableOfContents extends React.Component<I.BlockComponent> { +const BlockTableOfContents = observer(forwardRef<{}, I.BlockComponent>((props, ref) => { - _isMounted = false; - - constructor (props: I.BlockComponent) { - super(props); - - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onFocus = this.onFocus.bind(this); - }; - - render () { - const { block } = this.props; - const cn = [ 'wrap', 'focusable', 'c' + block.id ]; - const tree = this.getTree(); - - const Item = (item: any) => { - return ( - <div - className="item" - onClick={e => this.onClick(e, item.id)} - style={{ paddingLeft: item.depth * 24 }} - > - <span>{item.text}</span> - </div> - ); - }; - - return ( - <div className={cn.join(' ')} tabIndex={0} onKeyDown={this.onKeyDown} onKeyUp={this.onKeyUp} onFocus={this.onFocus}> - {!tree.length ? ( - <div className="empty">{translate('blockTableOfContentsAdd')}</div> - ) : ( - <React.Fragment> - {tree.map((item: any, i: number) => ( - <Item key={i} {...item} /> - ))} - </React.Fragment> - )} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentWillUnmount () { - this._isMounted = false; - }; - - onKeyDown (e: any) { - const { onKeyDown } = this.props; + const { rootId, block, isPopup, onKeyDown, onKeyUp } = props; + const cn = [ 'wrap', 'focusable', `c${block.id}` ]; + const onKeyDownHandler = (e: KeyboardEvent) => { if (onKeyDown) { - onKeyDown(e, '', [], { from: 0, to: 0 }, this.props); + onKeyDown(e, '', [], { from: 0, to: 0 }, props); }; }; - onKeyUp (e: any) { - const { onKeyUp } = this.props; - + const onKeyUpHandler = (e: KeyboardEvent) => { if (onKeyUp) { - onKeyUp(e, '', [], { from: 0, to: 0 }, this.props); + onKeyUp(e, '', [], { from: 0, to: 0 }, props); }; }; - onFocus () { - focus.set(this.props.block.id, { from: 0, to: 0 }); + const onFocus = () => { + focus.set(block.id, { from: 0, to: 0 }); }; - getTree () { - const { rootId } = this.props; + const getTree = () => { const blocks = S.Block.unwrapTree([ S.Block.wrapTree(rootId, rootId) ]).filter(it => it.isTextHeader()); const list: any[] = []; @@ -107,12 +55,12 @@ const BlockTableOfContents = observer(class BlockTableOfContents extends React.C text: String(block.content.text || translate('defaultNamePage')), }); }); + return list; }; - onClick (e: any, id: string) { - const { isPopup } = this.props; - const node = $('.focusable.c' + id); + const onClick = (e: any, id: string) => { + const node = $(`.focusable.c${id}`); if (!node.length) { return; @@ -126,7 +74,39 @@ const BlockTableOfContents = observer(class BlockTableOfContents extends React.C container.scrollTop(y); }; - -}); + + const Item = (item: any) => ( + <div + className="item" + onClick={e => onClick(e, item.id)} + style={{ paddingLeft: item.depth * 24 }} + > + <span>{item.text}</span> + </div> + ); + + const tree = getTree(); + + return ( + <div + className={cn.join(' ')} + tabIndex={0} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUpHandler} + onFocus={onFocus} + > + {!tree.length ? ( + <div className="empty">{translate('blockTableOfContentsAdd')}</div> + ) : ( + <> + {tree.map((item: any, i: number) => ( + <Item key={i} {...item} /> + ))} + </> + )} + </div> + ); + +})); export default BlockTableOfContents; \ No newline at end of file diff --git a/src/ts/component/block/text.tsx b/src/ts/component/block/text.tsx index df112ca0a4..5e5619e03d 100644 --- a/src/ts/component/block/text.tsx +++ b/src/ts/component/block/text.tsx @@ -63,7 +63,9 @@ const BlockText = observer(class BlockText extends React.Component<Props> { const { text, marks, style, checked, color, iconEmoji, iconImage } = content; const { theme } = S.Common; const root = S.Block.getLeaf(rootId, rootId); - const cv: string[] = [ 'value', 'focusable', 'c' + id ]; + const cn = [ 'flex' ]; + const cv = [ 'value', 'focusable', 'c' + id ]; + const checkRtl = keyboard.isRtl || U.Common.checkRtl(text); let marker: any = null; let placeholder = translate('placeholderBlock'); @@ -74,6 +76,10 @@ const BlockText = observer(class BlockText extends React.Component<Props> { cv.push('textColor textColor-' + color); }; + if (checkRtl) { + cn.push('isRtl'); + }; + // Subscriptions for (const mark of marks) { if ([ I.MarkType.Mention, I.MarkType.Object ].includes(mark.type)) { @@ -168,7 +174,7 @@ const BlockText = observer(class BlockText extends React.Component<Props> { return ( <div ref={node => this.node = node} - className="flex" + className={cn.join(' ')} > <div className="markers"> {marker ? <Marker {...marker} id={id} color={color} readonly={readonly} /> : ''} @@ -194,6 +200,7 @@ const BlockText = observer(class BlockText extends React.Component<Props> { onMouseUp={this.onMouseUp} onInput={this.onInput} onDragStart={e => e.preventDefault()} + onCompositionEnd={this.onKeyUp} /> </div> ); @@ -305,15 +312,14 @@ const BlockText = observer(class BlockText extends React.Component<Props> { const tag = Mark.getTag(I.MarkType.Latex); const code = Mark.getTag(I.MarkType.Code); const value = this.refEditable.getHtmlValue(); - const reg = /(^|[^\d<]+)?\$((?:[^$<]|\.)*?)\$([^\d]|$)/gi; - const regCode = new RegExp(`^${code}`, 'i'); + const reg = /(^|[^\d<\$]+)?\$((?:[^$<]|\.)*?)\$([^\d>\$]+|$)/gi; + const regCode = new RegExp(`^${code}|${code}$`, 'i'); if (!/\$((?:[^$<]|\.)*?)\$/.test(value)) { return; }; const match = value.matchAll(reg); - const render = (s: string) => { s = U.Common.fromHtmlSpecialChars(s); @@ -340,7 +346,17 @@ const BlockText = observer(class BlockText extends React.Component<Props> { const m3 = String(m[3] || ''); // Skip inline code marks - if (regCode.test(m1)) { + if (regCode.test(m1) || regCode.test(m3)) { + return; + }; + + // Skip Brazilian Real + if (/R$/.test(m1) || /R$/.test(m2)) { + return; + }; + + // Escaped $ sign + if (/\\$/.test(m1) || /\\$/.test(m2)) { return; }; @@ -697,6 +713,8 @@ const BlockText = observer(class BlockText extends React.Component<Props> { const oneSymbolBefore = range ? value[range.from - 1] : ''; const twoSymbolBefore = range ? value[range.from - 2] : ''; + keyboard.setRtl(U.Common.checkRtl(value)); + if (range) { isAllowedMenu = isAllowedMenu && (!range.from || (range.from == 1) || [ ' ', '\n', '(', '[', '"', '\'' ].includes(twoSymbolBefore)); }; @@ -1090,7 +1108,7 @@ const BlockText = observer(class BlockText extends React.Component<Props> { }; onSelect () { - if (keyboard.isContextDisabled) { + if (keyboard.isContextDisabled || keyboard.isComposition) { return; }; diff --git a/src/ts/component/block/type.tsx b/src/ts/component/block/type.tsx index 247b216726..f4422e63a8 100644 --- a/src/ts/component/block/type.tsx +++ b/src/ts/component/block/type.tsx @@ -180,7 +180,6 @@ const BlockType = observer(class BlockType extends React.Component<I.BlockCompon const { block } = this.props; const element = `#block-${block.id} #item-menu`; const obj = $(element); - const items = this.getItems(); S.Menu.open('typeSuggest', { element: `#block-${block.id} #item-menu`, @@ -192,8 +191,7 @@ const BlockType = observer(class BlockType extends React.Component<I.BlockCompon data: { filter: '', filters: [ - { relationKey: 'id', condition: I.FilterCondition.NotIn, value: items.map(it => it.id) }, - { relationKey: 'recommendedLayout', condition: I.FilterCondition.In, value: U.Object.getPageLayouts() }, + { relationKey: 'recommendedLayout', condition: I.FilterCondition.In, value: U.Object.getPageLayouts().concat(U.Object.getSetLayouts()) }, { relationKey: 'uniqueKey', condition: I.FilterCondition.NotEqual, value: J.Constant.typeKey.template } ], onClick: (item: any) => { diff --git a/src/ts/component/drag/box.tsx b/src/ts/component/drag/box.tsx index 237ecf4c46..57c6b68dbc 100644 --- a/src/ts/component/drag/box.tsx +++ b/src/ts/component/drag/box.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC, useRef } from 'react'; import { U } from 'Lib'; interface Props { @@ -6,61 +6,29 @@ interface Props { onDragEnd(oldIndex: number, newIndex: number): void; }; -class DragBox extends React.Component<Props> { - - _isMounted = false; - node: any = null; - cache: any = {}; - ox = 0; - oy = 0; - oldIndex = -1; - newIndex = -1; - - constructor (props: Props) { - super(props); - - this.onDragStart = this.onDragStart.bind(this); - }; - - render () { - const children = React.Children.map(this.props.children, (child: any) => { - return React.cloneElement(child, { - onDragStart: this.onDragStart - }); - }); +const DragBox: FC<Props> = ({ children: initialChildren, onDragEnd }) => { - return ( - <span - ref={node => this.node = node} - className="dragbox" - > - {children} - </span> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentWillUnmount () { - this._isMounted = false; - }; + const nodeRef = useRef(null); + const cache = useRef({}); + const ox = useRef(0); + const oy = useRef(0); + const oldIndex = useRef(-1); + const newIndex = useRef(-1); - onDragStart (e: any) { + const onDragStart = (e: any) => { e.preventDefault(); e.stopPropagation(); - if (!this._isMounted) { + if (!nodeRef.current) { return; }; const win = $(window); - const node = $(this.node); + const node = $(nodeRef.current); const items = node.find('.isDraggable'); const element = $(e.currentTarget); const clone = element.clone(); - const offset = node.offset(); + const { left, top } = node.offset(); items.each((i: number, item: any) => { item = $(item); @@ -71,7 +39,7 @@ class DragBox extends React.Component<Props> { }; const p = item.position(); - this.cache[id] = { + cache.current[id] = { x: p.left, y: p.top, width: item.outerWidth(), @@ -79,58 +47,57 @@ class DragBox extends React.Component<Props> { }; }); - this.ox = offset.left; - this.oy = offset.top; - this.oldIndex = element.data('index'); + ox.current = left; + oy.current = top; + oldIndex.current = element.data('index'); node.append(clone); clone.addClass('isClone'); element.addClass('isDragging'); win.off('mousemove.dragbox mouseup.dragbox'); - win.on('mousemove.dragbox', e => this.onDragMove(e)); - win.on('mouseup.dragbox', e => this.onDragEnd(e)); + win.on('mousemove.dragbox', e => onDragMove(e)); + win.on('mouseup.dragbox', e => onDragEndHandler(e)); }; - onDragMove (e: any) { - if (!this._isMounted) { + const onDragMove = (e: any) => { + if (!nodeRef.current) { return; }; - const node = $(this.node); + const node = $(nodeRef.current); const items = node.find('.isDraggable'); const clone = node.find('.isDraggable.isClone'); const width = clone.outerWidth(); const height = clone.outerHeight(); - const x = e.pageX - this.ox - width / 2; - const y = e.pageY - this.oy - height / 2; + const x = e.pageX - ox.current - width / 2; + const y = e.pageY - oy.current - height / 2; const center = x + width / 2; - this.newIndex = -1; + newIndex.current = -1; node.find('.isDraggable.isOver').removeClass('isOver left right'); clone.css({ transform: `translate3d(${x}px,${y}px,0px)` }); for (let i = 0; i < items.length; ++i) { const el = $(items.get(i)); - const rect = this.cache[el.data('id')]; + const rect = cache.current[el.data('id')]; if (rect && U.Common.rectsCollide({ x: center, y, width: 2, height }, rect)) { const isLeft = center <= rect.x + rect.width / 2; - this.newIndex = i; + newIndex.current = i; el.addClass('isOver ' + (isLeft ? 'left' : 'right')); break; }; }; }; - onDragEnd (e: any) { - if (!this._isMounted) { + const onDragEndHandler = (e: any) => { + if (!nodeRef.current) { return; }; - const node = $(this.node); - const { onDragEnd } = this.props; + const node = $(nodeRef.current); node.find('.isDraggable.isClone').remove(); node.find('.isDraggable.isDragging').removeClass('isDragging'); @@ -138,15 +105,25 @@ class DragBox extends React.Component<Props> { $(window).off('mousemove.dragbox mouseup.dragbox'); - if (this.newIndex >= 0) { - onDragEnd(this.oldIndex, this.newIndex); + if (newIndex.current >= 0) { + onDragEnd(oldIndex.current, newIndex.current); }; - this.cache = {}; - this.oldIndex = -1; - this.newIndex = -1; + cache.current = {}; + oldIndex.current = -1; + newIndex.current = -1; }; - + + const children = React.Children.map(initialChildren, (child: any) => React.cloneElement(child, { onDragStart })); + + return ( + <span + ref={nodeRef} + className="dragbox" + > + {children} + </span> + ); }; export default DragBox; \ No newline at end of file diff --git a/src/ts/component/drag/layer.tsx b/src/ts/component/drag/layer.tsx index e76b9f67fc..409674ac91 100644 --- a/src/ts/component/drag/layer.tsx +++ b/src/ts/component/drag/layer.tsx @@ -1,93 +1,22 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useImperativeHandle } from 'react'; import * as ReactDOM from 'react-dom'; import $ from 'jquery'; +import { observer } from 'mobx-react'; import { I, M, S, U, J, keyboard } from 'Lib'; -interface State { - rootId: string; - type: I.DropType; - width: number; - ids: string[]; -}; - -class DragLayer extends React.Component<object, State> { - - _isMounted = false; - node: any = null; - state = { - rootId: '', - type: I.DropType.None, - width: 0, - ids: [] as string[], - }; - - constructor (props: any) { - super(props); - - this.show = this.show.bind(this); - this.hide = this.hide.bind(this); - }; +const DragLayer = observer(forwardRef((_, ref: any) => { - render () { - const { width } = this.state; - - return ( - <div - ref={node => this.node = node} - id="dragLayer" - className="dragLayer" - style={{ width }} - > - <div id="inner" className="inner" /> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentDidUpdate () { - if (!this._isMounted) { - return; - }; + const nodeRef = useRef(null); - const node = $(this.node); - - node.find('.block').attr({ id: '' }); - node.find('.selectionTarget').attr({ id: '' }); - - this.renderContent(); - }; - - componentWillUnmount () { - this._isMounted = false; - }; - - show (rootId: string, type: I.DropType, ids: string[], component: any, x: number, y: number) { - if (!this._isMounted) { - return; - }; - + const show = (rootId: string, type: I.DropType, ids: string[], component: any) => { const comp = $(ReactDOM.findDOMNode(component)); const rect = (comp.get(0) as Element).getBoundingClientRect(); - - this.setState({ rootId, type, width: rect.width - J.Size.blockMenu, ids }); - }; - - hide () { - if (this._isMounted) { - this.setState({ rootId: '', type: I.DropType.None, ids: [], width: 0 }); - }; - }; - - renderContent () { - const { rootId, type, ids } = this.state; - const node = $(this.node); + const width = rect.width - J.Size.blockMenu; + const node = $(nodeRef.current); const inner = node.find('#inner').html(''); const container = U.Common.getPageContainer(keyboard.isPopup()); const wrap = $('<div></div>'); - + switch (type) { case I.DropType.Block: { wrap.addClass('blocks'); @@ -157,8 +86,31 @@ class DragLayer extends React.Component<object, State> { }; inner.append(wrap); + + node.css({ width }); + node.find('.block').attr({ id: '' }); + node.find('.selectionTarget').attr({ id: '' }); }; - -}; + + const hide = () => { + $(nodeRef.current).find('#inner').html(''); + }; + + useImperativeHandle(ref, () => ({ + show, + hide, + })); + + return ( + <div + ref={nodeRef} + id="dragLayer" + className="dragLayer" + > + <div id="inner" className="inner" /> + </div> + ); + +})); export default DragLayer; \ No newline at end of file diff --git a/src/ts/component/drag/provider.tsx b/src/ts/component/drag/provider.tsx index 269ab591ec..8d0910fb70 100644 --- a/src/ts/component/drag/provider.tsx +++ b/src/ts/component/drag/provider.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useImperativeHandle } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; @@ -10,64 +10,41 @@ interface Props { children?: React.ReactNode; }; -const OFFSET = 100; - -const DragProvider = observer(class DragProvider extends React.Component<Props> { - - node: any = null; - refLayer: any = null; - position: I.BlockPosition = I.BlockPosition.None; - hoverData: any = null; - canDrop = false; - init = false; - top = 0; - frame = 0; - - objects: any = null; - objectData: Map<string, any> = new Map(); - - origin: any = null; - - constructor (props: Props) { - super(props); - - this.onDragOver = this.onDragOver.bind(this); - this.onDragStart = this.onDragStart.bind(this); - this.onDragEnd = this.onDragEnd.bind(this); - this.onDropCommon = this.onDropCommon.bind(this); - this.onDrop = this.onDrop.bind(this); - }; +interface DragProviderRefProps { + onDragStart: (e: any, dropType: I.DropType, ids: string[], component: any) => void; + onScroll: () => void; +}; - render () { - const { children } = this.props; - - return ( - <div - ref={node => this.node = node} - id="dragProvider" - className="dragProvider" - onDragOver={this.onDragOver} - onDrop={this.onDropCommon} - > - <DragLayer {...this.props} ref={ref => this.refLayer = ref} /> - {children} - </div> - ); - }; +const OFFSET = 100; - initData () { - if (this.init) { +const DragProvider = observer(forwardRef<DragProviderRefProps, Props>((props, ref: any) => { + + const { children } = props; + const nodeRef = useRef(null); + const layerRef = useRef(null); + const isInitialised = useRef(false); + const position = useRef(I.BlockPosition.None); + const hoverData = useRef(null); + const canDrop = useRef(false); + const top = useRef(0); + const frame = useRef(0); + const objects = useRef(null); + const objectData = useRef(new Map()); + const origin = useRef(null); + + const initData = () => { + if (isInitialised.current) { return; }; const isPopup = keyboard.isPopup(); const container = $(isPopup ? '#popupPage-innerWrap' : '#root'); - this.clearState(); - this.init = true; - this.objects = container.find('.dropTarget.isDroppable'); + clearState(); + isInitialised.current = true; + objects.current = container.find('.dropTarget.isDroppable'); - this.objects.each((i: number, el: any) => { + objects.current.each((i: number, el: any) => { const item = $(el); const data = { id: item.attr('data-id'), @@ -98,7 +75,7 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> }; }; - this.objectData.set(data.cacheKey, { + objectData.current.set(data.cacheKey, { ...data, obj: item, index: i, @@ -114,11 +91,11 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> }); }; - onDropCommon (e: any) { + const onDropCommon = (e: any) => { e.preventDefault(); if (keyboard.isCommonDropDisabled) { - this.clearState(); + clearState(); return; }; @@ -135,17 +112,16 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> const isFileDrop = dataTransfer.files && dataTransfer.files.length; const last = S.Block.getFirstBlock(rootId, -1, it => it && it.canCreateBlock()); - let position = this.position; let data: any = null; let targetId = ''; let target: any = null; - if (this.hoverData && (this.position != I.BlockPosition.None)) { - data = this.hoverData; + if (hoverData.current && (position.current != I.BlockPosition.None)) { + data = hoverData.current; } else if (last && isFileDrop) { - data = this.objectData.get([ I.DropType.Block, last.id ].join('-')); - position = I.BlockPosition.Bottom; + data = objectData.current.get([ I.DropType.Block, last.id ].join('-')); + position.current = I.BlockPosition.Bottom; }; if (data) { @@ -156,7 +132,7 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> // Last drop zone if (targetId == 'blockLast') { targetId = ''; - position = I.BlockPosition.Bottom; + position.current = I.BlockPosition.Bottom; }; // String items drop @@ -165,7 +141,7 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> C.BlockPaste(rootId, targetId, { from: 0, to: 0 }, [], false, { html }, ''); }); - this.clearState(); + clearState(); return; }; @@ -180,43 +156,43 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> console.log('[DragProvider].onDrop paths', paths); - C.FileDrop(rootId, targetId, position, paths, () => { - if (target && target.isTextToggle() && (position == I.BlockPosition.InnerFirst)) { + C.FileDrop(rootId, targetId, position.current, paths, () => { + if (target && target.isTextToggle() && (position.current == I.BlockPosition.InnerFirst)) { S.Block.toggle(rootId, targetId, true); }; }); } else - if (data && this.canDrop && (position != I.BlockPosition.None)) { - this.onDrop(e, data.dropType, targetId, position); + if (data && canDrop && (position.current != I.BlockPosition.None)) { + onDrop(e, data.dropType, targetId, position.current); }; - this.clearState(); + clearState(); }; - onDragStart (e: any, dropType: I.DropType, ids: string[], component: any) { + const onDragStart = (e: any, dropType: I.DropType, ids: string[], component: any) => { const rootId = keyboard.getRootId(); const isPopup = keyboard.isPopup(); const selection = S.Common.getRef('selectionProvider'); const win = $(window); - const node = $(this.node); + const node = $(nodeRef.current); const container = U.Common.getScrollContainer(isPopup); const sidebar = $('#sidebar'); const layer = $('#dragLayer'); const body = $('body'); const dataTransfer = { rootId, dropType, ids, withAlt: e.altKey }; - this.origin = component; + origin.current = component; e.stopPropagation(); focus.clear(true); console.log('[DragProvider].onDragStart', dropType, ids); - this.top = container.scrollTop(); - this.refLayer.show(rootId, dropType, ids, component); - this.setClass(ids); - this.initData(); - this.unbind(); + top.current = container.scrollTop(); + layerRef.current.show(rootId, dropType, ids, component); + setClass(ids); + initData(); + unbind(); e.dataTransfer.setDragImage(layer.get(0), 0, 0); e.dataTransfer.setData('text/plain', JSON.stringify(dataTransfer)); @@ -229,11 +205,11 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> keyboard.disableSelection(true); Preview.hideAll(); - win.on('drag.drag', e => this.onDrag(e)); - win.on('dragend.drag', e => this.onDragEnd(e)); + win.on('drag.drag', e => onDrag(e)); + win.on('dragend.drag', e => onDragEnd(e)); - container.off('scroll.drag').on('scroll.drag', throttle(() => this.onScroll(), 20)); - sidebar.off('scroll.drag').on('scroll.drag', throttle(() => this.onScroll(), 20)); + container.off('scroll.drag').on('scroll.drag', throttle(() => onScroll(), 20)); + sidebar.off('scroll.drag').on('scroll.drag', throttle(() => onScroll(), 20)); $('.colResize.active').removeClass('active'); scrollOnMove.onMouseDown(e, isPopup); @@ -244,7 +220,7 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> selection?.hide(); }; - onDragOver (e: any) { + const onDragOver = (e: any) => { if (keyboard.isCommonDropDisabled) { return; }; @@ -252,25 +228,25 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> e.preventDefault(); e.stopPropagation(); - this.initData(); - this.checkNodes(e, e.pageX, e.pageY); + initData(); + checkNodes(e, e.pageX, e.pageY); }; - onDrag (e: any) { + const onDrag = (e: any) => { scrollOnMove.onMouseMove(e.clientX, e.clientY); }; - onDragEnd (e: any) { + const onDragEnd = (e: any) => { const isPopup = keyboard.isPopup(); - const node = $(this.node); + const node = $(nodeRef.current); const container = U.Common.getScrollContainer(isPopup); const sidebar = $('#sidebar'); const body = $('body'); - this.refLayer.hide(); - this.clearState(); - this.clearStyle(); - this.unbind(); + layerRef.current.hide(); + clearState(); + clearStyle(); + unbind(); keyboard.setDragging(false); keyboard.disableSelection(false); @@ -285,21 +261,20 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> scrollOnMove.onMouseUp(e); }; - onDrop (e: any, targetType: string, targetId: string, position: I.BlockPosition) { + const onDrop = (e: any, targetType: string, targetId: string, position: I.BlockPosition) => { const selection = S.Common.getRef('selectionProvider'); let data: any = {}; - try { data = JSON.parse(e.dataTransfer.getData('text/plain')) || {}; } catch (e) { /**/ }; + try { data = JSON.parse(e.dataTransfer.getData('text/plain')) || {}; } catch (e) {}; const { rootId, dropType, withAlt } = data; const ids = data.ids || []; + const contextId = rootId; if (!ids.length) { return; }; - const contextId = rootId; - let targetContextId = keyboard.getRootId(); let isToggle = false; @@ -345,7 +320,7 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> case I.DropType.Block: { // Drop into column is targeting last block - if (this.hoverData.isTargetCol) { + if (hoverData.current.isTargetCol) { const childrenIds = S.Block.getChildrenIds(targetContextId, targetId); targetId = childrenIds.length ? childrenIds[childrenIds.length - 1] : ''; @@ -419,12 +394,12 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> switch (position) { case I.BlockPosition.Top: case I.BlockPosition.Bottom: { - if (!this.origin) { + if (!origin.current) { break; }; // Sort - const { onRecordDrop } = this.origin; + const { onRecordDrop } = origin.current; if (onRecordDrop) { onRecordDrop(targetId, ids); @@ -490,16 +465,16 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> console.log('[DragProvider].onDrop from:', contextId, 'to: ', targetContextId); }; - onScroll () { + const onScroll = () => { if (keyboard.isDragging) { - for (const [ key, value ] of this.objectData) { + for (const [ key, value ] of objectData.current) { const { left, top } = value.obj.offset(); - this.objectData.set(key, { ...value, x: left, y: top }); + objectData.current.set(key, { ...value, x: left, y: top }); }; }; }; - checkNodes (e: any, ex: number, ey: number) { + const checkNodes = (e: any, ex: number, ey: number) => { const dataTransfer = e.dataTransfer || e.originalEvent.dataTransfer; const isItemDrag = U.Common.getDataTransferItems(dataTransfer.items).length ? true : false; const isFileDrag = dataTransfer.types.includes('Files'); @@ -512,12 +487,12 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> break; }; }; - } catch (e) { /**/ }; + } catch (e) { }; - this.setHoverData(null); - this.setPosition(I.BlockPosition.None); + setHoverData(null); + setPosition(I.BlockPosition.None); - for (const [ key, value ] of this.objectData) { + for (const [ key, value ] of objectData.current) { const { y, height, dropType } = value; let { x, width } = value; @@ -527,11 +502,12 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> }; if ((ex >= x) && (ex <= x + width) && (ey >= y) && (ey <= y + height)) { - this.setHoverData(value); + setHoverData(value); break; }; }; + const hd = hoverData.current; const dropType = String(data.droptype) || ''; const rootId = String(data.rootid) || ''; const ids = data.ids || []; @@ -552,24 +528,24 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> let col1 = 0; let col2 = 0; - if (this.hoverData) { - this.canDrop = true; + if (hd) { + canDrop.current = true; if (!isFileDrag && (dropType == I.DropType.Block)) { - this.canDrop = this.checkParentIds(ids, this.hoverData.id); + canDrop.current = checkParentIds(ids, hd.id); }; const initVars = () => { - x = this.hoverData.x; - y = this.hoverData.y; - width = this.hoverData.width; - height = this.hoverData.height; - isTargetTop = this.hoverData.isTargetTop; - isTargetBot = this.hoverData.isTargetBot; - isTargetCol = this.hoverData.isTargetCol; - isEmptyToggle = this.hoverData.isEmptyToggle; - - obj = $(this.hoverData.obj); + x = hd.x; + y = hd.y; + width = hd.width; + height = hd.height; + isTargetTop = hd.isTargetTop; + isTargetBot = hd.isTargetBot; + isTargetCol = hd.isTargetCol; + isEmptyToggle = hd.isEmptyToggle; + + obj = $(hd.obj); type = obj.attr('data-type'); style = Number(obj.attr('data-style')) || 0; canDropMiddle = Number(obj.attr('data-drop-middle')) || 0; @@ -582,47 +558,47 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> initVars(); if (ex <= col1) { - this.setPosition(I.BlockPosition.Left); + setPosition(I.BlockPosition.Left); } else if ((ex > col1) && (ex <= col2)) { if (ey <= y + height * 0.3) { - this.setPosition(I.BlockPosition.Top); + setPosition(I.BlockPosition.Top); } else if (ey >= y + height * 0.7) { - this.setPosition(I.BlockPosition.Bottom); + setPosition(I.BlockPosition.Bottom); } else { - this.setPosition(I.BlockPosition.InnerFirst); + setPosition(I.BlockPosition.InnerFirst); }; } else if (ex > col2) { - this.setPosition(I.BlockPosition.Right); + setPosition(I.BlockPosition.Right); }; - if (this.position == I.BlockPosition.Bottom) { - const targetBot = this.objectData.get(this.hoverData.cacheKey + '-bot'); + if (position.current == I.BlockPosition.Bottom) { + const targetBot = objectData.current.get(hd + '-bot'); if (targetBot) { - this.setHoverData(targetBot); + setHoverData(targetBot); initVars(); }; }; // canDropMiddle flag for restricted objects - if ((this.position == I.BlockPosition.InnerFirst) && !canDropMiddle) { - this.recalcPositionY(ey, y, height); + if ((position.current == I.BlockPosition.InnerFirst) && !canDropMiddle) { + recalcPositionY(ey, y, height); }; // Recalc position if dataTransfer items are dragged - if (isItemDrag && (this.position != I.BlockPosition.None)) { - this.recalcPositionY(ey, y, height); + if (isItemDrag && (position.current != I.BlockPosition.None)) { + recalcPositionY(ey, y, height); }; // You can drop vertically on Layout.Row if ((type == I.BlockType.Layout) && (style == I.LayoutStyle.Row)) { if (isTargetTop) { - this.setPosition(I.BlockPosition.Top); + setPosition(I.BlockPosition.Top); }; if (isTargetBot) { - this.setPosition(I.BlockPosition.Bottom); + setPosition(I.BlockPosition.Bottom); }; }; @@ -636,92 +612,82 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> I.TextStyle.Callout, I.TextStyle.Quote, ].includes(style) && - (this.position == I.BlockPosition.Bottom) + (position.current == I.BlockPosition.Bottom) ) { - this.setPosition(I.BlockPosition.None); + setPosition(I.BlockPosition.None); }; - if (this.position != I.BlockPosition.None) { + if (position.current != I.BlockPosition.None) { // You can only drop inside of menu items - if (this.hoverData.dropType == I.DropType.Menu) { - this.setPosition(I.BlockPosition.InnerFirst); + if (hd.dropType == I.DropType.Menu) { + setPosition(I.BlockPosition.InnerFirst); - if (rootId == this.hoverData.targetContextId) { - this.setPosition(I.BlockPosition.None); + if (rootId == hd.targetContextId) { + setPosition(I.BlockPosition.None); }; }; - if (isTargetTop || (this.hoverData.id == 'blockLast')) { - this.setPosition(I.BlockPosition.Top); + if (isTargetTop || (hd.id == 'blockLast')) { + setPosition(I.BlockPosition.Top); }; if (isTargetBot || isTargetCol) { - this.setPosition(I.BlockPosition.Bottom); + setPosition(I.BlockPosition.Bottom); }; if (isEmptyToggle) { - this.setPosition(I.BlockPosition.InnerFirst); + setPosition(I.BlockPosition.InnerFirst); }; }; - if ((dropType == I.DropType.Record) && (this.hoverData.dropType == I.DropType.Record) && !canDropMiddle) { - isReversed ? this.recalcPositionX(ex, x, width) : this.recalcPositionY(ey, y, height); + if ((dropType == I.DropType.Record) && (hd.dropType == I.DropType.Record) && !canDropMiddle) { + isReversed ? recalcPositionX(ex, x, width) : recalcPositionY(ey, y, height); }; - if (this.hoverData.dropType == I.DropType.Widget) { - this.recalcPositionY(ey, y, height); + if (hd.dropType == I.DropType.Widget) { + recalcPositionY(ey, y, height); if (isTargetTop) { - this.setPosition(I.BlockPosition.Top); + setPosition(I.BlockPosition.Top); }; if (isTargetBot) { - this.setPosition(I.BlockPosition.Bottom); + setPosition(I.BlockPosition.Bottom); }; }; }; - if (this.frame) { - raf.cancel(this.frame); + if (frame.current) { + raf.cancel(frame.current); }; - this.frame = raf(() => { - this.clearStyle(); - if ((this.position != I.BlockPosition.None) && this.canDrop && this.hoverData) { - obj.addClass('isOver ' + this.getDirectionClass(this.position)); + frame.current = raf(() => { + clearStyle(); + if ((position.current != I.BlockPosition.None) && canDrop.current && hd) { + obj.addClass(`isOver ${getDirectionClass(position.current)}`); }; }); }; - recalcPositionY = (ey: number, y: number, height: number) => { - if (ey <= y + height * 0.5) { - this.setPosition(I.BlockPosition.Top); - } else - if (ey >= y + height * 0.5) { - this.setPosition(I.BlockPosition.Bottom); - }; + const recalcPositionY = (ey: number, y: number, height: number) => { + setPosition(ey <= y + height * 0.5 ? I.BlockPosition.Top : I.BlockPosition.Bottom); }; - recalcPositionX = (ex: number, x: number, width: number) => { - if (ex <= x + width * 0.5) { - this.setPosition(I.BlockPosition.Top); - } else - if (ex >= x + width * 0.5) { - this.setPosition(I.BlockPosition.Bottom); - }; + const recalcPositionX = (ex: number, x: number, width: number) => { + setPosition(ex <= x + width * 0.5 ? I.BlockPosition.Top : I.BlockPosition.Bottom); }; - setClass (ids: string[]) { + const setClass = (ids: string[]) => { $('.block.isDragging').removeClass('isDragging'); for (const id of ids) { $('#block-' + id).addClass('isDragging'); }; }; - checkParentIds (ids: string[], id: string): boolean { + const checkParentIds = (ids: string[], id: string): boolean => { const parentIds: string[] = []; - this.getParentIds(id, parentIds); + getParentIds(id, parentIds); for (const dropId of ids) { if ((dropId == id) || (parentIds.length && parentIds.includes(dropId))) { @@ -731,7 +697,7 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> return true; }; - getParentIds (blockId: string, parentIds: string[]) { + const getParentIds = (blockId: string, parentIds: string[]) => { const rootId = keyboard.getRootId(); const item = S.Block.getMapElement(rootId, blockId); @@ -740,10 +706,10 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> }; parentIds.push(item.parentId); - this.getParentIds(item.parentId, parentIds); + getParentIds(item.parentId, parentIds); }; - getDirectionClass (dir: I.BlockPosition) { + const getDirectionClass = (dir: I.BlockPosition) => { let c = ''; switch (dir) { case I.BlockPosition.None: c = ''; break; @@ -757,35 +723,53 @@ const DragProvider = observer(class DragProvider extends React.Component<Props> return c; }; - clearStyle () { + const clearStyle = () => { $('.dropTarget.isOver').removeClass('isOver top bottom left right middle'); }; - clearState () { - if (this.hoverData) { - this.setHoverData(null); + const clearState = () => { + if (hoverData.current) { + setHoverData(null); }; - this.clearStyle(); - this.setPosition(I.BlockPosition.None); + clearStyle(); + setPosition(I.BlockPosition.None); - this.init = false; - this.objects = null; - this.objectData.clear(); + isInitialised.current = false; + objects.current = null; + objectData.current.clear(); }; - setHoverData (v: any) { - this.hoverData = v; + const setHoverData = (v: any) => { + hoverData.current = v; }; - setPosition (v: I.BlockPosition) { - this.position = v; + const setPosition = (v: I.BlockPosition) => { + position.current = v; }; - unbind () { + const unbind = () => { $(window).off('drag.drag dragend.drag'); }; -}); - -export default DragProvider; + useImperativeHandle(ref, () => ({ + onDragStart, + onScroll, + })); + + return ( + <div + ref={nodeRef} + id="dragProvider" + className="dragProvider" + onDragOver={onDragOver} + onDrop={onDropCommon} + > + <DragLayer {...props} ref={layerRef} /> + {children} + </div> + ); + +})); + +export default DragProvider; \ No newline at end of file diff --git a/src/ts/component/drag/target.tsx b/src/ts/component/drag/target.tsx index 6403b09c22..d15e327800 100644 --- a/src/ts/component/drag/target.tsx +++ b/src/ts/component/drag/target.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { I, U } from 'Lib'; interface Props { @@ -20,75 +20,62 @@ interface Props { onContextMenu?(e: any): void; }; -class DropTarget extends React.Component<Props> { +const DropTarget: FC<Props> = ({ + id = '', + rootId = '', + cacheKey = '', + targetContextId = '', + dropType = I.DropType.None, + type, + style = 0, + className = '', + canDropMiddle = false, + isTargetTop = false, + isTargetBottom = false, + isTargetColumn = false, + isReversed = false, + children, + onClick, + onContextMenu, +}) => { - public static defaultProps: Props = { - id: '', - rootId: '', - dropType: I.DropType.None, - }; + const key = [ dropType, cacheKey || id ]; + const cn = [ 'dropTarget', 'isDroppable', `root-${rootId}`, `drop-target-${id}`, className ]; - constructor (props: Props) { - super(props); - - this.onClick = this.onClick.bind(this); + if (isTargetTop) { + cn.push('targetTop'); + key.push('top'); }; - - render () { - const { - id, rootId, cacheKey, targetContextId, dropType, type, style, children, className, canDropMiddle, isTargetTop, isTargetBottom, isTargetColumn, - isReversed, onContextMenu, - } = this.props; - const key = [ dropType, cacheKey || id ]; - const cn = [ 'dropTarget', 'isDroppable', 'root-' + rootId, 'drop-target-' + id ]; - - if (className) { - cn.push(className); - }; - if (isTargetTop) { - cn.push('targetTop'); - key.push('top'); - }; - if (isTargetBottom) { - cn.push('targetBot'); - key.push('bot'); - }; - if (isTargetColumn) { - cn.push('targetCol'); - key.push('col'); - }; - - return ( - <div - key={'drop-target-' + id} - className={cn.join(' ')} - onClick={this.onClick} - onContextMenu={onContextMenu} - {...U.Common.dataProps({ - id, - type, - style: Number(style) || 0, - reversed: isReversed, - 'root-id': rootId, - 'cache-key': key.join('-'), - 'drop-type': dropType, - 'context-id': targetContextId, - 'drop-middle': Number(canDropMiddle) || 0, - })} - > - {children} - </div> - ); + if (isTargetBottom) { + cn.push('targetBot'); + key.push('bot'); }; - - onClick (e: any) { - const { onClick } = this.props; - - if (onClick) { - onClick(e); - }; + if (isTargetColumn) { + cn.push('targetCol'); + key.push('col'); }; - + + return ( + <div + key={`drop-target-${id}`} + className={cn.join(' ')} + onClick={onClick} + onContextMenu={onContextMenu} + {...U.Common.dataProps({ + id, + type, + style: Number(style) || 0, + reversed: isReversed, + 'root-id': rootId, + 'cache-key': key.join('-'), + 'drop-type': dropType, + 'context-id': targetContextId, + 'drop-middle': Number(canDropMiddle) || 0, + })} + > + {children} + </div> + ); }; export default DropTarget; \ No newline at end of file diff --git a/src/ts/component/editor/page.tsx b/src/ts/component/editor/page.tsx index 907ff170ad..1203606422 100644 --- a/src/ts/component/editor/page.tsx +++ b/src/ts/component/editor/page.tsx @@ -3,9 +3,8 @@ import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; import { throttle } from 'lodash'; -import { Icon, Loader, Deleted, DropTarget } from 'Component'; +import { Icon, Loader, Deleted, DropTarget, EditorControls } from 'Component'; import { I, C, S, U, J, Key, Preview, Mark, focus, keyboard, Storage, Action, translate, analytics, Renderer, sidebar } from 'Lib'; -import Controls from 'Component/page/elements/head/controls'; import PageHeadEditor from 'Component/page/elements/head/editor'; import Children from 'Component/page/elements/children'; @@ -98,7 +97,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat id="editorWrapper" className="editorWrapper" > - <Controls + <EditorControls ref={ref => this.refControls = ref} key="editorControls" {...this.props} @@ -188,8 +187,6 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat window.clearInterval(this.timeoutScreen); window.clearTimeout(this.timeoutLoading); window.clearTimeout(this.timeoutMove); - - Renderer.remove('commandEditor'); }; initNodes () { @@ -277,7 +274,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat const { isPopup, match } = this.props; let close = true; - if (isPopup && (match.params.id == this.id)) { + if (isPopup && (match?.params?.id == this.id)) { close = false; }; if (keyboard.isCloseDisabled) { @@ -292,11 +289,15 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat }; onCommand (cmd: string, arg: any) { - const { rootId } = this.props; + const { rootId, isPopup } = this.props; const { focused, range } = focus.state; const popupOpen = S.Popup.isOpen('', [ 'page' ]); const menuOpen = this.menuCheck(); + if (isPopup !== keyboard.isPopup()) { + return; + }; + switch (cmd) { case 'selectAll': { if (popupOpen || menuOpen || keyboard.isFocused) { @@ -333,14 +334,14 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat }; focusInit () { - const { rootId, isPopup } = this.props; - const isReadonly = this.isReadonly(); - const storage = Storage.getFocus(rootId); - - if (isReadonly) { + if (this.isReadonly()) { return; }; + const { rootId, isPopup } = this.props; + const storage = Storage.getFocus(rootId); + const root = S.Block.getLeaf(rootId, rootId); + let block = null; let from = 0; let to = 0; @@ -352,7 +353,12 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat }; if (!block) { - block = S.Block.getLeaf(rootId, J.Constant.blockId.title); + if (U.Object.isNoteLayout(root.layout)) { + block = S.Block.getFirstBlock(rootId, -1, it => it.isFocusable()); + } else { + block = S.Block.getLeaf(rootId, J.Constant.blockId.title); + }; + if (block && block.getLength()) { block = null; }; @@ -376,7 +382,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat $(window).off(events.map(it => `${it}.${ns}`).join(' ')); container.off(`scroll.${ns}`); - Renderer.remove('commandEditor'); + Renderer.remove(`commandEditor`); }; rebind () { @@ -416,7 +422,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat win.on(`resize.${ns}`, () => this.resizePage()); container.on(`scroll.${ns}`, e => this.onScroll()); - Renderer.on('commandEditor', (e: any, cmd: string, arg: any) => this.onCommand(cmd, arg)); + Renderer.on(`commandEditor`, (e: any, cmd: string, arg: any) => this.onCommand(cmd, arg)); }; onMouseMove (e: any) { @@ -453,7 +459,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat readonly || keyboard.isResizing || keyboard.isDragging || - (selection && selection.isSelecting) || + selection?.isSelecting() || menuOpen || popupOpen || isLoading @@ -547,7 +553,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat onKeyDownEditor (e: any) { const { rootId, isPopup } = this.props; - if (!isPopup && keyboard.isPopup()) { + if (isPopup !== keyboard.isPopup()) { return; }; @@ -563,6 +569,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat Preview.previewHide(true); const ids = selection.get(I.SelectType.Block); + const idsWithChildren = selection.get(I.SelectType.Block, true); const cmd = keyboard.cmdKey(); const readonly = this.isReadonly(); const styleParam = this.getStyleParam(); @@ -623,15 +630,7 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat ret = true; }); - if (ids.length) { - keyboard.shortcut('escape', e, () => { - if (!menuOpen) { - selection.clear(); - }; - - ret = true; - }); - + if (idsWithChildren.length) { // Mark-up let type = null; @@ -656,18 +655,28 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat data: { filter: '', onChange: (newType: I.MarkType, param: string) => { - C.BlockTextListSetMark(rootId, ids, { type: newType, param, range: { from: 0, to: 0 } }, () => { - analytics.event('ChangeTextStyle', { type: newType, count: ids.length }); + C.BlockTextListSetMark(rootId, idsWithChildren, { type: newType, param, range: { from: 0, to: 0 } }, () => { + analytics.event('ChangeTextStyle', { type: newType, count: idsWithChildren.length }); }); - } - } + }, + }, }); } else { - C.BlockTextListSetMark(rootId, ids, { type, param, range: { from: 0, to: 0 } }, () => { - analytics.event('ChangeTextStyle', { type, count: ids.length }); + C.BlockTextListSetMark(rootId, idsWithChildren, { type, param, range: { from: 0, to: 0 } }, () => { + analytics.event('ChangeTextStyle', { type, count: idsWithChildren.length }); }); }; }; + }; + + if (ids.length) { + keyboard.shortcut('escape', e, () => { + if (!menuOpen) { + selection.clear(); + }; + + ret = true; + }); // Duplicate keyboard.shortcut(`${cmd}+d`, e, () => { @@ -1429,12 +1438,13 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat return; }; + const isEnter = pressed == 'enter'; const isShift = !!pressed.match('shift'); const length = block.getLength(); const parent = S.Block.getParentLeaf(rootId, block.id); - const replace = !range.to && (block.isTextList() || parent?.isTextToggle()) && !length; + const replace = !range.to && block.isTextList() && !length; - if (block.isTextCode() && (pressed == 'enter')) { + if (block.isTextCode() && isEnter) { return; }; if (!block.isText() && keyboard.isFocused) { @@ -1799,6 +1809,12 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat }; onPasteEvent (e: any, props: any, data?: any) { + const { isPopup } = props; + + if (isPopup !== keyboard.isPopup()) { + return; + }; + if (keyboard.isPasteDisabled || this.isReadonly()) { return; }; @@ -1829,17 +1845,19 @@ const EditorPage = observer(class EditorPage extends React.Component<Props, Stat const block = S.Block.getLeaf(rootId, focused); const selection = S.Common.getRef('selectionProvider'); - let url = U.Common.matchUrl(data.text); - let isLocal = false; + if (!data.html) { + let url = U.Common.matchUrl(data.text); + let isLocal = false; - if (!url) { - url = U.Common.matchLocalPath(data.text); - isLocal = true; - }; + if (!url) { + url = U.Common.matchLocalPath(data.text); + isLocal = true; + }; - if (block && url && !block.isTextTitle() && !block.isTextDescription()) { - this.onPasteUrl(url, isLocal); - return; + if (block && url && !block.isTextTitle() && !block.isTextDescription()) { + this.onPasteUrl(url, isLocal); + return; + }; }; let id = ''; diff --git a/src/ts/component/footer/auth/disclaimer.tsx b/src/ts/component/footer/auth/disclaimer.tsx index 857d955863..316daf0c9f 100644 --- a/src/ts/component/footer/auth/disclaimer.tsx +++ b/src/ts/component/footer/auth/disclaimer.tsx @@ -1,13 +1,16 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { Label } from 'Component'; import { I, U, J, translate } from 'Lib'; -class FooterAuthDisclaimer extends React.Component<I.FooterComponent> { - - render () { - return <Label className="disclaimer" text={U.Common.sprintf(translate('authDisclaimer'), J.Url.terms, J.Url.privacy)} />; - }; +const FooterAuthDisclaimer = forwardRef<{}, I.FooterComponent>(() => { -}; + return ( + <Label + className="disclaimer" + text={U.Common.sprintf(translate('authDisclaimer'), J.Url.terms, J.Url.privacy)} + /> + ); -export default FooterAuthDisclaimer; +}); + +export default FooterAuthDisclaimer; \ No newline at end of file diff --git a/src/ts/component/footer/auth/index.tsx b/src/ts/component/footer/auth/index.tsx index ece7c03a4f..78314f7efa 100644 --- a/src/ts/component/footer/auth/index.tsx +++ b/src/ts/component/footer/auth/index.tsx @@ -1,12 +1,12 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { I, U } from 'Lib'; -class FooterAuthIndex extends React.Component<I.FooterComponent> { - - render () { - return <div className="copy">{U.Date.date('Y', U.Date.now())}, Anytype</div>; - }; +const FooterAuthIndex = forwardRef<{}, I.FooterComponent>(() => { -}; + return ( + <div className="copy">{U.Date.date('Y', U.Date.now())}, Anytype</div> + ); + +}); export default FooterAuthIndex; \ No newline at end of file diff --git a/src/ts/component/footer/index.tsx b/src/ts/component/footer/index.tsx index e501e9d63c..48b4872ec3 100644 --- a/src/ts/component/footer/index.tsx +++ b/src/ts/component/footer/index.tsx @@ -1,5 +1,5 @@ -import * as React from 'react'; -import { I, S, sidebar } from 'Lib'; +import React, { forwardRef, useRef } from 'react'; +import { I, S } from 'Lib'; import FooterAuthIndex from './auth'; import FooterAuthDisclaimer from './auth/disclaimer'; @@ -16,42 +16,14 @@ const Components = { mainObject: FooterMainObject, }; -class Footer extends React.Component<Props> { - - refChild: any = null; - - constructor (props: Props) { - super(props); - - this.onHelp = this.onHelp.bind(this); - }; +const Footer = forwardRef<{}, Props>((props, ref) => { - render () { - const { component, className } = this.props; - const Component = Components[component] || null; - const cn = [ 'footer', component, className ]; - - return ( - <div id="footer" className={cn.join(' ')}> - <Component - ref={ref => this.refChild = ref} - {...this.props} - onHelp={this.onHelp} - /> - </div> - ); - }; + const childRef = useRef(null); + const { component, className = '' } = props; + const Component = Components[component] || null; + const cn = [ 'footer', component, className ]; - componentDidMount () { - sidebar.resizePage(null, false); - }; - - componentDidUpdate () { - sidebar.resizePage(null, false); - this.refChild.forceUpdate(); - }; - - onHelp () { + const onHelp = () => { S.Menu.open('help', { element: '#footer #button-help', classNameWrap: 'fixed', @@ -61,6 +33,18 @@ class Footer extends React.Component<Props> { }); }; -}; + return ( + <div id="footer" className={cn.join(' ')}> + {Component ? ( + <Component + ref={childRef} + {...props} + onHelp={onHelp} + /> + ) : ''} + </div> + ); + +}); -export default Footer; +export default Footer; \ No newline at end of file diff --git a/src/ts/component/footer/main/object.tsx b/src/ts/component/footer/main/object.tsx index 88da5db8b2..454d6bfd33 100644 --- a/src/ts/component/footer/main/object.tsx +++ b/src/ts/component/footer/main/object.tsx @@ -1,61 +1,61 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; import { PieChart } from 'react-minimal-pie-chart'; import { Icon } from 'Component'; import { I, J, S, Preview, translate } from 'Lib'; -const FooterMainEdit = observer(class FooterMainEdit extends React.Component<I.FooterComponent> { - - render () { - const { onHelp } = this.props; - const { show } = S.Progress; - const theme = S.Common.getThemeClass(); - const current = S.Progress.getCurrent(); - const total = S.Progress.getTotal(); - const percent = Math.round((current / total) * 100); - const color = J.Theme[theme].progress; +const FooterMainObject = observer(forwardRef<{}, I.FooterComponent>((props, ref) => { - return ( - <div className="buttons"> - {total ? ( - <div - id="button-progress" - className="iconWrap" - onClick={() => S.Progress.showSet(!show)} - > - <div className="inner">{percent}</div> - <PieChart - totalValue={100} - startAngle={270} - lengthAngle={-360} - data={[ - { title: '', value: 100 - percent, color: color.bg }, - { title: '', value: percent, color: color.fg }, - ]} - /> - </div> - ) : ''} - <div - id="button-help" - className="iconWrap" - onClick={onHelp} - onMouseEnter={e => this.onTooltipShow(e, translate('commonHelp'))} - onMouseLeave={() => Preview.tooltipHide(false)} - > - <Icon /> - <div className="bg" /> - </div> - </div> - ); - }; + const { onHelp } = props; + const { show } = S.Progress; + const theme = S.Common.getThemeClass(); + const skipState = [ I.ProgressState.Done, I.ProgressState.Canceled ]; + const skipType = [ I.ProgressType.Migrate ]; + const list = S.Progress.getList(it => !skipType.includes(it.type) && !skipState.includes(it.state)); + const percent = S.Progress.getPercent(list); + const color = J.Theme[theme].progress; - onTooltipShow (e: any, text: string, caption?: string) { + const onTooltipShow = (e: any, text: string, caption?: string) => { const t = Preview.tooltipCaption(text, caption); if (t) { Preview.tooltipShow({ text: t, element: $(e.currentTarget), typeY: I.MenuDirection.Top }); }; }; -}); + return ( + <div className="buttons"> + {percent ? ( + <div + id="button-progress" + className="iconWrap" + onClick={() => S.Progress.showSet(!show)} + > + <div className="inner">{percent}</div> + <PieChart + totalValue={100} + startAngle={270} + lengthAngle={-360} + data={[ + { title: '', value: 100 - percent, color: color.bg }, + { title: '', value: percent, color: color.fg }, + ]} + /> + </div> + ) : ''} + + <div + id="button-help" + className="iconWrap" + onClick={onHelp} + onMouseEnter={e => onTooltipShow(e, translate('commonHelp'))} + onMouseLeave={() => Preview.tooltipHide(false)} + > + <Icon /> + <div className="bg" /> + </div> + </div> + ); + +})); -export default FooterMainEdit; \ No newline at end of file +export default FooterMainObject; \ No newline at end of file diff --git a/src/ts/component/form/drag/horizontal.tsx b/src/ts/component/form/drag/horizontal.tsx index 0c555cb04b..1a7a2ec691 100644 --- a/src/ts/component/form/drag/horizontal.tsx +++ b/src/ts/component/form/drag/horizontal.tsx @@ -1,10 +1,10 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useImperativeHandle, useEffect } from 'react'; import $ from 'jquery'; interface Props { id?: string; - className: string; - value: number; + className?: string; + value?: number; snaps?: number[]; strictSnap?: boolean; onStart?(e: any, v: number): void; @@ -12,172 +12,160 @@ interface Props { onEnd?(e: any, v: number): void; }; -const SNAP = 0.025; +interface DragHorizontalRefProps { + getValue(): number; + setValue(v: number): void; + resize(): void; +}; -class DragHorizontal extends React.Component<Props> { +const SNAP = 0.025; - public static defaultProps = { - value: 0, - min: 0, - max: 1, - className: '', - }; - - value = null; - ox = 0; - nw = 0; - iw = 0; - node: any = null; - back: any = null; - fill: any = null; - icon: any = null; - - constructor (props: Props) { - super(props); - - this.start = this.start.bind(this); +const DragHorizontal = forwardRef<DragHorizontalRefProps, Props>(({ + id = '', + className = '', + value: initalValue = 0, + snaps = [], + strictSnap = false, + onStart, + onMove, + onEnd, +}, ref) => { + let value = initalValue; + + const nodeRef = useRef(null); + const iconRef = useRef(null); + const backRef = useRef(null); + const fillRef = useRef(null); + const cn = [ 'input-drag-horizontal', className ]; + + const checkValue = (v: number): number => { + v = Number(v) || 0; + v = Math.max(0, v); + v = Math.min(1, v); + return v; }; - - render () { - const { id, className } = this.props; - const cn = [ 'input-drag-horizontal' ]; - if (className) { - cn.push(className); - }; - - return ( - <div - ref={node => this.node = node} - id={id} - className={cn.join(' ')} - onMouseDown={this.start} - > - <div id="back" className="back" /> - <div id="fill" className="fill" /> - <div id="icon" className="icon"> - <div className="bullet" /> - </div> - </div> - ); - }; - - componentDidMount () { - const node = $(this.node); - - this.back = node.find('#back'); - this.fill = node.find('#fill'); - this.icon = node.find('#icon'); - this.setValue(this.props.value); + const maxWidth = (): number => { + return $(nodeRef.current).width() - $(iconRef.current).width(); }; - setValue (v: number) { - this.move(this.checkValue(v) * this.maxWidth()); + const setValue = (v: number) => { + move(checkValue(v) * maxWidth()); }; - - getValue () { - return this.checkValue(this.value); + + const getValue = () => { + return checkValue(value); }; - resize () { - this.setValue(this.value); + const resize = () => { + setValue(value); }; - start (e: any) { + const start = (e: any) => { e.preventDefault(); e.stopPropagation(); - const { onStart, onMove, onEnd } = this.props; const win = $(window); - const node = $(this.node); - const iw = this.icon.width(); + const node = $(nodeRef.current); + const icon = $(iconRef.current); + const iw = icon.width(); const ox = node.offset().left; - this.move(e.pageX - ox - iw / 2); + move(e.pageX - ox - iw / 2); node.addClass('isDragging'); if (onStart) { - onStart(e, this.value); + onStart(e, value); }; win.off('mousemove.drag touchmove.drag').on('mousemove.drag touchmove.drag', (e: any) => { - this.move(e.pageX - ox - iw / 2); + move(e.pageX - ox - iw / 2); if (onMove) { - onMove(e, this.value); + onMove(e, value); }; }); win.off('mouseup.drag touchend.drag').on('mouseup.drag touchend.drag', (e: any) => { - this.end(e); + end(e); if (onEnd) { - onEnd(e, this.value); + onEnd(e, value); }; }); }; - - move (x: number) { - const { strictSnap } = this.props; - const snaps = this.props.snaps || []; - const node = $(this.node); + + const move = (x: number) => { + const node = $(nodeRef.current); + const icon = $(iconRef.current); + const back = $(backRef.current); + const fill = $(fillRef.current); const nw = node.width(); - const iw = this.icon.width() / 2; - const ib = parseInt(this.icon.css('border-width')); - const mw = this.maxWidth(); + const iw = icon.width() / 2; + const ib = parseInt(icon.css('border-width')); + const mw = maxWidth(); x = Math.max(0, x); x = Math.min(mw, x); - this.value = this.checkValue(x / mw); + value = checkValue(x / mw); // Snap - if (strictSnap && snaps.length && (this.value < snaps[0] / 2)) { - this.value = 0; + if (strictSnap && snaps.length && (value < snaps[0] / 2)) { + value = 0; } else { const step = 1 / snaps.length; for (const snap of snaps) { const d = strictSnap ? step / 2 : SNAP; - if ((this.value >= snap - d) && (this.value < snap + d)) { - this.value = snap; + if ((value >= snap - d) && (value < snap + d)) { + value = snap; break; }; }; }; - x = this.value * mw; + x = value * mw; const w = Math.min(nw, x + iw); - this.icon.css({ left: x }); - this.back.css({ left: (w + iw + ib), width: (nw - w - iw - ib) }); - this.fill.css({ width: (w - ib) }); + icon.css({ left: x }); + back.css({ left: (w + iw + ib), width: (nw - w - iw - ib) }); + fill.css({ width: (w - ib) }); }; - - end (e) { + + const end = (e) => { e.preventDefault(); e.stopPropagation(); - const win = $(window); - const node = $(this.node); - - win.off('mousemove.drag touchmove.drag mouseup.drag touchend.drag'); - node.removeClass('isDragging'); + $(window).off('mousemove.drag touchmove.drag mouseup.drag touchend.drag'); + $(nodeRef.current).removeClass('isDragging'); }; - maxWidth () { - const node = $(this.node); - return node.width() - this.icon.width(); - }; - - checkValue (v: number): number { - v = Number(v) || 0; - v = Math.max(0, v); - v = Math.min(1, v); - return v; - }; - -}; + useEffect(() => setValue(initalValue), []); + + useImperativeHandle(ref, () => ({ + getValue, + setValue, + resize, + })); + + return ( + <div + ref={nodeRef} + id={id} + className={cn.join(' ')} + onMouseDown={start} + onClick={e => e.stopPropagation()} + > + <div ref={backRef} className="back" /> + <div ref={fillRef} className="fill" /> + <div ref={iconRef} className="icon"> + <div className="bullet" /> + </div> + </div> + ); + +}); export default DragHorizontal; \ No newline at end of file diff --git a/src/ts/component/form/drag/vertical.tsx b/src/ts/component/form/drag/vertical.tsx index c27220ab73..23f1e88161 100644 --- a/src/ts/component/form/drag/vertical.tsx +++ b/src/ts/component/form/drag/vertical.tsx @@ -1,4 +1,5 @@ -import React, { useRef, useImperativeHandle, forwardRef, ChangeEvent, MouseEvent } from 'react'; +import React, { useRef, useImperativeHandle, forwardRef, ChangeEvent, MouseEvent, useEffect } from 'react'; +import $ from 'jquery'; import { Input } from 'Component'; interface Props { @@ -13,31 +14,53 @@ interface Props { onMouseEnter? (e: MouseEvent): void; }; -const DragVertical = forwardRef<HTMLDivElement, Props>(({ +interface DragVerticalRefProps { + getValue: () => number; + setValue: (v: number) => void; +}; + +const DragVertical = forwardRef<DragVerticalRefProps, Props>(({ id, className = '', - value, + value: initialValue = 0, min = 0, max = 1, step = 0.01, onChange, onMouseLeave, onMouseEnter, -}, forwardedRef) => { +}, ref) => { const inputRef = useRef(null); + const trackRef = useRef(null); const divRef = useRef(null); - useImperativeHandle(forwardedRef, () => divRef.current); + const setHeight = (v: number) => { + $(trackRef.current).css({ height: `${Math.round(v * 72)}px` }); + }; const handleChange = (e: ChangeEvent<HTMLInputElement>, value: string) => { + const v = 1 - Number(value) || 0; + e.preventDefault(); e.stopPropagation(); + setHeight(v); + if (onChange) { - onChange(e, 1 - Number(value) || 0); + onChange(e, v); }; }; + useImperativeHandle(ref, () => ({ + getValue: () => inputRef.current?.getValue(), + setValue: (v: number) => inputRef.current?.setValue(v), + })); + + useEffect(() => { + setHeight(initialValue); + inputRef.current.setValue(initialValue); + }, []); + return ( <div id={id} @@ -50,7 +73,7 @@ const DragVertical = forwardRef<HTMLDivElement, Props>(({ type="range" className="vertical-range" ref={inputRef} - value={String(value)} + value={String(initialValue)} min={min} max={max} step={step} @@ -61,7 +84,10 @@ const DragVertical = forwardRef<HTMLDivElement, Props>(({ }} /> <div className="slider-bg" /> - <div className="slider-track" style={{ height: `${Math.round(value * 72)}px` }} /> + <div + ref={trackRef} + className="slider-track" + /> </div> ); }); diff --git a/src/ts/component/form/editable.tsx b/src/ts/component/form/editable.tsx index c8c029cf70..51b88efff0 100644 --- a/src/ts/component/form/editable.tsx +++ b/src/ts/component/form/editable.tsx @@ -1,5 +1,4 @@ -import * as React from 'react'; -import raf from 'raf'; +import React, { forwardRef, useRef, useImperativeHandle } from 'react'; import { getRange, setRange } from 'selection-ranges'; import { I, U, keyboard, Mark } from 'Lib'; @@ -26,182 +25,118 @@ interface Props { onCompositionEnd?: (e: any) => void; }; -class Editable extends React.Component<Props> { - - _isMounted = false; - node: any = null; - refPlaceholder = null; - refEditable = null; - - constructor (props: Props) { - super(props); - - this.onInput = this.onInput.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onPaste = this.onPaste.bind(this); - this.onCompositionStart = this.onCompositionStart.bind(this); - this.onCompositionEnd = this.onCompositionEnd.bind(this); - }; - - render () { - const { id, classNameWrap, classNameEditor, classNamePlaceholder, readonly, placeholder, spellcheck, onSelect, onMouseDown, onMouseUp, onDragStart } = this.props; - const cnw = [ 'editableWrap' ]; - const cne = [ 'editable' ]; - const cnp = [ 'placeholder' ]; - - if (classNameWrap) { - cnw.push(classNameWrap); - }; - - if (classNameEditor) { - cne.push(classNameEditor); - }; - - if (classNamePlaceholder) { - cnp.push(classNamePlaceholder); - }; - - let editor = null; - if (readonly) { - cne.push('isReadonly'); - - editor = ( - <div - id={id} - ref={ref => this.refEditable = ref} - className={cne.join(' ')} - contentEditable={true} - suppressContentEditableWarning={true} - spellCheck={false} - onMouseUp={onSelect} - /> - ); - } else { - editor = ( - <div - id={id} - ref={ref => this.refEditable = ref} - className={cne.join(' ')} - contentEditable={true} - suppressContentEditableWarning={true} - spellCheck={spellcheck} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onFocus={this.onFocus} - onBlur={this.onBlur} - onSelect={onSelect} - onPaste={this.onPaste} - onMouseUp={onMouseUp} - onInput={this.onInput} - onDragStart={onDragStart} - onCompositionStart={this.onCompositionStart} - onCompositionEnd={this.onCompositionEnd} - /> - ); - }; - - return ( - <div - ref={node => this.node = node} - className={cnw.join(' ')} - onMouseDown={onMouseDown} - > - {editor} - <div - id="placeholder" - className={cnp.join(' ')} - ref={ref => this.refPlaceholder = ref} - > - {placeholder} - </div> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentWillUnmount () { - this._isMounted = false; - }; +interface EditableRefProps { + placeholderCheck: () => void; + placeholderSet: (v: string) => void; + placeholderHide: () => void; + placeholderShow: () => void; + setValue: (html: string) => void; + getTextValue: () => string; + getHtmlValue: () => string; + getRange: () => I.TextRange; + setRange: (range: I.TextRange) => void; + getNode: () => JQuery; +}; - placeholderCheck () { - this.getTextValue() ? this.placeholderHide() : this.placeholderShow(); +const Editable = forwardRef<EditableRefProps, Props>(({ + id = '', + classNameWrap = '', + classNameEditor = '', + classNamePlaceholder = '', + readonly = false, + placeholder = '', + spellcheck = false, + maxLength, + onSelect, + onMouseDown, + onMouseUp, + onDragStart, + onPaste, + onInput, + onKeyDown, + onKeyUp, + onFocus, + onBlur, + onCompositionStart, + onCompositionEnd, +}, ref) => { + + const nodeRef = useRef(null); + const placeholderRef = useRef(null); + const editableRef = useRef(null); + const cnw = [ 'editableWrap', classNameWrap ]; + const cne = [ 'editable', classNameEditor ]; + const cnp = [ 'placeholder', classNamePlaceholder ]; + + const placeholderCheck = () => { + getTextValue() ? placeholderHide() : placeholderShow(); }; - placeholderSet (v: string) { - $(this.refPlaceholder).text(v); + const placeholderSet = (v: string) => { + $(placeholderRef.current).text(v); }; - placeholderHide () { - $(this.refPlaceholder).hide(); + const placeholderHide = () => { + $(placeholderRef.current).hide(); }; - placeholderShow () { - $(this.refPlaceholder).show(); + const placeholderShow = () => { + $(placeholderRef.current).show(); }; - setValue (html: string) { - $(this.refEditable).get(0).innerHTML = U.Common.sanitize(html); + const setValue = (html: string) => { + $(editableRef.current).get(0).innerHTML = U.Common.sanitize(html); }; - getTextValue (): string { - const obj = Mark.cleanHtml($(this.refEditable).html()); + const getTextValue = (): string => { + const obj = Mark.cleanHtml($(editableRef.current).html()); return String(obj.get(0).innerText || ''); }; - getHtmlValue () : string { - return String($(this.refEditable).html() || ''); + const getHtmlValue = (): string => { + return String($(editableRef.current).html() || ''); }; - getRange (): I.TextRange { - const range = getRange($(this.refEditable).get(0) as Element); + const getRangeHandler = (): I.TextRange => { + const range = getRange($(editableRef.current).get(0) as Element); return range ? { from: range.start, to: range.end } : null; }; - setRange (range: I.TextRange) { - if (!range || !this._isMounted) { + const setRangeHandler = (range: I.TextRange) => { + if (!range) { return; }; - const el = $(this.refEditable).get(0); + const el = $(editableRef.current).get(0); el.focus({ preventScroll: true }); setRange(el, { start: range.from, end: range.to }); }; - onPaste (e: any) { - const { onPaste } = this.props; + const onPasteHandler = (e: any) => { + placeholderCheck(); if (onPaste) { onPaste(e); }; }; - onInput (e: any) { - const { onInput } = this.props; - - this.placeholderCheck(); + const onInputHandler = (e: any) => { + placeholderCheck(); if (onInput) { onInput(e); }; }; - onKeyDown (e: any): void { + const onKeyDownHandler = (e: any): void => { // Chinese IME is open if (keyboard.isComposition) { return; }; - const { maxLength, onKeyDown } = this.props; - if (maxLength) { - const text = this.getTextValue(); + const text = getTextValue(); if ((text.length >= maxLength) && !keyboard.isSpecial(e) && !keyboard.withCommand(e)) { e.preventDefault(); @@ -213,49 +148,118 @@ class Editable extends React.Component<Props> { }; }; - onKeyUp (e: any): void { + const onKeyUpHandler = (e: any): void => { // Chinese IME is open if (keyboard.isComposition) { return; }; - if (this.props.onKeyUp) { - this.props.onKeyUp(e); + if (onKeyUp) { + onKeyUp(e); }; }; - onFocus (e: any) { + const onFocusHandler = (e: any) => { keyboard.setFocus(true); - if (this.props.onFocus) { - this.props.onFocus(e); + if (onFocus) { + onFocus(e); }; }; - onBlur (e: any) { + const onBlurHandler = (e: any) => { keyboard.setFocus(false); - if (this.props.onBlur) { - this.props.onBlur(e); + if (onBlur) { + onBlur(e); }; }; - onCompositionStart (e: any) { + const onCompositionStartHandler = (e: any) => { keyboard.setComposition(true); - if (this.props.onCompositionStart) { - this.props.onCompositionStart(e); + if (onCompositionStart) { + onCompositionStart(e); }; }; - onCompositionEnd (e: any) { + const onCompositionEndHandler = (e: any) => { keyboard.setComposition(false); - if (this.props.onCompositionEnd) { - this.props.onCompositionEnd(e); + if (onCompositionEnd) { + onCompositionEnd(e); }; }; -}; + let editor = null; + if (readonly) { + cne.push('isReadonly'); + + editor = ( + <div + id={id} + ref={editableRef} + className={cne.join(' ')} + contentEditable={true} + suppressContentEditableWarning={true} + spellCheck={false} + onMouseUp={onSelect} + /> + ); + } else { + editor = ( + <div + id={id} + ref={editableRef} + className={cne.join(' ')} + contentEditable={true} + suppressContentEditableWarning={true} + spellCheck={spellcheck} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUpHandler} + onFocus={onFocusHandler} + onBlur={onBlurHandler} + onSelect={onSelect} + onPaste={onPasteHandler} + onMouseUp={onMouseUp} + onInput={onInputHandler} + onDragStart={onDragStart} + onCompositionStart={onCompositionStartHandler} + onCompositionEnd={onCompositionEndHandler} + /> + ); + }; + + useImperativeHandle(ref, () => ({ + placeholderCheck, + placeholderSet, + placeholderHide, + placeholderShow, + setValue, + getTextValue, + getHtmlValue, + getRange: getRangeHandler, + setRange: setRangeHandler, + getNode: () => $(nodeRef.current), + }), []); + + return ( + <div + ref={nodeRef} + className={cnw.join(' ')} + onMouseDown={onMouseDown} + > + {editor} + <div + id="placeholder" + className={cnp.join(' ')} + ref={placeholderRef} + > + {placeholder} + </div> + </div> + ); + +}); export default Editable; diff --git a/src/ts/component/form/emailCollection.tsx b/src/ts/component/form/emailCollection.tsx new file mode 100644 index 0000000000..6eeef03651 --- /dev/null +++ b/src/ts/component/form/emailCollection.tsx @@ -0,0 +1,292 @@ +import React, { FC, useState, useRef, useEffect } from 'react'; +import { Label, Checkbox, Input, Button, Icon, Pin } from 'Component'; +import { analytics, C, J, S, translate, U } from 'Lib'; + +interface Props { + onStepChange: () => void; + onComplete: () => void; +}; + +const EmailCollection: FC<Props> = ({ + onStepChange, + onComplete, +}) => { + + const checkboxTipsRef = useRef(null); + const checkboxNewsRef = useRef(null); + const emailRef = useRef(null); + const buttonRef = useRef(null); + const codeRef = useRef(null); + const interval = useRef(0); + const timeout = useRef(0); + const [ step, setStep ] = useState(0); + const [ status, setStatus ] = useState(''); + const [ statusText, setStatusText ] = useState(''); + const [ countdown, setCountdown ] = useState(0); + const [ email, setEmail ] = useState(''); + const [ subscribeNews, setSubscribeNews ] = useState(false); + const [ subscribeTips, setSubscribeTips ] = useState(false); + const [ pinDisabled, setPinDisabled ] = useState(false); + const [ showCodeSent, setShowCodeSent ] = useState(false); + + let content = null; + let descriptionSuffix = 'Description'; + + const onCheck = (ref, type: string) => { + if (!ref.current) { + return; + }; + + const val = ref.current.getValue(); + + ref.current.toggle(); + + if (!val) { + analytics.event('ClickEmailCollection', { route: 'OnboardingTooltip', step: 1, type }); + }; + }; + + const setStepHandler = (newStep: number) => { + if (step == newStep) { + return; + }; + + setStep(newStep); + onStepChange(); + analytics.event('EmailCollection', { route: 'OnboardingTooltip', step: newStep }); + }; + + const setStatusAndText = (status: string, statusText: string) => { + setStatus(status); + setStatusText(statusText); + }; + + const clearStatus = () => { + setStatusAndText('', ''); + }; + + const validateEmail = () => { + clearStatus(); + + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => { + const value = emailRef.current?.getValue(); + const isValid = U.Common.checkEmail(value); + + if (value && !isValid) { + setStatusAndText('error', translate('errorIncorrectEmail')); + }; + + buttonRef.current?.setDisabled(!isValid); + }, J.Constant.delay.keyboard); + }; + + const onSubmitEmail = (e: any) => { + e.preventDefault(); + + if (!buttonRef.current || !emailRef.current) { + return; + }; + + if (buttonRef.current.isDisabled()) { + return; + }; + + analytics.event('ClickEmailCollection', { route: 'OnboardingTooltip', step: 1, type: 'SignUp' }); + + setEmail(emailRef.current?.getValue()); + setSubscribeNews(checkboxNewsRef.current?.getValue()); + setSubscribeTips(checkboxTipsRef.current?.getValue()); + }; + + const verifyEmail = () => { + buttonRef.current?.setLoading(true); + + C.MembershipGetVerificationEmail(email, subscribeNews, subscribeTips, true, (message) => { + buttonRef.current?.setLoading(false); + + if (message.error.code) { + setStatusAndText('error', message.error.description); + return; + }; + + setStepHandler(1); + startCountdown(60); + }); + }; + + const onConfirmEmailCode = () => { + const code = codeRef.current?.getValue(); + + setPinDisabled(true); + + C.MembershipVerifyEmailCode(code, (message) => { + if (message.error.code) { + setStatusAndText('error', message.error.description); + setPinDisabled(false); + codeRef.current?.reset(); + return; + }; + + setStepHandler(2); + }); + }; + + const onResend = (e: any) => { + e.preventDefault(); + + if (countdown) { + return; + }; + + verifyEmail(); + analytics.event('ClickEmailCollection', { route: 'OnboardingTooltip', step: 2, type: 'Resend' }); + }; + + const startCountdown = (s: number) => { + const { emailConfirmationTime } = S.Common; + + if (!emailConfirmationTime) { + S.Common.emailConfirmationTimeSet(U.Date.now()); + }; + + setCountdown(s); + setShowCodeSent(true); + window.setTimeout(() => setShowCodeSent(false), 2000); + }; + + const tick = () => { + window.clearTimeout(interval.current); + interval.current = window.setTimeout(() => setCountdown(countdown => countdown - 1), 1000); + }; + + const clear = () => { + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => clearStatus(), 4000); + }; + + useEffect(() => { + buttonRef.current?.setDisabled(true); + analytics.event('EmailCollection', { route: 'OnboardingTooltip', step: 1 }); + + return () => { + window.clearTimeout(timeout.current); + window.clearTimeout(interval.current); + }; + }); + + useEffect(() => { + if (interval.current) { + tick(); + }; + }, [ showCodeSent ]); + + useEffect(() => { + if (status || statusText) { + clear(); + }; + }, [ status, statusText ]); + + useEffect(() => { + if (timeout.current) { + clear(); + }; + }, [ pinDisabled ]); + + useEffect(() => { + if (email) { + verifyEmail(); + }; + }, [ email, subscribeNews, subscribeTips ]); + + useEffect(() => { + if (countdown) { + tick(); + } else { + window.clearTimeout(interval.current); + S.Common.emailConfirmationTimeSet(0); + }; + }, [ countdown ]); + + switch (step) { + case 0: { + content = ( + <div className="step step0"> + <form onSubmit={onSubmitEmail}> + <div className="check" onClick={() => onCheck(checkboxTipsRef, 'Tips')}> + <Checkbox ref={checkboxTipsRef} value={false} /> {translate('emailCollectionCheckboxTipsLabel')} + </div> + <div className="check" onClick={() => onCheck(checkboxNewsRef, 'Updates')}> + <Checkbox ref={checkboxNewsRef} value={false} /> {translate('emailCollectionCheckboxNewsLabel')} + </div> + + <div className="inputWrapper"> + <Input ref={emailRef} onKeyUp={validateEmail} placeholder={translate(`emailCollectionEnterEmail`)} /> + </div> + + {status ? <div className={[ 'statusBar', status ].join(' ')}>{statusText}</div> : ''} + + <div className="buttonWrapper"> + <Button ref={buttonRef} onClick={onSubmitEmail} className="c36" text={translate('commonSignUp')} /> + </div> + </form> + </div> + ); + break; + }; + + case 1: { + content = ( + <div className="step step1"> + <Pin + ref={codeRef} + pinLength={4} + isVisible={true} + onSuccess={onConfirmEmailCode} + readonly={pinDisabled} + /> + + {status ? <div className={[ 'statusBar', status ].join(' ')}>{statusText}</div> : ''} + + <div onClick={onResend} className={[ 'resend', (countdown ? 'countdown' : '') ].join(' ')}> + {showCodeSent ? translate('emailCollectionCodeSent') : translate('popupMembershipResend')} + {countdown && !showCodeSent ? U.Common.sprintf(translate('popupMembershipCountdown'), countdown) : ''} + </div> + </div> + ); + break; + }; + + case 2: { + descriptionSuffix = 'News'; + if (subscribeTips) { + descriptionSuffix = 'Tips'; + }; + if (subscribeTips && subscribeNews) { + descriptionSuffix = 'NewsAndTips'; + }; + + content = ( + <div className="step step2"> + <Icon /> + + <div className="buttonWrapper"> + <Button onClick={onComplete} className="c36" text={translate('emailCollectionGreat')} /> + </div> + </div> + ); + break; + }; + }; + + return ( + <div className="emailCollectionForm"> + <Label className="category" text={translate(`emailCollectionStep${step}Title`)} /> + <Label className="descr" text={translate(`emailCollectionStep${step}${descriptionSuffix}`)} /> + + {content} + </div> + ); +}; + +export default EmailCollection; \ No newline at end of file diff --git a/src/ts/component/form/filter.tsx b/src/ts/component/form/filter.tsx index 122ff11016..b3b617dead 100644 --- a/src/ts/component/form/filter.tsx +++ b/src/ts/component/form/filter.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useImperativeHandle, useEffect, useState, useRef } from 'react'; import $ from 'jquery'; import { Input, Icon } from 'Component'; import { I, keyboard, translate } from 'Lib'; @@ -27,134 +27,81 @@ interface Props { onIconClick?(e: any): void; }; -interface State { - isActive: boolean; +interface FilterRefProps { + focus(): void; + blur(): void; + setActive(v: boolean): void; + setValue(v: string): void; + getValue(): string; + getRange(): I.TextRange; + setRange(range: I.TextRange): void; }; -class Filter extends React.Component<Props, State> { - - public static defaultProps = { - className: '', - inputClassName: '', - tooltipY: I.MenuDirection.Bottom, +const Filter = forwardRef<FilterRefProps, Props>(({ + id = '', + className = '', + inputClassName = '', + icon = '', + value = '', + placeholder = translate('commonFilterClick'), + placeholderFocus = '', + tooltip = '', + tooltipCaption = '', + tooltipX = I.MenuDirection.Center, + tooltipY = I.MenuDirection.Bottom, + focusOnMount = false, + onClick, + onFocus, + onBlur, + onKeyDown, + onKeyUp, + onChange, + onSelect, + onClear, + onIconClick, +}, ref) => { + const nodeRef = useRef(null); + const inputRef = useRef(null); + const placeholderRef = useRef(null); + const [ isFocused, setIsFocused ] = useState(false); + const [ isActive, setIsActive ] = useState(false); + const cn = [ 'filter', className ]; + + if (isFocused) { + cn.push('isFocused'); }; - state = { - isActive: false, - }; - - node: any = null; - isFocused = false; - placeholder: any = null; - ref = null; - - constructor (props: Props) { - super(props); - - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onChange = this.onChange.bind(this); - this.onClear = this.onClear.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onInput = this.onInput.bind(this); + if (isActive) { + cn.push('isActive'); }; - - render () { - const { isActive } = this.state; - const { id, value, icon, tooltip, tooltipCaption, tooltipX, tooltipY, placeholder = translate('commonFilterClick'), className, inputClassName, focusOnMount, onKeyDown, onKeyUp, onClick, onIconClick } = this.props; - const cn = [ 'filter' ]; - - if (className) { - cn.push(className); - }; - if (isActive) { - cn.push('isActive'); - }; - - let iconObj = null; - if (icon) { - iconObj = ( - <Icon - className={icon} - tooltip={tooltip} - tooltipCaption={tooltipCaption} - tooltipX={tooltipX} - tooltipY={tooltipY} - onClick={onIconClick} - /> - ); - }; - - return ( - <div - ref={node => this.node = node} - id={id} - className={cn.join(' ')} - onClick={onClick} - > - <div className="inner"> - {iconObj} - - <div className="filterInputWrap"> - <Input - ref={ref => this.ref = ref} - id="input" - className={inputClassName} - value={value} - focusOnMount={focusOnMount} - onFocus={this.onFocus} - onBlur={this.onBlur} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onInput={() => this.placeholderCheck()} - onCompositionStart={() => this.placeholderCheck()} - onCompositionEnd={() => this.placeholderCheck()} - /> - <div id="placeholder" className="placeholder">{placeholder}</div> - </div> - - <Icon className="clear" onClick={this.onClear} /> - </div> - <div className="line" /> - </div> + let iconObj = null; + if (icon) { + iconObj = ( + <Icon + className={icon} + tooltip={tooltip} + tooltipCaption={tooltipCaption} + tooltipX={tooltipX} + tooltipY={tooltipY} + onClick={onIconClick} + /> ); }; - componentDidMount() { - const node = $(this.node); - - this.ref.setValue(this.props.value); - this.placeholder = node.find('#placeholder'); - - this.checkButton(); - this.resize(); - }; - - componentDidUpdate () { - this.checkButton(); - this.resize(); - }; - - focus () { - this.ref.focus(); - this.checkButton(); + const focus = () => { + inputRef.current.focus(); }; - blur () { - this.ref.blur(); + const blur = () => { + inputRef.current.blur(); }; - onFocus (e: any) { - const { placeholderFocus, onFocus } = this.props; - - this.isFocused = true; - this.addFocusedClass(); + const onFocusHandler = (e: any) => { + setIsFocused(true); if (placeholderFocus) { - this.placeholderSet(placeholderFocus); + placeholderSet(placeholderFocus); }; if (onFocus) { @@ -162,14 +109,11 @@ class Filter extends React.Component<Props, State> { }; }; - onBlur (e: any) { - const { placeholderFocus, placeholder, onBlur } = this.props; - - this.isFocused = false; - this.removeFocusedClass(); + const onBlurHandler = (e: any) => { + setIsFocused(false); if (placeholderFocus) { - this.placeholderSet(placeholder); + placeholderSet(placeholder); }; if (onBlur) { @@ -177,122 +121,161 @@ class Filter extends React.Component<Props, State> { }; }; - onInput () { - this.placeholderCheck(); - }; - - addFocusedClass () { - this.addClass('isFocused'); - }; - - removeFocusedClass () { - this.removeClass('isFocused'); - }; - - addClass (c: string) { - $(this.node).addClass(c); - }; - - removeClass (c: string) { - $(this.node).removeClass(c); - }; - - setActive (v: boolean) { - this.setState({ isActive: v }); + const onSelectHandler = (e: any) => { + if (onSelect) { + onSelect(e); + }; }; - onClear (e: any) { + const onClearHandler = (e: any) => { e.preventDefault(); e.stopPropagation(); - const { onClear } = this.props; - - this.ref.setValue(''); - this.ref.focus(); - this.onChange(e, ''); - + inputRef.current.setValue(''); + inputRef.current.focus(); + + onChangeHandler(e, ''); if (onClear) { onClear(); }; }; - onChange (e: any, v: string) { + const onChangeHandler = (e: any, v: string) => { // Chinese IME is open if (keyboard.isComposition) { return; }; - this.checkButton(); + resize(); - if (this.props.onChange) { - this.props.onChange(v); + if (onChange) { + onChange(v); }; }; - onKeyDown (e: any, v: string): void { + const onKeyDownHandler = (e: any, v: string): void => { // Chinese IME is open if (keyboard.isComposition) { return; }; - if (this.props.onKeyDown) { - this.props.onKeyDown(e, v); + buttonCheck(); + + if (onKeyDown) { + onKeyDown(e, v); }; }; - onKeyUp (e: any, v: string): void { + const onKeyUpHandler = (e: any, v: string): void => { // Chinese IME is open if (keyboard.isComposition) { return; }; - if (this.props.onKeyUp) { - this.props.onKeyUp(e, v); + buttonCheck(); + + if (onKeyUp) { + onKeyUp(e, v); }; }; - checkButton () { - $(this.node).toggleClass('active', !!this.getValue()); - this.placeholderCheck(); + const buttonCheck = () => { + $(nodeRef.current).toggleClass('active', Boolean(getValue())); + placeholderCheck(); }; - setValue (v: string) { - this.ref.setValue(v); - this.checkButton(); + const getValue = () => { + return inputRef.current?.getValue(); }; - getValue () { - return this.ref.getValue(); + const getRange = (): I.TextRange => { + return inputRef.current.getRange(); }; - getRange () { - return this.ref.getRange(); + const setRange = (range: I.TextRange) => { + inputRef.current.setRange(range); }; - setRange (range: I.TextRange) { - this.ref.setRange(range); + const placeholderCheck = () => { + getValue() ? placeholderHide() : placeholderShow(); }; - placeholderCheck () { - this.getValue() ? this.placeholderHide() : this.placeholderShow(); + const placeholderSet = (v: string) => { + $(placeholderRef.current).text(v); + }; + + const placeholderHide = () => { + $(placeholderRef.current).hide(); }; - placeholderSet (v: string) { - this.placeholder.text(v); + const placeholderShow = () => { + $(placeholderRef.current).show(); }; - - placeholderHide () { - this.placeholder.hide(); + + const resize = () => { + const ref = $(placeholderRef.current); + ref.css({ lineHeight: ref.height() + 'px' }); }; - placeholderShow () { - this.placeholder.show(); + const init = () => { + buttonCheck(); + resize(); }; - resize () { - this.placeholder.css({ lineHeight: this.placeholder.height() + 'px' }); + useEffect(() => init()); + + useImperativeHandle(ref, () => ({ + focus, + blur, + setActive: v => setIsActive(v), + isFocused: () => isFocused, + setValue: (v: string) => inputRef.current.setValue(v), + getValue, + getRange, + setRange, + })); + + const val = getValue(); + + if (val) { + cn.push('active'); }; -}; + return ( + <div + ref={nodeRef} + id={id} + className={cn.join(' ')} + onClick={onClick} + > + <div className="inner"> + {iconObj} + + <div className="filterInputWrap"> + <Input + ref={inputRef} + id="input" + className={inputClassName} + value={value} + focusOnMount={focusOnMount} + onFocus={onFocusHandler} + onBlur={onBlurHandler} + onChange={onChangeHandler} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUpHandler} + onSelect={onSelectHandler} + onInput={() => placeholderCheck()} + onCompositionStart={() => placeholderCheck()} + onCompositionEnd={() => placeholderCheck()} + /> + <div ref={placeholderRef} className="placeholder">{placeholder}</div> + </div> + + <Icon className="clear" onClick={onClearHandler} /> + </div> + <div className="line" /> + </div> + ); +}); export default Filter; \ No newline at end of file diff --git a/src/ts/component/form/input.tsx b/src/ts/component/form/input.tsx index e6147b897a..3aaf0658bb 100644 --- a/src/ts/component/form/input.tsx +++ b/src/ts/component/form/input.tsx @@ -1,4 +1,6 @@ -import React, { FC, useEffect, useRef, useState, forwardRef, useImperativeHandle } from 'react'; +import React, { + useEffect, useRef, useState, forwardRef, useImperativeHandle, ChangeEvent, SyntheticEvent, KeyboardEvent, FormEvent, FocusEvent, ClipboardEvent +} from 'react'; import $ from 'jquery'; import Inputmask from 'inputmask'; import { I, keyboard } from 'Lib'; @@ -100,84 +102,46 @@ const Input = forwardRef<InputRef, Props>(({ }; const focus = () => { - inputRef.current?.focus({ preventScroll: true }) + inputRef.current?.focus({ preventScroll: true }); }; - useEffect(() => { - if (maskOptions && inputRef.current) { - new Inputmask(maskOptions.mask, maskOptions).mask($(inputRef.current).get(0)); - }; - - if (focusOnMount && inputRef.current) { - focus(); - }; - - return () => { - if (isFocused.current) { - keyboard.setFocus(false); - keyboard.disableSelection(false); - }; - }; - }, [ maskOptions, focusOnMount ]); - - useImperativeHandle(ref, () => ({ - focus, - blur: () => inputRef.current?.blur(), - select: () => inputRef.current?.select(), - setValue: (v: string) => setValue(String(v || '')), - getValue: () => String(value || ''), - setType: (v: string) => setInputType(v), - setError: (hasError: boolean) => $(inputRef.current).toggleClass('withError', hasError), - getSelectionRect, - setPlaceholder: (placeholder: string) => $(inputRef.current).attr({ placeholder }), - setRange: (range: I.TextRange) => { - callWithTimeout(() => { - focus(); - inputRef.current?.setSelectionRange(range.from, range.to); - }); - }, - getRange: (): I.TextRange | null => rangeRef.current, - })); - const handleEvent = ( handler: ((e: any, value: string) => void) | undefined, - e: React.SyntheticEvent<HTMLInputElement> + e: SyntheticEvent<HTMLInputElement> ) => { - let val = null; - if (e.currentTarget) { - val = e.currentTarget.value; - } else - if (e.target) { - val = String($(e.target).val()); - }; - - if (val === null) { - console.log('[Input Event] No value to handle!'); - return; - }; - handler?.(e, val); + handler?.(e, String($(e.target || e.currentTarget).val() || '')); }; - const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const handleChange = (e: ChangeEvent<HTMLInputElement>) => { setValue(e.target.value); handleEvent(onChange, e); }; - const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => { - if ($(inputRef.current).hasClass('disabled')) return; + const handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => { + if ($(inputRef.current).hasClass('disabled')) { + return; + }; + handleEvent(onKeyUp, e); }; - const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { - if ($(inputRef.current).hasClass('disabled')) return; + const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => { + if ($(inputRef.current).hasClass('disabled')) { + return; + }; + handleEvent(onKeyDown, e); }; - const handleInput = (e: React.FormEvent<HTMLInputElement>) => { + const handleInput = (e: FormEvent<HTMLInputElement>) => { handleEvent(onInput, e); }; - const handleFocus = (e: React.FocusEvent<HTMLInputElement>) => { + const handleFocus = (e: FocusEvent<HTMLInputElement>) => { + if (readonly) { + return; + }; + isFocused.current = true; addClass('isFocused'); keyboard.setFocus(true); @@ -185,7 +149,11 @@ const Input = forwardRef<InputRef, Props>(({ handleEvent(onFocus, e); }; - const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => { + const handleBlur = (e: FocusEvent<HTMLInputElement>) => { + if (readonly) { + return; + }; + isFocused.current = false; removeClass('isFocused'); keyboard.setFocus(false); @@ -193,7 +161,7 @@ const Input = forwardRef<InputRef, Props>(({ handleEvent(onBlur, e); }; - const handlePaste = (e: React.ClipboardEvent<HTMLInputElement>) => { + const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => { e.persist(); callWithTimeout(() => { updateRange(e); @@ -201,7 +169,7 @@ const Input = forwardRef<InputRef, Props>(({ }); }; - const handleCut = (e: React.ClipboardEvent<HTMLInputElement>) => { + const handleCut = (e: ClipboardEvent<HTMLInputElement>) => { e.persist(); callWithTimeout(() => { updateRange(e); @@ -209,7 +177,7 @@ const Input = forwardRef<InputRef, Props>(({ }); }; - const handleSelect = (e: React.SyntheticEvent<HTMLInputElement>) => { + const handleSelect = (e: SyntheticEvent<HTMLInputElement>) => { updateRange(e); handleEvent(onSelect, e); }; @@ -219,9 +187,11 @@ const Input = forwardRef<InputRef, Props>(({ onCompositionStart?.(); }; - const handleCompositionEnd = () => { + const handleCompositionEnd = (e) => { keyboard.setComposition(false); onCompositionEnd?.(); + + handleChange(e); }; const addClass = (className: string) => { @@ -277,6 +247,46 @@ const Input = forwardRef<InputRef, Props>(({ return rect; }; + useEffect(() => setValue(initialValue), []); + + useEffect(() => { + if (maskOptions && inputRef.current) { + new Inputmask(maskOptions.mask, maskOptions).mask($(inputRef.current).get(0)); + }; + + if (focusOnMount && inputRef.current) { + focus(); + }; + + return () => { + if (isFocused.current) { + keyboard.setFocus(false); + keyboard.disableSelection(false); + }; + }; + }, [ maskOptions, focusOnMount ]); + + useEffect(() => onChange?.($.Event('change'), value), [ value ]); + + useImperativeHandle(ref, () => ({ + focus, + blur: () => inputRef.current?.blur(), + select: () => inputRef.current?.select(), + setValue: (v: string) => setValue(String(v || '')), + getValue: () => String(value || ''), + setType: (v: string) => setInputType(v), + setError: (hasError: boolean) => $(inputRef.current).toggleClass('withError', hasError), + getSelectionRect, + setPlaceholder: (placeholder: string) => $(inputRef.current).attr({ placeholder }), + setRange: (range: I.TextRange) => { + callWithTimeout(() => { + focus(); + inputRef.current?.setSelectionRange(range.from, range.to); + }); + }, + getRange: (): I.TextRange | null => rangeRef.current, + })); + return ( <input ref={inputRef} diff --git a/src/ts/component/form/inputWithFile.tsx b/src/ts/component/form/inputWithFile.tsx index bc13e48123..70a70ed1b9 100644 --- a/src/ts/component/form/inputWithFile.tsx +++ b/src/ts/component/form/inputWithFile.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC, useRef, useState, useEffect } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { Icon, Input, Button } from 'Component'; @@ -17,229 +17,126 @@ interface Props { onChangeFile? (e: any, path: string): void; }; -interface State { - focused: boolean; - size: Size; -}; - const SMALL_WIDTH = 248; const ICON_WIDTH = 60; enum Size { Icon = 0, Small = 1, Full = 2 }; -class InputWithFile extends React.Component<Props, State> { - - public static defaultProps = { - withFile: true, - canResize: true, - }; - - _isMounted = false; - node: any = null; - state = { - focused: false, - size: Size.Full, - }; - t = 0; - refUrl: any = null; - - constructor (props: Props) { - super(props); - - this.onSubmit = this.onSubmit.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onClickFile = this.onClickFile.bind(this); +const InputWithFile: FC<Props> = ({ + icon = '', + textUrl = translate('inputWithFileTextUrl'), + textFile = '', + withFile = true, + accept, + block, + readonly = false, + canResize = true, + onChangeUrl, + onChangeFile, +}) => { + + const [ isFocused, setIsFocused ] = useState(false); + const [ size, setSize ] = useState(Size.Full); + const nodeRef = useRef(null); + const urlRef = useRef(null); + const timeout = useRef(0); + const cn = [ 'inputWithFile', 'resizable' ]; + const or = ` ${translate('commonOr')} `; + const isSmall = size == Size.Small; + const isIcon = size == Size.Icon; + + let placeholder = textUrl; + let onClick = null; + + if (!withFile) { + cn.push('noFile'); }; - render () { - const { focused, size } = this.state; - const { icon, textUrl = translate('inputWithFileTextUrl'), textFile, withFile, readonly } = this.props; - const cn = [ 'inputWithFile', 'resizable' ]; - const or = ` ${translate('commonOr')} `; - const onBlur = focused ? this.onBlur : null; - const onFocus = !focused ? this.onFocus : null; - const isSmall = size == Size.Small; - const isIcon = size == Size.Icon; - - let placeholder = textUrl; - let onClick = null; - - if (!withFile) { - cn.push('noFile'); - }; - - if (isSmall) { - cn.push('isSmall'); - }; - - if (readonly) { - cn.push('isReadonly'); - }; - - if (isIcon) { - cn.push('isIcon'); - onClick = e => this.onClickFile(e); - }; - - if (focused) { - cn.push('isFocused'); - }; - - if (withFile && focused) { - placeholder += or + (!isSmall ? textFile : ''); - }; - - return ( - <div - ref={node => this.node = node} - className={cn.join(' ')} - onClick={onClick} - > - {icon ? <Icon className={icon} /> : ''} - - <div id="text" className="txt"> - <form id="form" onSubmit={this.onSubmit}> - {focused ? ( - <React.Fragment> - <Input - id="url" - ref={ref => this.refUrl = ref} - placeholder={placeholder} - onPaste={e => this.onChangeUrl(e, true)} - onFocus={onFocus} - onBlur={onBlur} - /> - <Button type="input" className="dn" /> - </React.Fragment> - ) : ( - <span className="urlToggle" onClick={this.onFocus}>{textUrl + (withFile && isSmall ? or : '')}</span> - )} - </form> + if (isSmall) { + cn.push('isSmall'); + }; - {withFile ? ( - <span className="fileWrap" onMouseDown={this.onClickFile}> - {!isSmall ? <span> {translate('commonOr')} </span> : ''} - <span className="border">{textFile}</span> - </span> - ) : ''} - </div> - </div> - ); + if (readonly) { + cn.push('isReadonly'); }; - componentDidMount () { - this._isMounted = true; - this.resize(); - this.rebind(); + if (isIcon) { + cn.push('isIcon'); + onClick = e => onClickFile(e); }; - componentDidUpdate () { - const { focused } = this.state; - const { block } = this.props; - - this.resize(); - this.rebind(); - - if (focused) { - if (this.refUrl) { - this.refUrl.focus(); - }; - focus.set(block.id, { from: 0, to: 0 }); - }; + if (isFocused) { + cn.push('isFocused'); }; - componentWillUnmount () { - const { focused } = focus.state; - const { block } = this.props; - - this._isMounted = false; - this.unbind(); - - if (focused == block.id) { - keyboard.setFocus(false); - }; + if (withFile && isFocused) { + placeholder += or + (!isSmall ? textFile : ''); }; - - rebind () { - const { canResize } = this.props; - if (!this._isMounted || !canResize) { - return; + + const rebind = () => { + if (canResize) { + $(nodeRef.current).off('resizeMove').on('resizeMove', () => resize()); }; - - $(this.node).off('resizeMove').on('resizeMove', (e: any) => this.resize()); }; - unbind () { - const { canResize } = this.props; - if (!this._isMounted || !canResize) { - return; + const unbind = () => { + if (canResize) { + $(nodeRef.current).off('resizeMove'); }; - - $(this.node).off('resizeMove'); }; - resize () { - const { canResize } = this.props; + const resize = () => { if (!canResize) { return; }; raf(() => { - if (!this._isMounted) { + const node = $(nodeRef.current); + if (!node.length) { return; }; - - const node = $(this.node); + const rect = (node.get(0) as HTMLInputElement).getBoundingClientRect(); - - let size = Size.Full; + + let s = Size.Full; if (rect.width <= SMALL_WIDTH) { - size = Size.Small; + s = Size.Small; }; if (rect.width <= ICON_WIDTH) { - size = Size.Icon; + s = Size.Icon; }; - - if (size != this.state.size) { - this.setState({ size }); + + if (s != size) { + setSize(s); }; }); }; - onFocus (e: any) { + const onFocusHandler = (e: any) => { e.stopPropagation(); - const { readonly } = this.props; - if (readonly) { - return; + if (!readonly) { + setIsFocused(true); }; - this.setState({ focused: true }); }; - onBlur (e: any) { + const onBlurHandler = (e: any) => { e.stopPropagation(); - this.setState({ focused: false }); + setIsFocused(false); }; - focus () { - this.setState({ focused: true }); - }; - - onChangeUrl (e: any, force: boolean) { - const { onChangeUrl, readonly } = this.props; - + const onChangeUrlHandler = (e: any, force: boolean) => { if (readonly) { return; }; - window.clearTimeout(this.t); - this.t = window.setTimeout(() => { - if (!this.refUrl) { + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => { + if (!urlRef.current) { return; }; - const url = this.refUrl.getValue() || ''; + const url = String(urlRef.current.getValue() || ''); if (!url) { return; }; @@ -250,9 +147,7 @@ class InputWithFile extends React.Component<Props, State> { }, force ? 50 : J.Constant.delay.keyboard); }; - onClickFile (e: any) { - const { onChangeFile, accept, readonly } = this.props; - + const onClickFile = (e: any) => { e.preventDefault(); e.stopPropagation(); @@ -267,11 +162,77 @@ class InputWithFile extends React.Component<Props, State> { }); }; - onSubmit (e: any) { + const onSubmit = (e: any) => { e.preventDefault(); - this.onChangeUrl(e, true); + onChangeUrlHandler(e, true); }; + + const onBlur = isFocused ? onBlurHandler : null; + const onFocus = !isFocused ? onFocusHandler : null; + + useEffect(() => { + resize(); + rebind(); + + return () => { + const { focused } = focus.state; + + unbind(); + + if (focused == block.id) { + keyboard.setFocus(false); + }; + }; + }, []); + + useEffect(() => { + resize(); + rebind(); + + if (isFocused) { + keyboard.setFocus(true); + urlRef.current?.focus(); + focus.set(block.id, { from: 0, to: 0 }); + }; + + }, [ isFocused, size ]); + return ( + <div + ref={nodeRef} + className={cn.join(' ')} + onClick={onClick} + > + {icon ? <Icon className={icon} /> : ''} + + <div className="inputWithFile-inner"> + <form className="form" onSubmit={onSubmit}> + {isFocused ? ( + <> + <Input + ref={urlRef} + placeholder={placeholder} + onPaste={e => onChangeUrlHandler(e, true)} + onKeyDown={e => e.stopPropagation()} + onFocus={onFocus} + onBlur={onBlur} + /> + <Button type="input" className="dn" /> + </> + ) : ( + <span className="urlToggle" onClick={onFocusHandler}>{textUrl + (withFile && isSmall ? or : '')}</span> + )} + </form> + + {withFile ? ( + <span className="fileWrap" onMouseDown={onClickFile}> + {!isSmall ? <span> {translate('commonOr')} </span> : ''} + <span className="border">{textFile}</span> + </span> + ) : ''} + </div> + </div> + ); }; export default InputWithFile; \ No newline at end of file diff --git a/src/ts/component/form/inputWithLabel.tsx b/src/ts/component/form/inputWithLabel.tsx index c124b6f66e..f9d85e26e3 100644 --- a/src/ts/component/form/inputWithLabel.tsx +++ b/src/ts/component/form/inputWithLabel.tsx @@ -1,7 +1,6 @@ -import * as React from 'react'; -import $ from 'jquery'; -import { Input, Icon, Label } from 'Component'; -import { I, translate } from 'Lib'; +import React, { forwardRef, useRef, useEffect, useState, useImperativeHandle } from 'react'; +import { Input, Label } from 'Component'; +import { I } from 'Lib'; interface Props { label: string; @@ -16,76 +15,56 @@ interface Props { onMouseLeave?(e: any): void; }; -class InputWithLabel extends React.Component<Props> { - - node: any = null; - isFocused = false; - placeholder: any = null; - ref = null; - - constructor (props: Props) { - super(props); - - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - }; - - render () { - const { label } = this.props; - - return ( - <div - ref={node => this.node = node} - onClick={() => this.ref.focus()} - className="inputWithLabel" - > - <div className="inner"> - <Label text={label} /> - - <Input - ref={ref => this.ref = ref} - {...this.props} - onFocus={this.onFocus} - onBlur={this.onBlur} - /> - - </div> - </div> - ); - }; - - componentDidMount() { - const node = $(this.node); +interface InputWithLabelRefProps { + focus(): void; + blur(): void; + setValue(v: string): void; + getValue(): string; + setRange(range: I.TextRange): void; + isFocused(): boolean; +}; - this.ref.setValue(this.props.value); +const InputWithLabel = forwardRef<InputWithLabelRefProps, Props>((props, ref) => { + const { + label = '', + value = '', + readonly = false, + onFocus, + onBlur, + } = props; + + const nodeRef = useRef(null); + const inputRef = useRef(null); + const [ isFocused, setIsFocused ] = useState(false); + const cn = [ 'inputWithLabel' ]; + + if (isFocused) { + cn.push('isFocused'); }; - focus () { - this.ref.focus(); + const focus = () => { + inputRef.current?.focus(); }; - blur () { - this.ref.blur(); + const blur = () => { + inputRef.current?.blur(); }; - setValue (v: string) { - this.ref.setValue(v); + const setValue = (v: string) => { + inputRef.current?.setValue(v); }; - getValue () { - return this.ref.getValue(); + const getValue = () => { + return inputRef.current?.getValue(); }; - setRange (range: I.TextRange) { - this.ref.setRange(range); + const setRange = (range: I.TextRange) => { + inputRef.current?.setRange(range); }; - onFocus (e: any) { - const { onFocus, readonly } = this.props; - + const onFocusHandler = (e: any) => { if (!readonly) { - this.isFocused = true; - this.addFocusedClass(); + setIsFocused(true); }; if (onFocus) { @@ -93,12 +72,9 @@ class InputWithLabel extends React.Component<Props> { }; }; - onBlur (e: any) { - const { onBlur, readonly } = this.props; - + const onBlurHandler = (e: any) => { if (!readonly) { - this.isFocused = false; - this.removeFocusedClass(); + setIsFocused(false); }; if (onBlur) { @@ -106,16 +82,37 @@ class InputWithLabel extends React.Component<Props> { }; }; - addFocusedClass () { - const node = $(this.node); - node.addClass('isFocused'); - }; + useEffect(() => inputRef.current.setValue(value), []); + + useImperativeHandle(ref, () => ({ + focus, + blur, + setValue, + getValue, + setRange, + isFocused: () => isFocused, + })); + + return ( + <div + ref={nodeRef} + onClick={focus} + className={cn.join(' ')} + > + <div className="inner"> + <Label text={label} /> + + <Input + ref={inputRef} + {...props} + onFocus={onFocusHandler} + onBlur={onBlurHandler} + /> - removeFocusedClass () { - const node = $(this.node); - node.removeClass('isFocused'); - }; + </div> + </div> + ); -}; +}); -export default InputWithLabel; +export default InputWithLabel; \ No newline at end of file diff --git a/src/ts/component/form/phrase.tsx b/src/ts/component/form/phrase.tsx index e62e8fd8ef..0c79c60d2b 100644 --- a/src/ts/component/form/phrase.tsx +++ b/src/ts/component/form/phrase.tsx @@ -1,11 +1,11 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect, useImperativeHandle } from 'react'; import $ from 'jquery'; import { getRange, setRange } from 'selection-ranges'; import { Icon } from 'Component'; import { J, S, keyboard, Storage } from 'Lib'; interface Props { - value: string; + value?: string; className?: string; readonly?: boolean; isHidden?: boolean; @@ -18,10 +18,11 @@ interface Props { onClick?: (e: any) => void; }; -interface State { - phrase: string[]; - isHidden: boolean; - hasError: boolean; +interface PhraseRefProps { + setValue: (value: string) => void; + getValue: () => string; + setError: (hasError: boolean) => void; + focus: () => void; }; const COLORS = [ @@ -34,133 +35,55 @@ const COLORS = [ 'lime', ]; -class Phrase extends React.Component<Props, State> { - - public static defaultProps: Props = { - value: '', - className: '', - }; - - state: State = { - isHidden: true, - hasError: false, - phrase: [], - }; - - node = null; - refEntry = null; - refPlaceholder = null; - range = null; - - constructor (props: Props) { - super(props); - - this.onSelect = this.onSelect.bind(this); - this.onClick = this.onClick.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onPaste = this.onPaste.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onToggle = this.onToggle.bind(this); - }; - - render () { - const { readonly, className, onCopy, placeholder } = this.props; - const { isHidden, hasError, phrase } = this.state; - const cn = [ 'phraseWrapper', className ]; - - if (isHidden) { - cn.push('isHidden'); - }; - - if (hasError) { - cn.push('hasError'); - }; - - if (readonly) { - cn.push('isReadonly'); - }; - - const renderWord = (word: string, index: number) => { - const color = COLORS[index % COLORS.length]; - const cn = isHidden ? `bg bg-${color}` : `textColor textColor-${color}`; - - return <span className={[ 'word', cn ].join(' ')} key={index}>{word}</span>; - }; - - let placeholderEl = null; - if (placeholder) { - placeholderEl = ( - <div - ref={ref => this.refPlaceholder = ref} - id="placeholder" - className="placeholder" - > - {placeholder} - </div> - ); - }; - - return ( - <div - ref={ref => this.node = ref} - className={cn.join(' ')} - onClick={this.onClick} - > - <div className="phraseInnerWrapper"> - {!phrase.length ? <span className="word" /> : ''} - {phrase.map(renderWord)} - <span - ref={ref => this.refEntry = ref} - id="entry" - contentEditable={true} - suppressContentEditableWarning={true} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onPaste={this.onPaste} - onBlur={this.onBlur} - onFocus={this.onFocus} - onSelect={this.onSelect} - > - {'\n'} - </span> - </div> - - {placeholderEl} - <Icon className={[ (isHidden ? 'see' : 'hide'), 'withBackground' ].join(' ')} onClick={this.onToggle} /> - <Icon className="copy withBackground" onClick={onCopy} /> - </div> - ); +const Phrase = forwardRef<PhraseRefProps, Props>(({ + value = '', + className = '', + readonly = false, + isHidden: initialHidden = false, + checkPin = false, + placeholder = '', + onKeyDown, + onChange, + onToggle, + onCopy, + onClick, +}, ref) => { + + const [ isHidden, setIsHidden ] = useState(false); + const [ hasError, setHasError ] = useState(false); + const [ dummy, setDummy ] = useState(0); + const placeholderRef = useRef(null); + const entryRef = useRef(null); + const range = useRef(null); + const phrase = useRef([]); + const cn = [ 'phraseWrapper', className ]; + + if (isHidden) { + cn.push('isHidden'); }; - componentDidMount () { - const { value, isHidden } = this.props; - - this.setState({ isHidden }); - this.setValue(value); - this.focus(); + if (hasError) { + cn.push('hasError'); }; - componentDidUpdate () { - this.placeholderCheck(); + if (readonly) { + cn.push('isReadonly'); }; - onClick (e: any) { - this.focus(); + const onClickHandler = (e: any) => { + focus(); - if (this.props.onClick) { - this.props.onClick(e); + if (onClick) { + onClick(e); }; }; - onKeyDown (e: React.KeyboardEvent) { - const { onKeyDown } = this.props; - const entry = $(this.refEntry); + const onKeyDownHandler = (e: React.KeyboardEvent) => { + const entry = $(entryRef.current); keyboard.shortcut('space, enter', e, () => { e.preventDefault(); - this.updateValue(); + updateValue(); }); keyboard.shortcut('backspace', e, () => { @@ -173,69 +96,70 @@ class Phrase extends React.Component<Props, State> { e.preventDefault(); - this.setState(({ phrase }) => { - phrase.pop(); - return { phrase }; - }); + phrase.current.pop(); + setDummy(dummy + 1); }); - this.placeholderCheck(); + placeholderCheck(); if (onKeyDown) { onKeyDown(e); }; }; - onKeyUp (e: React.KeyboardEvent) { - this.placeholderCheck(); + const onKeyUp = (e: React.KeyboardEvent) => { + placeholderCheck(); }; - updateValue () { - const value = this.getEntryValue(); + const updateValue = () => { + const value = getEntryValue(); if (!value.length) { return; }; - this.clear(); + clear(); - this.state.phrase = this.checkValue(this.state.phrase.concat([ value ])); - this.setState({ phrase: this.state.phrase }); + phrase.current = checkValue(phrase.current.concat(value.split(' '))); + setDummy(dummy + 1); }; - onPaste (e) { + const onPaste = (e) => { e.preventDefault(); const cb = e.clipboardData || e.originalEvent.clipboardData; - const text = this.normalizeWhiteSpace(cb.getData('text/plain')); + const text = normalizeWhiteSpace(cb.getData('text/plain')); + + clear(); + phrase.current = checkValue(phrase.current.concat(text.split(' '))); + + if (text) { + placeholderHide(); + }; - this.clear(); - this.setState(({ phrase }) => ({ phrase: this.checkValue(phrase.concat(text.split(' '))) })); + setDummy(dummy + 1); }; - onBlur () { - this.placeholderCheck(); + const onBlur = () => { + placeholderCheck(); }; - onFocus () { - this.placeholderCheck(); + const onFocus = () => { + placeholderCheck(); }; - onSelect () { - const node = $(this.node); - const entry = node.find('#entry'); + const onSelect = () => { + const entry = $(entryRef.current); if (entry.length) { - this.range = getRange(entry.get(0)); + range.current = getRange(entry.get(0)); }; }; - onToggle () { - const { checkPin, onToggle } = this.props; - const { isHidden } = this.state; + const onToggleHandler = () => { const pin = Storage.getPin(); const onSuccess = () => { - this.setState({ isHidden: !isHidden }); + setIsHidden(!isHidden); if (onToggle) { onToggle(!isHidden); @@ -249,56 +173,118 @@ class Phrase extends React.Component<Props, State> { }; }; - checkValue (v: string[]) { + const checkValue = (v: string[]) => { return v.map(it => it.substring(0, J.Constant.count.phrase.letter)).filter(it => it).slice(0, J.Constant.count.phrase.word); }; - setError (v: boolean) { - this.setState({ hasError: v }); + const setError = (v: boolean) => { + setHasError(v); }; - focus () { - const entry = $(this.refEntry); + const focus = () => { + if (readonly) { + return; + }; + + const entry = $(entryRef.current); entry.trigger('focus'); - setRange(entry.get(0), this.range || { start: 0, end: 0 }); + setRange(entry.get(0), range.current || { start: 0, end: 0 }); }; - clear () { - $(this.refEntry).text(''); + const clear = () => { + $(entryRef.current).text(''); }; - getEntryValue () { - return this.normalizeWhiteSpace($(this.refEntry).text()).toLowerCase(); + const getEntryValue = () => { + return normalizeWhiteSpace($(entryRef.current).text()).toLowerCase(); }; - normalizeWhiteSpace = (val: string) => { + const normalizeWhiteSpace = (val: string) => { return String(val || '').replace(/\s\s+/g, ' ').trim() || ''; }; - setValue (value: string) { - const text = this.normalizeWhiteSpace(value); - const phrase = text.length ? text.split(' '): []; + const setValue = (value: string) => { + const text = normalizeWhiteSpace(value); - this.setState({ phrase }); + phrase.current = text.length ? text.split(' '): []; + setDummy(dummy + 1); }; - getValue () { - return this.state.phrase.join(' ').trim().toLowerCase(); + const getValue = () => { + return phrase.current.join(' ').trim().toLowerCase(); }; - placeholderCheck () { - this.getValue().length || this.getEntryValue() ? this.placeholderHide() : this.placeholderShow(); + const placeholderCheck = () => { + getValue().length || getEntryValue() ? placeholderHide() : placeholderShow(); }; - placeholderHide () { - $(this.refPlaceholder).hide(); + const placeholderHide= () => { + $(placeholderRef.current).hide(); }; - placeholderShow () { - $(this.refPlaceholder).show(); + const placeholderShow = () => { + $(placeholderRef.current).show(); }; -}; + useEffect(() => { + setIsHidden(initialHidden); + setValue(value); + focus(); + }, []); + + useEffect(() => { + placeholderCheck(); + }, [ phrase ]); + + useImperativeHandle(ref, () => ({ + setValue, + getValue, + setError, + focus, + onToggle: onToggleHandler, + })); + + return ( + <div + className={cn.join(' ')} + onClick={onClickHandler} + > + <div className="phraseInnerWrapper"> + {!phrase.current.length ? <span className="word" /> : ''} + {phrase.current.map((item: string, i: number) => { + const color = COLORS[i % COLORS.length]; + const cn = isHidden ? `bg bg-${color}` : `textColor textColor-${color}`; + const word = isHidden ? '•'.repeat(item.length) : item; + + return ( + <span className={[ 'word', cn ].join(' ')} key={i}> + {word} + </span> + ); + })} + <span + ref={entryRef} + id="entry" + contentEditable={true} + suppressContentEditableWarning={true} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUp} + onPaste={onPaste} + onBlur={onBlur} + onFocus={onFocus} + onSelect={onSelect} + > + {'\n'} + </span> + </div> + + {placeholder ? <div ref={placeholderRef} id="placeholder" className="placeholder">{placeholder}</div> : ''} + <Icon className={[ (isHidden ? 'see' : 'hide'), 'withBackground' ].join(' ')} onClick={onToggleHandler} /> + <Icon className="copy withBackground" onClick={onCopy} /> + </div> + ); + +}); export default Phrase; \ No newline at end of file diff --git a/src/ts/component/form/pin.tsx b/src/ts/component/form/pin.tsx index 9860e3dcdc..b2d021f8c1 100644 --- a/src/ts/component/form/pin.tsx +++ b/src/ts/component/form/pin.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect, useImperativeHandle } from 'react'; import sha1 from 'sha1'; import { Input } from 'Component'; import { keyboard } from 'Lib'; @@ -14,141 +14,90 @@ interface Props { readonly?: boolean; }; -type State = { - index: number; +interface PinRefProps { + clear: () => void; + reset: () => void; + focus: () => void; + getValue: () => string; }; -/** - * This component provides an input field for a pin code - */ - const TIMEOUT_DURATION = 150; -class Pin extends React.Component<Props, State> { - - public static defaultProps = { - pinLength: 6, - expectedPin: null, - focusOnMount: true, - isVisible: false, - isNumeric: false, - }; - - state = { - index: 0, - }; - - inputRefs = []; - - // This timeout is used so that the input boxes first show the inputted value as text, then hides it as password showing (•) - timeout = 0; - - render () { - const { pinLength, isNumeric, readonly } = this.props; - const props: any = { - maxLength: 1, - onKeyUp: this.onInputKeyUp, - readonly, - }; - - if (isNumeric) { - props.inputMode = 'numeric'; - }; +const Pin = forwardRef<PinRefProps, Props>(({ + isNumeric = false, + pinLength = 6, + expectedPin = null, + focusOnMount = true, + isVisible = false, + readonly = false, + onSuccess = () => {}, + onError = () => {}, +}, ref) => { - return ( - <div className="pin" onClick={this.onClick}> - {Array(pinLength).fill(null).map((_, i) => ( - <Input - ref={ref => this.inputRefs[i] = ref} - key={i} - onPaste={e => this.onPaste(e, i)} - onFocus={() => this.onInputFocus(i)} - onKeyDown={e => this.onInputKeyDown(e, i)} - onChange={(_, value) => this.onInputChange(i, value)} - {...props} - /> - ))} - </div> - ); - }; - - componentDidMount () { - if (this.props.focusOnMount) { - this.focus(); - }; - this.rebind(); - }; + const inputRefs = useRef([]); + const index = useRef(0); - componentWillUnmount () { - window.clearTimeout(this.timeout); - this.unbind(); + const rebind = () => { + unbind(); + $(window).on('mousedown.pin', e => e.preventDefault()); }; - rebind = () => { - this.unbind(); - $(window).on('mousedown.pin', e => { e.preventDefault(); }); - }; - - unbind = () => { + const unbind = () => { $(window).off('mousedown.pin'); }; - focus = () => { - this.inputRefs[this.state.index].focus(); + const focus = () => { + inputRefs.current[index.current].focus(); }; - onClick = () => { - this.focus(); + const onClick = () => { + focus(); }; /** triggers when all the pin characters have been entered in, resetting state and calling callbacks */ - check = () => { - const { expectedPin } = this.props; - const pin = this.getValue(); + const check = () => { + const pin = getValue(); const success = !expectedPin || (expectedPin === sha1(pin)); - const onSuccess = this.props.onSuccess || (() => {}); - const onError = this.props.onError || (() => {}); success ? onSuccess(pin) : onError(); }; /** returns the pin state stored in the input DOM */ - getValue = () => { - return this.inputRefs.map((input) => input.getValue()).join(''); + const getValue = () => { + return inputRefs.current.map((input) => input.getValue()).join(''); }; /** sets all the input boxes to empty string */ - clear = () => { - for (const i in this.inputRefs) { - this.inputRefs[i].setValue(''); + const clear = () => { + for (const i in inputRefs.current) { + inputRefs.current[i].setValue(''); }; }; /** resets state */ - reset () { - this.setState({ index: 0 }, () => { - this.clear(); - this.focus(); - - for (const i in this.inputRefs) { - this.inputRefs[i].setType('text'); - }; - }); + const reset = () => { + index.current = 0; + clear(); + focus(); + + for (const i in inputRefs.current) { + inputRefs.current[i].setType('text'); + }; }; // Input subcomponent methods - onInputFocus = (index: number) => { - this.setState({ index }); + const onInputFocus = (idx: number) => { + index.current = idx; }; - onInputKeyDown = (e, index: number) => { - const { isNumeric } = this.props; - const prev = this.inputRefs[index - 1]; + const onInputKeyDown = (e, index: number) => { + const current = inputRefs.current[index]; + const prev = inputRefs.current[index - 1]; if (prev) { keyboard.shortcut('backspace', e, () => { - prev.setValue(''); + current.setValue(''); prev.setType('text'); prev.focus(); }); @@ -161,18 +110,15 @@ class Pin extends React.Component<Props, State> { }; }; - onInputKeyUp = () => { - const { pinLength } = this.props; - - if (this.getValue().length === pinLength) { - this.check(); + const onInputKeyUp = () => { + if (getValue().length === pinLength) { + check(); }; }; - onInputChange = (index: number, value: string) => { - const { isVisible, isNumeric } = this.props; - const input = this.inputRefs[index]; - const next = this.inputRefs[index + 1]; + const onInputChange = (index: number, value: string) => { + const input = inputRefs.current[index]; + const next = inputRefs.current[index + 1]; let newValue = value; if (isNumeric) { @@ -196,29 +142,70 @@ class Pin extends React.Component<Props, State> { }; if (!isVisible) { - this.timeout = window.setTimeout(() => input.setType('password'), TIMEOUT_DURATION); + window.setTimeout(() => input.setType('password'), TIMEOUT_DURATION); }; }; - async onPaste (e: any, index: number) { + const onPaste = async (e: any, index: number) => { e.preventDefault(); - const { pinLength } = this.props; const text = await navigator.clipboard.readText(); const value = String(text || '').split(''); for (let i = index; i < pinLength; i++) { - const input = this.inputRefs[i]; + const input = inputRefs.current[i]; const char = value[i - index] || ''; input.setValue(char); input.setType('text'); }; - this.inputRefs[pinLength - 1].focus(); - this.check(); + inputRefs.current[pinLength - 1].focus(); + check(); }; -}; + useImperativeHandle(ref, () => ({ + clear, + reset, + focus, + getValue, + })); + + useEffect(() => { + if (focusOnMount) { + window.setTimeout(() => focus(), 10); + }; + + rebind(); + return () => unbind(); + }, []); + + const props: any = { + maxLength: 1, + onKeyUp: onInputKeyUp, + readonly, + }; + + if (isNumeric) { + props.inputMode = 'numeric'; + }; + + return ( + <div className="pin" onClick={onClick}> + {Array(pinLength).fill(null).map((_, i) => ( + <Input + ref={ref => inputRefs.current[i] = ref} + key={i} + onPaste={e => onPaste(e, i)} + onFocus={() => onInputFocus(i)} + onKeyDown={e => onInputKeyDown(e, i)} + onChange={(_, value) => onInputChange(i, value)} + {...props} + /> + ))} + </div> + ); + +}); -export default Pin; +export default Pin; \ No newline at end of file diff --git a/src/ts/component/form/select.tsx b/src/ts/component/form/select.tsx index f61f1a4a76..fac21b5ccd 100644 --- a/src/ts/component/form/select.tsx +++ b/src/ts/component/form/select.tsx @@ -1,6 +1,6 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useEffect, useImperativeHandle, MouseEvent } from 'react'; import $ from 'jquery'; -import { I, S, Relation } from 'Lib'; +import { I, S, U, Relation } from 'Lib'; import { Icon, MenuItemVertical } from 'Component'; interface Props { @@ -11,7 +11,7 @@ interface Props { element?: string; value: any; options: I.Option[]; - noFilter: boolean; + noFilter?: boolean; isMultiple?: boolean; showOn?: string; readonly?: boolean; @@ -19,159 +19,82 @@ interface Props { onChange? (id: any): void; }; -interface State { - value: string[]; - options: I.Option[]; +interface SelectRefProps { + getValue: () => any; + setValue: (v: any) => void; + setOptions: (options: I.Option[]) => void; }; -class Select extends React.Component<Props, State> { - - public static defaultProps = { - initial: '', - noFilter: true, - showOn: 'click', - }; - - _isMounted = false; - state = { - value: [], - options: [] as I.Option[] - }; - - constructor (props: Props) { - super(props); - - this.show = this.show.bind(this); - this.hide = this.hide.bind(this); +const Select = forwardRef<SelectRefProps, Props>(({ + id = '', + initial = '', + className = '', + arrowClassName = '', + element = '', + value: initialValue = [], + options: initialOptions = [], + noFilter = true, + isMultiple = false, + showOn = 'click', + readonly = false, + menuParam = {}, + onChange, +}, ref) => { + const [ value, setValue ] = useState(initialValue); + const [ options, setOptions ] = useState(initialOptions); + const cn = [ 'select', className ]; + const acn = [ 'arrow', arrowClassName ]; + const current: any[] = []; + + if (className) { + cn.push(className); }; - - render () { - const { id, className, arrowClassName, readonly, showOn } = this.props; - const { options } = this.state; - const cn = [ 'select' ]; - const acn = [ 'arrow', (arrowClassName ? arrowClassName : '') ]; - const value = Relation.getArrayValue(this.state.value); - const current: any[] = []; - - if (className) { - cn.push(className); - }; - - if (readonly) { - cn.push('isReadonly'); - }; - value.forEach((id: string) => { - const option = options.find(item => item.id == id); - if (option) { - current.push(option); - }; - }); - - if (!current.length && options.length) { - current.push(options[0]); - }; - - let onClick = null; - let onMouseDown = null; - let onMouseEnter = null; - - if (showOn == 'mouseDown') { - onMouseDown = this.show; - }; - - if (showOn == 'click') { - onClick = this.show; - }; - - if (showOn == 'mouseEnter') { - onMouseEnter = this.show; - }; - - return ( - <div - id={`select-${id}`} - className={cn.join(' ')} - onClick={onClick} - onMouseDown={onMouseDown} - onMouseEnter={onMouseEnter} - > - {current ? ( - <React.Fragment> - {current.map((item: any, i: number) => ( - <MenuItemVertical key={i} {...item} /> - ))} - <Icon className={acn.join(' ')} /> - </React.Fragment> - ) : ''} - </div> - ); + if (readonly) { + cn.push('isReadonly'); }; - - componentDidMount () { - this._isMounted = true; - const options = this.getOptions(); - - let value = Relation.getArrayValue(this.props.value); - if (!value.length && options.length) { - value = [ options[0].id ]; + let val = Relation.getArrayValue(value); + val.forEach((id: string) => { + const option = options.find(item => item.id == id); + if (option) { + current.push(option); }; + }); - this.setState({ value, options }); - }; - - componentWillUnmount () { - this._isMounted = false; + if (!current.length && options.length) { + current.push(options[0]); }; - getOptions () { - const { initial } = this.props; - const options = []; + const getOptions = () => { + const ret = []; if (initial) { - options.push({ id: '', name: initial, isInitial: true }); + ret.push({ id: '', name: initial, isInitial: true }); }; - for (const option of this.props.options) { - options.push(option); + for (const option of initialOptions) { + ret.push(option); }; - - return options; - }; - - setOptions (options: any[]) { - this.setState({ options }); + return ret; }; - getValue (): any { - const { isMultiple } = this.props; - const value = Relation.getArrayValue(this.state.value); - - return isMultiple ? value : value[0]; + const getValue = (val: any): any => { + return isMultiple ? val : (val.length ? val[0] : ''); }; - - setValue (v: any) { - const value = Relation.getArrayValue(v); - if (this._isMounted) { - this.state.value = value; - this.setState({ value }); - }; + const setValueHandler = (v: any) => { + setValue(Relation.getArrayValue(v)); }; - show (e: React.MouseEvent) { + const show = (e: MouseEvent) => { e.stopPropagation(); - const { id, onChange, noFilter, isMultiple, readonly } = this.props; - const { value, options } = this.state; - const elementId = `#select-${id}`; - const element = this.props.element || elementId; - if (readonly) { return; }; - const mp = this.props.menuParam || {}; + const el = element || `#select-${id}`; + const mp = menuParam || {}; let onOpen = null; let onClose = null; @@ -185,18 +108,18 @@ class Select extends React.Component<Props, State> { delete(mp.onClose); }; - const menuParam = Object.assign({ - element, + const param = Object.assign({ + element: el, noFlipX: true, onOpen: (context: any) => { - window.setTimeout(() => $(element).addClass('isFocused')); + window.setTimeout(() => $(el).addClass('isFocused')); if (onOpen) { onOpen(context); }; }, onClose: () => { - window.setTimeout(() => $(element).removeClass('isFocused')); + window.setTimeout(() => $(el).removeClass('isFocused')); if (onClose) { onClose(); @@ -204,47 +127,97 @@ class Select extends React.Component<Props, State> { }, }, mp); - menuParam.data = Object.assign({ + param.data = Object.assign({ noFilter, noClose: true, value, - options, + options: U.Menu.prepareForSelect(options), onSelect: (e: any, item: any) => { - let { value } = this.state; - if (item.id !== '') { if (isMultiple) { - value = value.includes(item.id) ? value.filter(it => it != item.id) : [ ...value, item.id ]; + val = val.includes(item.id) ? val.filter(it => it != item.id) : [ ...val, item.id ]; } else { - value = [ item.id ]; + val = [ item.id ]; }; } else { - value = []; + val = []; }; - this.setValue(value); + setValueHandler(val); if (onChange) { - onChange(this.getValue()); + onChange(getValue(val)); }; if (!isMultiple) { - this.hide(); + hide(); } else { - S.Menu.updateData('select', { value }); + S.Menu.updateData('select', { value: val }); }; }, }, mp.data || {}); S.Menu.closeAll([ 'select' ], () => { - S.Menu.open('select', menuParam); + S.Menu.open('select', param); }); }; - hide () { + const hide = () => { S.Menu.close('select'); }; - -}; + + let onClick = null; + let onMouseDown = null; + let onMouseEnter = null; + + if (showOn == 'mouseDown') { + onMouseDown = show; + }; + + if (showOn == 'click') { + onClick = show; + }; + + if (showOn == 'mouseEnter') { + onMouseEnter = show; + }; + + useEffect(() => { + const options = getOptions(); + + let val = Relation.getArrayValue(initialValue); + if (!val.length && options.length) { + val = [ options[0].id ]; + }; + + setValue(val); + setOptions(options); + }, []); + + useImperativeHandle(ref, () => ({ + getValue: () => getValue(val), + setValue: setValueHandler, + setOptions: (options: I.Option[]) => setOptions(options), + })); + + return ( + <div + id={`select-${id}`} + className={cn.join(' ')} + onClick={onClick} + onMouseDown={onMouseDown} + onMouseEnter={onMouseEnter} + > + {current ? ( + <> + {current.map((item: any, i: number) => ( + <MenuItemVertical key={i} {...item} /> + ))} + <Icon className={acn.join(' ')} /> + </> + ) : ''} + </div> + ); +}); export default Select; \ No newline at end of file diff --git a/src/ts/component/form/switch.tsx b/src/ts/component/form/switch.tsx index 4b085a4580..62aef6301b 100644 --- a/src/ts/component/form/switch.tsx +++ b/src/ts/component/form/switch.tsx @@ -46,7 +46,7 @@ const Switch = forwardRef<SwitchRefProps, Props>(({ }; }; - useEffect(() => setValue(initialValue)); + useEffect(() => setValue(initialValue), []); useImperativeHandle(ref, () => ({ getValue: () => value, diff --git a/src/ts/component/form/textarea.tsx b/src/ts/component/form/textarea.tsx index bd41fe9fec..52d91485ce 100644 --- a/src/ts/component/form/textarea.tsx +++ b/src/ts/component/form/textarea.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect, useImperativeHandle } from 'react'; import $ from 'jquery'; import { keyboard } from 'Lib'; @@ -22,182 +22,150 @@ interface Props { onPaste?(e: any): void; }; -interface State { - value: string; +interface TextareaRefProps { + focus(): void; + select(): void; + getValue(): string; + setError(v: boolean): void; + addClass(v: string): void; + removeClass(v: string): void; }; -class Textarea extends React.Component<Props, State> { - - public static defaultProps = { - value: '' - }; - - _isMounted = false; - node: any = null; - textAreaElement: HTMLTextAreaElement; - - state = { - value: '', - }; - - constructor (props: Props) { - super(props); - - this.onChange = this.onChange.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - this.onKeyUp = this.onKeyUp.bind(this); - this.onInput = this.onInput.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onCopy = this.onCopy.bind(this); - this.onPaste = this.onPaste.bind(this); - }; - - render () { - const { id, name, className, placeholder, rows, autoComplete, readonly, maxLength } = this.props; - const { value } = this.state; - const cn = [ 'textarea' ]; - - if (className) { - cn.push(className); +const Textarea = forwardRef<TextareaRefProps, Props>(({ + id = '', + name = '', + placeholder = '', + className = '', + rows = null, + value: initialValue = '', + autoComplete = null, + maxLength = null, + readonly = false, + onChange, + onKeyDown, + onKeyUp, + onInput, + onFocus, + onBlur, + onCopy, + onPaste, +}, ref) => { + const [ value, setValue ] = useState(initialValue); + const nodeRef = useRef(null); + const cn = [ 'textarea' ]; + + if (className) { + cn.push(className); + }; + + const onChangeHandler = (e: any) => { + setValue(e.target.value); + if (onChange) { + onChange(e, e.target.value); }; - - return ( - <textarea - ref={node => this.node = node} - name={name} - id={id} - placeholder={placeholder} - value={value} - rows={rows} - className={cn.join(' ')} - autoComplete={autoComplete} - readOnly={readonly} - onChange={this.onChange} - onKeyDown={this.onKeyDown} - onKeyUp={this.onKeyUp} - onInput={this.onInput} - onFocus={this.onFocus} - onBlur={this.onBlur} - onCopy={this.onCopy} - onPaste={this.onPaste} - maxLength={maxLength ? maxLength : undefined} - spellCheck={false} - /> - ); - }; - - componentDidMount () { - this._isMounted = true; - this.textAreaElement = $(this.node).get(0) as HTMLTextAreaElement; - this.setValue(this.props.value ? this.props.value : ''); }; - componentWillUnmount () { - this._isMounted = false; - }; - - onChange (e: any) { - this.setValue(e.target.value); - if (this.props.onChange) { - this.props.onChange(e, e.target.value); + const onKeyDownHandler = (e: any) => { + setValue(e.target.value); + if (onKeyDown) { + onKeyDown(e, e.target.value); }; }; - onKeyDown (e: any) { - this.setValue(e.target.value); - if (this.props.onKeyDown) { - this.props.onKeyDown(e, e.target.value); - }; - }; - - onKeyUp (e: any) { - this.setValue(e.target.value); - if (this.props.onKeyUp) { - this.props.onKeyUp(e, e.target.value); + const onKeyUpHandler = (e: any) => { + setValue(e.target.value); + if (onKeyUp) { + onKeyUp(e, e.target.value); }; }; - onInput (e: any) { - if (this.props.onInput) { - this.props.onInput(e, e.target.value); + const onInputHandler = (e: any) => { + if (onInput) { + onInput(e, e.target.value); }; }; - - onFocus (e: any) { - if (this.props.onFocus) { - this.props.onFocus(e, this.state.value); + + const onFocusHandler = (e: any) => { + if (onFocus) { + onFocus(e, value); }; - keyboard.setFocus(true); - this.addClass('isFocused'); + $(nodeRef.current).addClass('isFocused'); }; - - onBlur (e: any) { - if (this.props.onBlur) { - this.props.onBlur(e, this.state.value); + + const onBlurHandler = (e: any) => { + if (onBlur) { + onBlur(e, value); }; - keyboard.setFocus(false); - this.removeClass('isFocused'); + $(nodeRef.current).removeClass('isFocused'); }; - onCopy (e: any) { - if (this.props.onCopy) { - this.props.onCopy(e, this.state.value); + const onCopyHandler = (e: any) => { + if (onCopy) { + onCopy(e, value); }; }; - - onPaste (e: any) { - if (this.props.onPaste) { - this.props.onPaste(e); + + const onPasteHandler = (e: any) => { + if (onPaste) { + onPaste(e); }; }; - focus () { - window.setTimeout(() => { - if (!this._isMounted) { - return; - }; - - this.textAreaElement.focus({ preventScroll: true }); - }); + const focus = () => { + window.setTimeout(() => nodeRef.current.focus({ preventScroll: true })); }; - select () { - window.setTimeout(() => { - if (!this._isMounted) { - return; - }; - - this.textAreaElement.select(); - }); + const select = () => { + window.setTimeout(() => nodeRef.current.select()); }; - setValue (v: string) { - this.setState({ value: v }); - }; - - getValue () { - return this.state.value; - }; - - setError (v: boolean) { - $(this.node).toggleClass('withError', v); + const setError = (v: boolean) => { + $(nodeRef.current).toggleClass('withError', v); }; - addClass (v: string) { - if (this._isMounted) { - $(this.node).addClass(v); - }; + const addClass = (v: string) => { + $(nodeRef.current).addClass(v); }; - removeClass (v: string) { - if (this._isMounted) { - $(this.node).removeClass(v); - }; + const removeClass = (v: string) => { + $(nodeRef.current).removeClass(v); }; -}; + useEffect(() => setValue(initialValue)); + useImperativeHandle(ref, () => ({ + focus, + select, + getValue: () => value, + setError, + addClass, + removeClass + })); + + return ( + <textarea + ref={nodeRef} + name={name} + id={id} + placeholder={placeholder} + value={value} + rows={rows} + className={cn.join(' ')} + autoComplete={autoComplete} + readOnly={readonly} + onChange={onChangeHandler} + onKeyDown={onKeyDownHandler} + onKeyUp={onKeyUpHandler} + onInput={onInputHandler} + onFocus={onFocusHandler} + onBlur={onBlurHandler} + onCopy={onCopyHandler} + onPaste={onPasteHandler} + maxLength={maxLength ? maxLength : undefined} + spellCheck={false} + /> + ); +}); export default Textarea; \ No newline at end of file diff --git a/src/ts/component/graph/provider.tsx b/src/ts/component/graph/provider.tsx index 11ec34949f..5dfb72d48e 100644 --- a/src/ts/component/graph/provider.tsx +++ b/src/ts/component/graph/provider.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useImperativeHandle, useRef, useEffect } from 'react'; import * as ReactDOM from 'react-dom'; import $ from 'jquery'; import * as d3 from 'd3'; @@ -14,116 +14,83 @@ interface Props { storageKey: string; }; -const Graph = observer(class Graph extends React.Component<Props> { - - node: any = null; - canvas: any = null; - edges: any[] = []; - nodes: any[] = []; - worker: any = null; - images: any = {}; - subject: any = null; - isDragging = false; - isPreviewDisabled = false; - ids: string[] = []; - timeoutPreview = 0; - zoom: any = null; - previewId = ''; - - constructor (props: Props) { - super(props); - - this.onMessage = this.onMessage.bind(this); - this.nodeMapper = this.nodeMapper.bind(this); - this.setRootId = this.setRootId.bind(this); - }; - - render () { - return ( - <div - ref={node => this.node = node} - id="graphWrapper" - > - <div id={this.getId()} /> - </div> - ); - }; - - componentDidMount () { - this.rebind(); - }; +interface GraphRefProps { + init: () => void; + resize: () => void; + addNewNode: (id: string, sourceId?: string, param?: any, callBack?: (object: any) => void) => void; +}; - componentWillUnmount () { - if (this.worker) { - this.worker.terminate(); +const Graph = observer(forwardRef<GraphRefProps, Props>(({ + id = '', + isPopup = false, + rootId = '', + data = {}, + storageKey = '', +}, ref) => { + + const nodeRef = useRef(null); + const worker = useRef(null); + const theme = S.Common.getThemeClass(); + const elementId = [ 'graph', id ].join('-') + U.Common.getEventNamespace(isPopup); + const previewId = useRef(''); + const canvas = useRef(null); + const edges = useRef([]); + const nodes = useRef([]); + const images = useRef({}); + const subject = useRef(null); + const isDragging = useRef(false); + const isPreviewDisabled = useRef(false); + const ids = useRef([]); + const zoom = useRef(null); + + const send = (id: string, param: any, transfer?: any[]) => { + if (worker.current) { + worker.current.postMessage({ id, ...param }, transfer); }; - - this.unbind(); - this.onPreviewHide(); }; - rebind () { + const rebind = () => { const win = $(window); - this.unbind(); - win.on('updateGraphSettings.graph', () => this.updateSettings()); - win.on('updateGraphRoot.graph', (e: any, data: any) => this.setRootId(data.id)); - win.on('removeGraphNode.graph', (e: any, data: any) => this.send('onRemoveNode', { ids: U.Common.objectCopy(data.ids) })); - win.on(`keydown.graph`, e => this.onKeyDown(e)); - win.on('updateTheme.graph', () => { - const theme = S.Common.getThemeClass(); - this.send('updateTheme', { theme, colors: J.Theme[theme].graph || {} }); - }); + unbind(); + win.on('updateGraphSettings.graph', () => updateSettings()); + win.on('updateGraphRoot.graph', (e: any, data: any) => setRootId(data.id)); + win.on('removeGraphNode.graph', (e: any, data: any) => send('onRemoveNode', { ids: U.Common.objectCopy(data.ids) })); + win.on(`keydown.graph`, e => onKeyDown(e)); }; - unbind () { - const events = [ 'updateGraphSettings', 'updateGraphRoot', 'updateTheme', 'removeGraphNode', 'keydown' ]; + const unbind = () => { + const events = [ 'updateGraphSettings', 'updateGraphRoot', 'removeGraphNode', 'keydown' ]; $(window).off(events.map(it => `${it}.graph`).join(' ')); }; - getId (): string { - const { id, isPopup } = this.props; - const ret = [ 'graph' ]; - - if (id) { - ret.push(id); - }; - if (isPopup) { - ret.push('popup'); - }; - return ret.join('-'); - }; - - init () { - const { data, rootId, storageKey } = this.props; - const node = $(this.node); + const init = () => { + const node = $(nodeRef.current); const density = window.devicePixelRatio; - const elementId = `#${this.getId()}`; const width = node.width(); const height = node.height(); - const theme = S.Common.getThemeClass(); const settings = S.Common.getGraph(storageKey); - this.images = {}; - this.zoom = d3.zoom().scaleExtent([ 0.05, 10 ]).on('zoom', e => this.onZoom(e)); - this.edges = (data.edges || []).map(this.edgeMapper); - this.nodes = (data.nodes || []).map(this.nodeMapper); + images.current = {}; + zoom.current = d3.zoom().scaleExtent([ 0.05, 10 ]).on('zoom', e => onZoom(e)); + edges.current = (data.edges || []).map(edgeMapper); + nodes.current = (data.nodes || []).map(nodeMapper); node.find('canvas').remove(); - this.canvas = d3.select(elementId).append('canvas') + canvas.current = d3.select(`#${elementId}`).append('canvas') .attr('width', (width * density) + 'px') .attr('height', (height * density) + 'px') .node(); - const transfer = node.find('canvas').get(0).transferControlToOffscreen(); + const transfer = canvas.current.transferControlToOffscreen(); - this.worker = new Worker('workers/graph.js'); - this.worker.onerror = (e: any) => console.log(e); - this.worker.addEventListener('message', this.onMessage); + worker.current = new Worker('workers/graph.js'); + worker.current.onerror = (e: any) => console.log(e); + worker.current.addEventListener('message', onMessage); - this.send('init', { + send('init', { canvas: transfer, width, height, @@ -131,20 +98,20 @@ const Graph = observer(class Graph extends React.Component<Props> { theme, settings, rootId, - nodes: this.nodes, - edges: this.edges, + nodes: nodes.current, + edges: edges.current, colors: J.Theme[theme].graph || {}, }, [ transfer ]); - d3.select(this.canvas) + d3.select(canvas.current) .call(d3.drag(). - subject(() => this.subject). - on('start', (e: any, d: any) => this.onDragStart(e)). - on('drag', (e: any, d: any) => this.onDragMove(e)). - on('end', (e: any, d: any) => this.onDragEnd(e)) + subject(() => subject.current). + on('start', (e: any, d: any) => onDragStart(e)). + on('drag', (e: any, d: any) => onDragMove(e)). + on('end', (e: any, d: any) => onDragEnd(e)) ) - .call(this.zoom) - .call(this.zoom.transform, d3.zoomIdentity.translate(0, 0).scale(1)) + .call(zoom.current) + .call(zoom.current.transform, d3.zoomIdentity.translate(0, 0).scale(1)) .on('click', (e: any) => { const { local } = S.Common.getGraph(storageKey); const [ x, y ] = d3.pointer(e); @@ -156,25 +123,25 @@ const Graph = observer(class Graph extends React.Component<Props> { event = e.shiftKey ? 'onSelect' : 'onClick'; }; - this.send(event, { x, y }); + send(event, { x, y }); }) .on('dblclick', (e: any) => { if (e.shiftKey) { const [ x, y ] = d3.pointer(e); - this.send('onSelect', { x, y, selectRelated: true }); + send('onSelect', { x, y, selectRelated: true }); }; }) .on('contextmenu', (e: any) => { const [ x, y ] = d3.pointer(e); - this.send('onContextMenu', { x, y }); + send('onContextMenu', { x, y }); }) .on('mousemove', (e: any) => { const [ x, y ] = d3.pointer(e); - this.send('onMouseMove', { x, y }); + send('onMouseMove', { x, y }); }); }; - nodeMapper (d: any) { + const nodeMapper = (d: any) => { d = d || {}; d.layout = Number(d.layout) || 0; d.radius = 4; @@ -197,21 +164,21 @@ const Graph = observer(class Graph extends React.Component<Props> { d.iconEmoji = ''; }; - if (!this.images[d.src]) { + if (!images.current[d.src]) { const img = new Image(); img.onload = () => { - if (this.images[d.src]) { + if (images.current[d.src]) { return; }; createImageBitmap(img, { resizeWidth: 160, resizeQuality: 'high' }).then((res: any) => { - if (this.images[d.src]) { + if (images.current[d.src]) { return; }; - this.images[d.src] = true; - this.send('image', { src: d.src, bitmap: res }); + images.current[d.src] = true; + send('image', { src: d.src, bitmap: res }); }); }; img.crossOrigin = ''; @@ -221,24 +188,24 @@ const Graph = observer(class Graph extends React.Component<Props> { return d; }; - edgeMapper (d: any) { + const edgeMapper = (d: any) => { d.type = Number(d.type) || 0; d.typeName = translate('edgeType' + d.type); return d; }; - updateSettings () { - this.send('updateSettings', S.Common.getGraph(this.props.storageKey)); + const updateSettings = () => { + send('updateSettings', S.Common.getGraph(storageKey)); }; - onDragStart (e: any) { - this.isDragging = true; - this.send('onDragStart', { active: e.active }); + const onDragStart = (e: any) => { + isDragging.current = true; + send('onDragStart', { active: e.active }); }; - onDragMove (e: any) { - const p = d3.pointer(e, d3.select(this.canvas)); - const node = $(this.node); + const onDragMove = (e: any) => { + const p = d3.pointer(e, d3.select(canvas.current)); + const node = $(nodeRef.current); if (!node || !node.length) { return; @@ -246,36 +213,36 @@ const Graph = observer(class Graph extends React.Component<Props> { const { left, top } = node.offset(); - this.send('onDragMove', { - subjectId: this.subject.id, + send('onDragMove', { + subjectId: subject.current?.id, active: e.active, x: p[0] - left, y: p[1] - top, }); }; - onDragEnd (e: any) { - this.isDragging = false; - this.subject = null; - this.send('onDragEnd', { active: e.active }); + const onDragEnd = (e: any) => { + isDragging.current = false; + subject.current = null; + send('onDragEnd', { active: e.active }); }; - onZoom ({ transform }) { - this.send('onZoom', { transform }); + const onZoom = ({ transform }) => { + send('onZoom', { transform }); }; - onPreviewShow (data: any) { - if (this.isPreviewDisabled || !this.subject) { + const onPreviewShow = (data: any) => { + if (isPreviewDisabled.current || !subject.current) { return; }; const win = $(window); const body = $('body'); - const node = $(this.node); + const node = $(nodeRef.current); const { left, top } = node.offset(); - const render = this.previewId != this.subject.id; + const render = previewId != subject.current.id; - this.previewId = this.subject.id; + previewId.current = subject.current.id; let el = $('#graphPreview'); @@ -293,31 +260,26 @@ const Graph = observer(class Graph extends React.Component<Props> { body.find('#graphPreview').remove(); body.append(el); - ReactDOM.render(<PreviewDefault object={this.subject} className="previewGraph" />, el.get(0), position); - analytics.event('SelectGraphNode', { objectType: this.subject.type, layout: this.subject.layout }); + ReactDOM.render(<PreviewDefault object={subject.current} className="previewGraph" />, el.get(0), position); + analytics.event('SelectGraphNode', { objectType: subject.current.type, layout: subject.current.layout }); } else { position(); }; }; - onPreviewHide () { + const onPreviewHide = () => { $('#graphPreview').remove(); }; - onMessage (e) { - const { storageKey } = this.props; + const onMessage = (e) => { const settings = S.Common.getGraph(storageKey); const { id, data } = e.data; - const node = $(this.node); + const node = $(nodeRef.current); const { left, top } = node.offset(); const menuParam = { - onOpen: () => { - this.isPreviewDisabled = true; - }, - onClose: () => { - this.isPreviewDisabled = false; - }, + onOpen: () => isPreviewDisabled.current = true, + onClose: () => isPreviewDisabled.current = false, recalcRect: () => ({ width: 0, height: 0, @@ -328,30 +290,30 @@ const Graph = observer(class Graph extends React.Component<Props> { switch (id) { case 'onClick': { - this.onClickObject(data.node); + onClickObject(data.node); break; }; case 'onSelect': { - this.onSelect(data.node, data.related); + onSelect(data.node, data.related); break; }; case 'onMouseMove': { - if (this.isDragging) { + if (isDragging.current) { break; }; - this.subject = this.nodes.find(d => d.id == data.node); + subject.current = getNode(data.node); if (settings.preview) { - this.subject ? this.onPreviewShow(data) : this.onPreviewHide(); + subject.current ? onPreviewShow(data) : onPreviewHide(); }; break; }; case 'onDragMove': { - this.onPreviewHide(); + onPreviewHide(); break; }; @@ -360,91 +322,94 @@ const Graph = observer(class Graph extends React.Component<Props> { break; }; - this.onPreviewHide(); - this.onContextMenu(data.node.id, menuParam); + onPreviewHide(); + onContextMenu(data.node.id, menuParam); break; }; case 'onContextSpaceClick': { - this.onPreviewHide(); - this.onContextSpaceClick(menuParam, data); + onPreviewHide(); + onContextSpaceClick(menuParam, data); break; }; case 'onTransform': { - d3.select(this.canvas) - .call(this.zoom) - .call(this.zoom.transform, d3.zoomIdentity.translate(data.x, data.y).scale(data.k)); + d3.select(canvas.current) + .call(zoom.current) + .call(zoom.current.transform, d3.zoomIdentity.translate(data.x, data.y).scale(data.k)); break; }; + case 'setRootId': { + $(window).trigger('updateGraphRoot', { id: data.node }); + }; + }; }; - onKeyDown (e: any) { + const onKeyDown = (e: any) => { const cmd = keyboard.cmdKey(); - const length = this.ids.length; + const length = ids.current.length; keyboard.shortcut(`${cmd}+f`, e, () => $('#button-header-search').trigger('click')); - if (length) { - keyboard.shortcut('escape', e, () => { - this.ids = []; - this.send('onSetSelected', { ids: [] }); - }); + if (!length) { + return; + }; - keyboard.shortcut('backspace, delete', e, () => { - Action.archive(this.ids, analytics.route.graph, () => { - this.nodes = this.nodes.filter(d => !this.ids.includes(d.id)); - this.send('onRemoveNode', { ids: this.ids }); - }); + keyboard.shortcut('escape', e, () => setSelected([])); + + keyboard.shortcut('backspace, delete', e, () => { + Action.archive(ids.current, analytics.route.graph, () => { + nodes.current = nodes.current.filter(d => !ids.current.includes(d.id)); + send('onRemoveNode', { ids: ids }); }); - }; + }); }; - onContextMenu (id: string, param: any) { - const ids = this.ids.length ? this.ids : [ id ]; + const onContextMenu = (id: string, param: any) => { + const selected = ids.current.length ? ids.current : [ id ]; S.Menu.open('dataviewContext', { ...param, data: { route: analytics.route.graph, - objectIds: ids, - getObject: id => this.getNode(id), + objectIds: selected, + getObject: id => getNode(id), allowedLinkTo: true, allowedOpen: true, onLinkTo: (sourceId: string, targetId: string) => { - const target = this.getNode(targetId); + const target = getNode(targetId); if (target) { - this.edges.push(this.edgeMapper({ type: I.EdgeType.Link, source: sourceId, target: targetId })); - this.send('onSetEdges', { edges: this.edges }); + edges.current.push(edgeMapper({ type: I.EdgeType.Link, source: sourceId, target: targetId })); + send('onSetEdges', { edges: edges.current }); } else { - this.addNewNode(targetId, sourceId, null); + addNewNode(targetId, sourceId, null); }; }, onSelect: (itemId: string) => { switch (itemId) { case 'archive': { - this.nodes = this.nodes.filter(d => !ids.includes(d.id)); - this.send('onRemoveNode', { ids }); + nodes.current = nodes.current.filter(d => !selected.includes(d.id)); + send('onRemoveNode', { ids: selected }); break; }; case 'fav': { - ids.forEach((id: string) => { - const node = this.getNode(id); + selected.forEach(id => { + const node = getNode(id); if (node) { node.isFavorite = true; }; }); - this.send('onSetEdges', { edges: this.edges }); + send('onSetEdges', { edges: edges.current }); break; }; case 'unfav': { - ids.forEach((id: string) => { - const node = this.getNode(id); + selected.forEach(id => { + const node = getNode(id); if (node) { node.isFavorite = false; @@ -454,14 +419,13 @@ const Graph = observer(class Graph extends React.Component<Props> { }; }; - this.ids = []; - this.send('onSetSelected', { ids: this.ids }); + setSelected(ids.current); }, } }); }; - onContextSpaceClick (param: any, data: any) { + const onContextSpaceClick = (param: any, data: any) => { if (!U.Space.canMyParticipantWrite()) { return; }; @@ -478,7 +442,7 @@ const Graph = observer(class Graph extends React.Component<Props> { const flags = [ I.ObjectFlag.SelectType, I.ObjectFlag.SelectTemplate ]; U.Object.create('', '', {}, I.BlockPosition.Bottom, '', flags, analytics.route.graph, (message: any) => { - U.Object.openConfig(message.details, { onClose: () => this.addNewNode(message.targetId, '', data) }); + U.Object.openConfig(message.details, { onClose: () => addNewNode(message.targetId, '', data) }); }); break; }; @@ -488,48 +452,46 @@ const Graph = observer(class Graph extends React.Component<Props> { }); }; - onSelect (id: string, related?: string[]) { - const isSelected = this.ids.includes(id); + const onSelect = (id: string, related?: string[]) => { + const isSelected = ids.current.includes(id); - let ids = [ id ]; + let ret = [ id ]; if (related && related.length) { if (!isSelected) { - this.ids = []; + ret = []; }; - ids = ids.concat(related); + ret = ret.concat(related); }; - ids.forEach((id) => { + ret.forEach(id => { if (isSelected) { - this.ids = this.ids.filter(it => it != id); + ids.current = ids.current.filter(it => it != id); return; }; - this.ids = this.ids.includes(id) ? this.ids.filter(it => it != id) : this.ids.concat([ id ]); + ids.current = ids.current.includes(id) ? ids.current.filter(it => it != id) : ids.current.concat([ id ]); }); - this.send('onSetSelected', { ids: this.ids }); + setSelected(ids.current); }; - onClickObject (id: string) { - this.ids = []; - this.send('onSetSelected', { ids: [] }); - - U.Object.openAuto(this.nodes.find(d => d.id == id)); + const onClickObject = (id: string) => { + setSelected([]); + U.Object.openAuto(getNode(id)); }; - addNewNode (id: string, sourceId?: string, param?: any, callBack?: (object: any) => void) { + const addNewNode = (id: string, sourceId?: string, param?: any, callBack?: (object: any) => void) => { U.Object.getById(id, {}, (object: any) => { - object = this.nodeMapper(object); + object = nodeMapper(object); if (param) { object = Object.assign(object, param); }; - this.nodes.push(object); - this.send('onAddNode', { target: object, sourceId }); + nodes.current.push(object); + send('onAddNode', { target: object, sourceId }); if (callBack) { callBack(object); @@ -537,30 +499,60 @@ const Graph = observer(class Graph extends React.Component<Props> { }); }; - getNode (id: string) { - return this.nodes.find(d => d.id == id); + const getNode = (id: string) => { + return nodes.current.find(d => d.id == id); }; - setRootId (id: string) { - this.send('setRootId', { rootId: id }); + const setRootId = (id: string) => { + send('setRootId', { rootId: id }); }; - send (id: string, param: any, transfer?: any[]) { - if (this.worker) { - this.worker.postMessage({ id, ...param }, transfer); - }; + const setSelected = (selected: string[]) => { + ids.current = selected; + send('onSetSelected', { ids: ids.current }); }; - resize () { - const node = $(this.node); + const resize = () => { + const node = $(nodeRef.current); - this.send('resize', { + send('resize', { width: node.width(), - height: node.height(), + height: node.height(), density: window.devicePixelRatio, }); }; -}); + useEffect(() => { + rebind(); + + return () => { + unbind(); + onPreviewHide(); + + if (worker.current) { + worker.current.terminate(); + }; + }; + }, []); + + useEffect(() => { + send('updateTheme', { theme, colors: J.Theme[theme].graph || {} }); + }, [ theme ]); + + useImperativeHandle(ref, () => ({ + init, + resize, + addNewNode, + })); + + return ( + <div + ref={nodeRef} + className="graphWrapper" + > + <div id={elementId} /> + </div> + ); +})); export default Graph; \ No newline at end of file diff --git a/src/ts/component/header/auth/index.tsx b/src/ts/component/header/auth/index.tsx index 386caae683..2aea053f25 100644 --- a/src/ts/component/header/auth/index.tsx +++ b/src/ts/component/header/auth/index.tsx @@ -1,31 +1,18 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { Icon } from 'Component'; import { S } from 'Lib'; -class HeaderAuthIndex extends React.Component { - - constructor (props: any) { - super(props); +const HeaderAuthIndex = forwardRef(() => { - this.onSettings = this.onSettings.bind(this); - }; - - render () { - return ( - <React.Fragment> - <div className="side left" /> - <div className="side center" /> - <div className="side right"> - <Icon className="settings withBackground" onClick={this.onSettings} /> - </div> - </React.Fragment> - ); - }; - - onSettings () { - S.Popup.open('settingsOnboarding', {}); - }; - -}; + return ( + <> + <div className="side left" /> + <div className="side center" /> + <div className="side right"> + <Icon className="settings withBackground" onClick={() => S.Popup.open('settingsOnboarding', {})} /> + </div> + </> + ); +}); export default HeaderAuthIndex; \ No newline at end of file diff --git a/src/ts/component/header/index.tsx b/src/ts/component/header/index.tsx index d60d6e358a..bd112d7803 100644 --- a/src/ts/component/header/index.tsx +++ b/src/ts/component/header/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect, useImperativeHandle } from 'react'; import { I, S, U, J, Renderer, keyboard, sidebar, Preview, translate } from 'Lib'; import { Icon } from 'Component'; @@ -25,80 +25,70 @@ const Components = { mainEmpty: HeaderMainEmpty, }; -class Header extends React.Component<Props> { - - refChild: any = null; - - constructor (props: Props) { - super(props); - - this.menuOpen = this.menuOpen.bind(this); - this.renderLeftIcons = this.renderLeftIcons.bind(this); - this.renderTabs = this.renderTabs.bind(this); - this.onSearch = this.onSearch.bind(this); - this.onTooltipShow = this.onTooltipShow.bind(this); - this.onTooltipHide = this.onTooltipHide.bind(this); - this.onDoubleClick = this.onDoubleClick.bind(this); - this.onExpand = this.onExpand.bind(this); - this.onRelation = this.onRelation.bind(this); +const Header = forwardRef<{}, Props>((props, ref) => { + + const { + component, + className = '', + withBanner = false, + rootId = '', + tab = '', + tabs = [], + layout = I.ObjectLayout.Page, + isPopup = false, + onTab, + } = props; + + const childRef = useRef(null); + const Component = Components[component] || null; + const cn = [ 'header', component, className ]; + + if (![ 'authIndex' ].includes(component)) { + cn.push('isCommon'); }; - - render () { - const { component, className, withBanner } = this.props; - const Component = Components[component] || null; - const cn = [ 'header', component, className ]; - - if (![ 'authIndex' ].includes(component)) { - cn.push('isCommon'); - }; - - if (withBanner) { - cn.push('withBanner'); - }; - return ( - <div id="header" className={cn.join(' ')} onDoubleClick={this.onDoubleClick}> - {Component ? ( - <Component - ref={ref => this.refChild = ref} - {...this.props} - onSearch={this.onSearch} - onTooltipShow={this.onTooltipShow} - onTooltipHide={this.onTooltipHide} - menuOpen={this.menuOpen} - renderLeftIcons={this.renderLeftIcons} - renderTabs={this.renderTabs} - onRelation={this.onRelation} - /> - ) : ''} - </div> - ); + if (withBanner) { + cn.push('withBanner'); }; - componentDidMount () { - sidebar.resizePage(null, false); + const onGraph = (e: MouseEvent) => { + e.stopPropagation(); + U.Object.openAuto({ id: keyboard.getRootId(), layout: I.ObjectLayout.Graph }); }; - componentDidUpdate () { - sidebar.resizePage(null, false); - this.refChild.forceUpdate(); - }; + const renderLeftIcons = (onOpen?: () => void) => { + const cmd = keyboard.cmdSymbol(); + const alt = keyboard.altSymbol(); + const isWin = U.Common.isPlatformWindows(); + const isLinux = U.Common.isPlatformLinux(); + const cb = isWin || isLinux ? `${alt} + ←` : `${cmd} + [`; + const cf = isWin || isLinux ? `${alt} + →` : `${cmd} + ]`; + + const buttons: any[] = [ + { id: 'expand', name: translate('commonOpenObject'), onClick: onOpen || onExpand }, + { id: 'back', name: translate('commonBack'), caption: cb, onClick: () => keyboard.onBack(), disabled: !keyboard.checkBack() }, + { id: 'forward', name: translate('commonForward'), caption: cf, onClick: () => keyboard.onForward(), disabled: !keyboard.checkForward() }, + { id: 'graph', name: translate('commonGraph'), caption: `${cmd} + ${alt} + O`, onClick: onGraph }, + ]; - renderLeftIcons (onOpen?: () => void) { return ( - <React.Fragment> - <Icon - className="expand withBackground" - tooltip={translate('commonOpenObject')} - onClick={onOpen || this.onExpand} - /> - </React.Fragment> + <> + {buttons.map(item => { + const cn = [ item.id, 'withBackground' ]; + + if (item.disabled) { + cn.push('disabled'); + }; + + return ( + <Icon key={item.id} className={cn.join(' ')} onClick={e => item.onClick(e)} /> + ); + })} + </> ); }; - renderTabs () { - const { tab, tabs, onTab } = this.props; - + const renderTabs = () => { return ( <div id="tabs" className="tabs"> {tabs.map((item: any, i: number) => ( @@ -106,8 +96,8 @@ class Header extends React.Component<Props> { key={i} className={[ 'tab', (item.id == tab ? 'active' : '') ].join(' ')} onClick={() => onTab(item.id)} - onMouseOver={e => this.onTooltipShow(e, item.tooltip, item.tooltipCaption)} - onMouseOut={this.onTooltipHide} + onMouseOver={e => onTooltipShow(e, item.tooltip, item.tooltipCaption)} + onMouseOut={onTooltipHide} > {item.name} </div> @@ -116,37 +106,34 @@ class Header extends React.Component<Props> { ); }; - onExpand () { - const { rootId, layout } = this.props; - + const onExpand = () => { S.Popup.closeAll(null, () => U.Object.openRoute({ id: rootId, layout })); }; - onSearch () { + const onSearch = () => { keyboard.onSearchPopup('Header'); }; - onTooltipShow (e: any, text: string, caption?: string) { + const onTooltipShow = (e: any, text: string, caption?: string) => { const t = Preview.tooltipCaption(text, caption); if (t) { Preview.tooltipShow({ text: t, element: $(e.currentTarget), typeY: I.MenuDirection.Bottom }); }; }; - onTooltipHide () { + const onTooltipHide = () => { Preview.tooltipHide(false); }; - onDoubleClick () { + const onDoubleClick = () => { if (U.Common.isPlatformMac()) { Renderer.send('winCommand', 'maximize'); }; }; - menuOpen (id: string, elementId: string, param: Partial<I.MenuParam>) { - const { isPopup } = this.props; + const menuOpen = (id: string, elementId: string, param: Partial<I.MenuParam>) => { const st = $(window).scrollTop(); - const element = $(`${this.getContainer()} ${elementId}`); + const element = $(`${getContainer()} ${elementId}`); const menuParam: any = Object.assign({ element, offsetY: 4, @@ -160,18 +147,17 @@ class Header extends React.Component<Props> { S.Menu.closeAllForced(null, () => S.Menu.open(id, menuParam)); }; - onRelation (param?: Partial<I.MenuParam>, data?: any) { + const onRelation = (param?: Partial<I.MenuParam>, data?: any) => { param = param || {}; data = data || {}; - const { isPopup, rootId } = this.props; const cnw = [ 'fixed' ]; if (!isPopup) { cnw.push('fromHeader'); }; - this.menuOpen('blockRelationView', '#button-header-relation', { + menuOpen('blockRelationView', '#button-header-relation', { noFlipX: true, noFlipY: true, horizontal: I.MenuDirection.Right, @@ -186,10 +172,50 @@ class Header extends React.Component<Props> { }); }; - getContainer () { - return (this.props.isPopup ? '.popup' : '') + ' .header'; + const getContainer = () => { + return (isPopup ? '.popup' : '') + ' .header'; }; -}; + useEffect(() => { + sidebar.resizePage(null, false); + }); + + useImperativeHandle(ref, () => ({ + setVersion: (version: string) => { + if (childRef.current && childRef.current.setVersion) { + childRef.current.setVersion(version); + }; + }, + + forceUpdate: () => { + if (childRef.current && childRef.current.forceUpdate) { + childRef.current.forceUpdate(); + }; + }, + })); + + return ( + <div + id="header" + className={cn.join(' ')} + onDoubleClick={onDoubleClick} + > + {Component ? ( + <Component + ref={childRef} + {...props} + onSearch={onSearch} + onTooltipShow={onTooltipShow} + onTooltipHide={onTooltipHide} + menuOpen={menuOpen} + renderLeftIcons={renderLeftIcons} + renderTabs={renderTabs} + onRelation={onRelation} + /> + ) : ''} + </div> + ); + +}); export default Header; \ No newline at end of file diff --git a/src/ts/component/header/main/chat.tsx b/src/ts/component/header/main/chat.tsx index 87a68cca4a..4847882eff 100644 --- a/src/ts/component/header/main/chat.tsx +++ b/src/ts/component/header/main/chat.tsx @@ -1,60 +1,23 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; -import { Sync } from 'Component'; -import { I, S, U, J, keyboard } from 'Lib'; - -interface State { - templatesCnt: number; -}; - -const HeaderMainChat = observer(class HeaderMainChat extends React.Component<I.HeaderComponent, State> { - - state = { - templatesCnt: 0 - }; - - constructor (props: I.HeaderComponent) { - super(props); - - this.onSync = this.onSync.bind(this); - this.onOpen = this.onOpen.bind(this); - }; - - render () { - const { rootId, renderLeftIcons } = this.props; - - return ( - <React.Fragment> - <div className="side left"> - {renderLeftIcons(this.onOpen)} - <Sync id="button-header-sync" onClick={this.onSync} /> - </div> - - <div className="side center" /> - <div className="side right" /> - </React.Fragment> - ); - }; - - onOpen () { - const { rootId } = this.props; - const object = S.Detail.get(rootId, rootId, []); +import { I, S, U, keyboard } from 'Lib'; +const HeaderMainChat = observer(forwardRef<{}, I.HeaderComponent>((props, ref) => { + const { rootId, renderLeftIcons } = props; + + const onOpen = () => { keyboard.disableClose(true); - S.Popup.closeAll(null, () => U.Object.openRoute(object)); + S.Popup.closeAll(null, () => U.Object.openRoute(S.Detail.get(rootId, rootId, []))); }; - onSync () { - const { rootId, menuOpen } = this.props; + return ( + <> + <div className="side left">{renderLeftIcons(onOpen)}</div> + <div className="side center" /> + <div className="side right" /> + </> + ); - menuOpen('syncStatus', '#button-header-sync', { - subIds: [ 'syncStatusInfo' ], - data: { - rootId, - } - }); - }; - -}); +})); export default HeaderMainChat; \ No newline at end of file diff --git a/src/ts/component/header/main/empty.tsx b/src/ts/component/header/main/empty.tsx index 620255503c..598151e258 100644 --- a/src/ts/component/header/main/empty.tsx +++ b/src/ts/component/header/main/empty.tsx @@ -1,18 +1,18 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { I } from 'Lib'; -class HeaderMainEmpty extends React.Component<I.HeaderComponent> { +const HeaderMainEmpty = forwardRef<{}, I.HeaderComponent>((props, ref) => { + + const { renderLeftIcons } = props; - render () { - return ( - <React.Fragment> - <div className="side left">{this.props.renderLeftIcons()}</div> - <div className="side center" /> - <div className="side right" /> - </React.Fragment> - ); - }; + return ( + <> + <div className="side left">{renderLeftIcons()}</div> + <div className="side center" /> + <div className="side right" /> + </> + ); -}; +}); export default HeaderMainEmpty; \ No newline at end of file diff --git a/src/ts/component/header/main/graph.tsx b/src/ts/component/header/main/graph.tsx index 459a58830d..413fc73b7e 100644 --- a/src/ts/component/header/main/graph.tsx +++ b/src/ts/component/header/main/graph.tsx @@ -1,48 +1,32 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect, useImperativeHandle } from 'react'; import { Icon } from 'Component'; import { I, S, U, J, translate } from 'Lib'; -class HeaderMainGraph extends React.Component<I.HeaderComponent> { +const HeaderMainGraph = forwardRef<{}, I.HeaderComponent>((props, ref) => { - refFilter: any = null; - rootId = ''; + const { renderLeftIcons, renderTabs, menuOpen, rootId } = props; + const rootIdRef = useRef(''); - constructor (props: I.HeaderComponent) { - super(props); - - this.onSearch = this.onSearch.bind(this); - this.onFilter = this.onFilter.bind(this); - this.onSettings = this.onSettings.bind(this); + const unbind = () => { + $(window).off(`updateGraphRoot.header`); }; - render () { - const { renderLeftIcons, renderTabs } = this.props; - - return ( - <React.Fragment> - <div className="side left">{renderLeftIcons()}</div> - <div className="side center">{renderTabs()}</div> - - <div className="side right"> - <Icon id="button-header-search" className="btn-search withBackground" tooltip={translate('headerGraphTooltipSearch')} onClick={this.onSearch} /> - <Icon id="button-header-filter" className="btn-filter withBackground dn" tooltip={translate('headerGraphTooltipFilters')} onClick={this.onFilter} /> - <Icon id="button-header-settings" className="btn-settings withBackground" tooltip={translate('headerGraphTooltipSettings')} onClick={this.onSettings} /> - </div> - </React.Fragment> - ); - }; + const rebind = () => { + const win = $(window); - componentDidMount(): void { - this.setRootId(this.props.rootId); + unbind(); + win.on('updateGraphRoot.header', (e: any, data: any) => initRootId(data.id)); }; - onSearch () { - this.props.menuOpen('searchObject', '#button-header-search', { + const onSearch = () => { + const rootId = rootIdRef.current; + + menuOpen('searchObject', '#button-header-search', { horizontal: I.MenuDirection.Right, data: { - rootId: this.rootId, - blockId: this.rootId, - blockIds: [ this.rootId ], + rootId, + blockId: rootId, + blockIds: [ rootId ], filters: U.Data.graphFilters(), filter: S.Common.getGraph(J.Constant.graphId.global).filter, canAdd: true, @@ -56,11 +40,11 @@ class HeaderMainGraph extends React.Component<I.HeaderComponent> { }); }; - onFilter () { + const onFilter = () => { }; - onSettings () { - this.props.menuOpen('graphSettings', '#button-header-settings', { + const onSettings = () => { + menuOpen('graphSettings', '#button-header-settings', { horizontal: I.MenuDirection.Right, data: { allowLocal: true, @@ -69,10 +53,30 @@ class HeaderMainGraph extends React.Component<I.HeaderComponent> { }); }; - setRootId (id: string) { - this.rootId = id; + const initRootId = (id: string) => { + rootIdRef.current = id; }; -}; + useEffect(() => { + initRootId(rootId) + rebind(); + + return () => unbind(); + }, []); + + return ( + <> + <div className="side left">{renderLeftIcons()}</div> + <div className="side center">{renderTabs()}</div> + + <div className="side right"> + <Icon id="button-header-search" className="btn-search withBackground" tooltip={translate('headerGraphTooltipSearch')} onClick={onSearch} /> + <Icon id="button-header-filter" className="btn-filter withBackground dn" tooltip={translate('headerGraphTooltipFilters')} onClick={onFilter} /> + <Icon id="button-header-settings" className="btn-settings withBackground" tooltip={translate('headerGraphTooltipSettings')} onClick={onSettings} /> + </div> + </> + ); + +}); export default HeaderMainGraph; \ No newline at end of file diff --git a/src/ts/component/header/main/history.tsx b/src/ts/component/header/main/history.tsx index a418246096..c44b0f0f1b 100644 --- a/src/ts/component/header/main/history.tsx +++ b/src/ts/component/header/main/history.tsx @@ -1,64 +1,49 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useImperativeHandle } from 'react'; import { observer } from 'mobx-react'; import { Icon } from 'Component'; -import { I, S, U, keyboard } from 'Lib'; +import { I, S, U, keyboard, translate } from 'Lib'; -interface State { - version: I.HistoryVersion; +interface HeaderMainHistoryRefProps { + setVersion: (version: I.HistoryVersion) => void; }; -const HeaderMainHistory = observer(class HeaderMainHistory extends React.Component<I.HeaderComponent, State> { - - state = { - version: null, - }; - - constructor (props: I.HeaderComponent) { - super(props); - - this.onBack = this.onBack.bind(this); - this.onRelation = this.onRelation.bind(this); - }; - - render () { - const { rootId, renderLeftIcons } = this.props; - const { version } = this.state; - const cmd = keyboard.cmdSymbol(); - const object = S.Detail.get(rootId, rootId, []); - const showMenu = !U.Object.isTypeOrRelationLayout(object.layout); - - return ( - <React.Fragment> - <div className="side left">{renderLeftIcons()}</div> - - <div className="side center"> - <div className="txt"> - {version ? U.Date.date('M d, Y g:i:s A', version.time) : ''} - </div> +const HeaderMainHistory = observer(forwardRef<HeaderMainHistoryRefProps, I.HeaderComponent>((props, ref) => { + const { rootId, renderLeftIcons, onRelation } = props; + const [ version, setVersion ] = useState<I.HistoryVersion | null>(null); + const [ dummyState, setDummyState ] = useState(0); + const cmd = keyboard.cmdSymbol(); + const object = S.Detail.get(rootId, rootId, []); + const showMenu = !U.Object.isTypeOrRelationLayout(object.layout); + + useImperativeHandle(ref, () => ({ + setVersion, + forceUpdate: () => setDummyState(dummyState + 1), + })); + + return ( + <React.Fragment> + <div className="side left">{renderLeftIcons()}</div> + + <div className="side center"> + <div className="txt"> + {version ? U.Date.date('M d, Y g:i:s A', version.time) : ''} </div> - - <div className="side right"> - {showMenu ? <Icon id="button-header-relation" tooltip="Relations" tooltipCaption={`${cmd} + Shift + R`} className="relation withBackground" onClick={this.onRelation} /> : ''} - </div> - </React.Fragment> - ); - }; - - onBack (e: any) { - const { rootId } = this.props; - const object = S.Detail.get(rootId, rootId, []); - - U.Object.openEvent(e, object); - }; - - onRelation () { - this.props.onRelation({}, { readonly: true }); - }; - - setVersion (version: I.HistoryVersion) { - this.setState({ version }); - }; - -}); + </div> + + <div className="side right"> + {showMenu ? ( + <Icon + id="button-header-relation" + tooltip={translate('commonRelations')} + tooltipCaption={`${cmd} + Shift + R`} + className="relation withBackground" + onClick={() => onRelation({}, { readonly: true })} + /> + ) : ''} + </div> + </React.Fragment> + ); + +})); export default HeaderMainHistory; \ No newline at end of file diff --git a/src/ts/component/header/main/navigation.tsx b/src/ts/component/header/main/navigation.tsx index 79a56e3b5c..be5bd62474 100644 --- a/src/ts/component/header/main/navigation.tsx +++ b/src/ts/component/header/main/navigation.tsx @@ -1,20 +1,18 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { I } from 'Lib'; -class HeaderMainNavigation extends React.Component<I.HeaderComponent> { +const HeaderMainNavigation = forwardRef<{}, I.HeaderComponent>((props, ref) => { + + const { renderLeftIcons, renderTabs } = props; - render () { - const { renderLeftIcons, renderTabs } = this.props; + return ( + <> + <div className="side left">{renderLeftIcons()}</div> + <div className="side center">{renderTabs()}</div> + <div className="side right" /> + </> + ); - return ( - <React.Fragment> - <div className="side left">{renderLeftIcons()}</div> - <div className="side center">{renderTabs()}</div> - <div className="side right" /> - </React.Fragment> - ); - }; - -}; +}); export default HeaderMainNavigation; \ No newline at end of file diff --git a/src/ts/component/header/main/object.tsx b/src/ts/component/header/main/object.tsx index 730d4363a5..ea5a10c365 100644 --- a/src/ts/component/header/main/object.tsx +++ b/src/ts/component/header/main/object.tsx @@ -1,69 +1,51 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useEffect, useImperativeHandle } from 'react'; import { observer } from 'mobx-react'; -import { Icon, IconObject, Sync, ObjectName, Label } from 'Component'; +import { Button, Icon, IconObject, ObjectName, Label } from 'Component'; import { I, S, U, J, keyboard, translate } from 'Lib'; import HeaderBanner from 'Component/page/elements/head/banner'; -interface State { - templatesCnt: number; -}; - -const HeaderMainObject = observer(class HeaderMainObject extends React.Component<I.HeaderComponent, State> { - - state = { - templatesCnt: 0 +const HeaderMainObject = observer(forwardRef<{}, I.HeaderComponent>((props, ref) => { + + const { config } = S.Common; + const { rootId, match, isPopup, onSearch, onTooltipShow, onTooltipHide, renderLeftIcons, onRelation, menuOpen } = props; + const [ templatesCnt, setTemplateCnt ] = useState(0); + const [ dummy, setDummy ] = useState(0); + const root = S.Block.getLeaf(rootId, rootId); + const object = S.Detail.get(rootId, rootId, J.Relation.template); + const isDeleted = object._empty_ || object.isDeleted; + const isLocked = root ? root.isLocked() : false; + const isTypeOrRelation = U.Object.isTypeOrRelationLayout(object.layout); + const isDate = U.Object.isDateLayout(object.layout); + const showShare = !isTypeOrRelation && !isDate && config.experimental && !isDeleted; + const showRelations = !isTypeOrRelation && !isDate && !isDeleted; + const showMenu = !isTypeOrRelation && !isDeleted; + const cmd = keyboard.cmdSymbol(); + const allowedTemplateSelect = (object.internalFlags || []).includes(I.ObjectFlag.SelectTemplate); + const bannerProps = { type: I.BannerType.None, isPopup, object, count: 0 }; + + let center = null; + let locked = ''; + + if (object.isArchived) { + bannerProps.type = I.BannerType.IsArchived; + } else + if (U.Object.isTemplate(object.type)) { + bannerProps.type = I.BannerType.IsTemplate; + } else + if (allowedTemplateSelect && templatesCnt) { + bannerProps.type = I.BannerType.TemplateSelect; + bannerProps.count = templatesCnt + 1; }; - constructor (props: I.HeaderComponent) { - super(props); - - this.onRelation = this.onRelation.bind(this); - this.onMore = this.onMore.bind(this); - this.onSync = this.onSync.bind(this); - this.onOpen = this.onOpen.bind(this); - this.updateTemplatesCnt = this.updateTemplatesCnt.bind(this); + if (isLocked) { + locked = translate('headerObjectLocked'); + } else + if (U.Object.isTypeOrRelationLayout(object.layout) && !S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Delete ])) { + locked = translate('commonSystem'); }; - render () { - const { rootId, onSearch, onTooltipShow, onTooltipHide, isPopup, renderLeftIcons } = this.props; - const { templatesCnt } = this.state; - const root = S.Block.getLeaf(rootId, rootId); - - if (!root) { - return null; - }; - - const object = S.Detail.get(rootId, rootId, J.Relation.template); - const isLocked = root ? root.isLocked() : false; - const showMenu = !U.Object.isTypeOrRelationLayout(object.layout); - const canSync = showMenu && !object.templateIsBundled && !U.Object.isParticipantLayout(object.layout); - const cmd = keyboard.cmdSymbol(); - const allowedTemplateSelect = (object.internalFlags || []).includes(I.ObjectFlag.SelectTemplate); - const bannerProps: any = {}; - - let center = null; - let banner = I.BannerType.None; - let locked = ''; - - if (object.isArchived && U.Space.canMyParticipantWrite()) { - banner = I.BannerType.IsArchived; - } else - if (U.Object.isTemplate(object.type)) { - banner = I.BannerType.IsTemplate; - } else - if (allowedTemplateSelect && templatesCnt) { - banner = I.BannerType.TemplateSelect; - bannerProps.count = templatesCnt + 1; - }; - - if (isLocked) { - locked = translate('headerObjectLocked'); - } else - if (U.Object.isTypeOrRelationLayout(object.layout) && !S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Delete ])) { - locked = translate('commonSystem'); - }; - - if (banner == I.BannerType.None) { + if (!isDeleted) { + if (bannerProps.type == I.BannerType.None) { center = ( <div id="path" @@ -80,51 +62,18 @@ const HeaderMainObject = observer(class HeaderMainObject extends React.Component </div> ); } else { - center = <HeaderBanner type={banner} object={object} isPopup={isPopup} {...bannerProps} />; + center = <HeaderBanner {...bannerProps} />; }; - - return ( - <React.Fragment> - <div className="side left"> - {renderLeftIcons(this.onOpen)} - {canSync ? <Sync id="button-header-sync" onClick={this.onSync} /> : ''} - </div> - - <div className="side center"> - {center} - </div> - - <div className="side right"> - {showMenu ? <Icon id="button-header-relation" tooltip="Relations" tooltipCaption={`${cmd} + Shift + R`} className="relation withBackground" onClick={this.onRelation} /> : ''} - {showMenu ? <Icon id="button-header-more" tooltip="Menu" className="more withBackground" onClick={this.onMore} /> : ''} - </div> - </React.Fragment> - ); - }; - - componentDidMount () { - this.init(); - }; - - componentDidUpdate () { - this.init(); }; - init () { - this.updateTemplatesCnt(); - }; - - onOpen () { - const { rootId } = this.props; + const onOpen = () => { const object = S.Detail.get(rootId, rootId, []); keyboard.disableClose(true); S.Popup.closeAll(null, () => U.Object.openRoute(object)); }; - onMore () { - const { isPopup, match, rootId, menuOpen } = this.props; - + const onMore = () => { menuOpen('object', '#button-header-more', { horizontal: I.MenuDirection.Right, subIds: J.Menu.object, @@ -138,27 +87,22 @@ const HeaderMainObject = observer(class HeaderMainObject extends React.Component }); }; - onSync () { - const { rootId, menuOpen } = this.props; - - menuOpen('syncStatus', '#button-header-sync', { - subIds: [ 'syncStatusInfo' ], + const onShare = () => { + menuOpen('publish', '#button-header-share', { + horizontal: I.MenuDirection.Right, data: { rootId, } }); }; - onRelation () { - const { rootId } = this.props; + const onRelationHandler = () => { const object = S.Detail.get(rootId, rootId, [ 'isArchived' ]); - this.props.onRelation({}, { readonly: object.isArchived }); + onRelation({}, { readonly: object.isArchived }); }; - updateTemplatesCnt () { - const { rootId } = this.props; - const { templatesCnt } = this.state; + const updateTemplatesCnt = () => { const object = S.Detail.get(rootId, rootId, [ 'internalFlags' ]); const allowedTemplateSelect = (object.internalFlags || []).includes(I.ObjectFlag.SelectTemplate); @@ -172,11 +116,62 @@ const HeaderMainObject = observer(class HeaderMainObject extends React.Component }; if (message.records.length != templatesCnt) { - this.setState({ templatesCnt: message.records.length }); + setTemplateCnt(message.records.length); }; }); }; -}); - -export default HeaderMainObject; + useEffect(() => { + updateTemplatesCnt(); + }); + + useImperativeHandle(ref, () => ({ + forceUpdate: () => setDummy(dummy + 1), + })); + + return ( + <> + <div className="side left"> + {renderLeftIcons(onOpen)} + </div> + + <div className="side center"> + {center} + </div> + + <div className="side right"> + {showShare ? ( + <Button + id="button-header-share" + text="Share" + color="blank" + className="c28" + onClick={onShare} + /> + ) : ''} + + {showRelations ? ( + <Icon + id="button-header-relation" + tooltip={translate('commonRelations')} + tooltipCaption={`${cmd} + Shift + R`} + className="relation withBackground" + onClick={onRelationHandler} + /> + ) : ''} + + {showMenu ? ( + <Icon + id="button-header-more" + tooltip={translate('commonMenu')} + className="more withBackground" + onClick={onMore} + /> + ) : ''} + </div> + </> + ); + +})); + +export default HeaderMainObject; \ No newline at end of file diff --git a/src/ts/component/index.tsx b/src/ts/component/index.tsx index f91b9b802e..1549f0623e 100644 --- a/src/ts/component/index.tsx +++ b/src/ts/component/index.tsx @@ -17,9 +17,9 @@ import ListPopup from './list/popup'; import ListMenu from './list/menu'; import ListNotification from './list/notification'; import ListChildren from './list/children'; -import ListObjectPreview from './list/previewObject'; +import ListPreviewObject from './list/previewObject'; import ListObject from './list/object'; -import ListObjectManager from './list/objectManager'; +import ListManager from './list/objectManager'; import Header from './header'; import Footer from './footer'; @@ -58,6 +58,9 @@ import DragVertical from './form/drag/vertical'; import Pin from './form/pin'; import Filter from './form/filter'; import Phrase from './form/phrase'; +import EmailCollection from './form/emailCollection'; +import HeadSimple from './page/elements/head/simple'; +import EditorControls from './page/elements/head/controls'; import Pager from './util/pager'; import Dimmer from './util/dimmer'; @@ -66,7 +69,6 @@ import Toast from './util/toast'; import Marker from './util/marker'; import Sync from './util/sync'; import LoadMore from './util/loadMore'; -import Navigation from './util/navigation'; import Icon from './util/icon'; import IconObject from './util/iconObject'; @@ -94,7 +96,8 @@ import ShareTooltip from './util/share/tooltip'; import ShareBanner from './util/share/banner'; import FooterAuthDisclaimer from './footer/auth/disclaimer'; -import EmailCollectionForm from './util/emailCollectionForm'; +import Floater from './util/floater'; +import QR from './util/qr'; export { Page, @@ -114,9 +117,9 @@ export { ListPopup, ListMenu, ListChildren, - ListObjectPreview, + ListPreviewObject, ListObject, - ListObjectManager, + ListManager, ListNotification, Header, @@ -153,7 +156,6 @@ export { Title, Label, Error, - Navigation, Notification, Icon, @@ -189,5 +191,10 @@ export { ShareBanner, FooterAuthDisclaimer, - EmailCollectionForm, + EmailCollection, + Floater, + HeadSimple, + EditorControls, + + QR, }; diff --git a/src/ts/component/list/children.tsx b/src/ts/component/list/children.tsx index 1a579faecc..c81cbb2b3d 100644 --- a/src/ts/component/list/children.tsx +++ b/src/ts/component/list/children.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { Block } from 'Component'; import { observer } from 'mobx-react'; import { DropTarget } from 'Component'; @@ -10,108 +10,107 @@ interface Props extends I.BlockComponent { onResizeStart? (e: any, index: number): void; }; -const ListChildren = observer(class ListChildren extends React.Component<Props> { - - refObj: any = {}; +const ListChildren = observer(forwardRef<{}, Props>((props, ref) => { + const { + rootId = '', + block, + index = 0, + readonly = false, + className = '', + onMouseMove, + onMouseLeave, + onResizeStart, + } = props; - constructor (props: Props) { - super(props); - - this.onEmptyToggle = this.onEmptyToggle.bind(this); - }; - - render () { - const { onMouseMove, onMouseLeave, onResizeStart, rootId, block, index, readonly } = this.props; - const childrenIds = S.Block.getChildrenIds(rootId, block.id); - const children = S.Block.getChildren(rootId, block.id); - const length = childrenIds.length; + const childrenIds = S.Block.getChildrenIds(rootId, block.id); + const children = S.Block.getChildren(rootId, block.id); + const length = childrenIds.length; - if (!length) { - if (block.isTextToggle() && !readonly) { - return ( - <DropTarget - {...this.props} - className="emptyToggle" - rootId={rootId} - id={block.id} - style={block.content.style} - type={block.type} - dropType={I.DropType.Block} - canDropMiddle={true} - onClick={this.onEmptyToggle} - cacheKey="emptyToggle" - > - {translate('blockTextToggleEmpty')} - </DropTarget> - ); - } else { - return null; - }; - }; - - const className = String(this.props.className || '').replace(/first|last/g, ''); - const cn = [ 'children', (block.isTextToggle() ? 'canToggle' : '') ]; - const isRow = block.isLayoutRow(); + const onEmptyToggle = (e: any) => { + C.BlockCreate(rootId, block.id, I.BlockPosition.Inner, { type: I.BlockType.Text }, (message: any) => { + focus.set(message.blockId, { from: 0, to: 0 }); + focus.apply(); + }); + }; - let ColResize: any = (): any => null; - - if (isRow) { - ColResize = (item: any) => ( - <div className={[ 'colResize', 'c' + item.index ].join(' ')} onMouseDown={e => onResizeStart(e, item.index)}> - <div className="inner"> - <div className="line" /> - </div> - </div> + if (!length) { + if (block.isTextToggle() && !readonly) { + return ( + <DropTarget + {...props} + className="emptyToggle" + rootId={rootId} + id={block.id} + style={block.content.style} + type={block.type} + dropType={I.DropType.Block} + canDropMiddle={true} + onClick={onEmptyToggle} + cacheKey="emptyToggle" + > + {translate('blockTextToggleEmpty')} + </DropTarget> ); + } else { + return null; }; + }; + + const c = String(className || '').replace(/first|last/g, ''); + const cn = [ 'children', (block.isTextToggle() ? 'canToggle' : '') ]; + const isRow = block.isLayoutRow(); - return ( - <div id={`block-children-${block.id}`} className={cn.join(' ')} onMouseMove={onMouseMove} onMouseLeave={onMouseLeave}> - {children.map((item: any, i: number) => { - const css: any = {}; - const cn = []; - - if (isRow) { - css.width = (item.fields.width || 1 / length ) * 100 + '%'; - }; - - if (className) { - cn.push(className); - }; - if (i == 0) { - cn.push('first'); - }; - if (i == length - 1) { - cn.push('last'); - }; - - return ( - <React.Fragment key={`block-child-${item.id}`}> - {(i > 0) && isRow ? <ColResize index={i} /> : ''} - <Block - key={`block-${item.id}`} - {...this.props} - block={item} - css={css} - className={cn.join(' ')} - index={index + '-' + i} - /> - </React.Fragment> - ); - })} + let ColResize: any = (): any => null; + + if (isRow) { + ColResize = (item: any) => ( + <div className={[ 'colResize', 'c' + item.index ].join(' ')} onMouseDown={e => onResizeStart(e, item.index)}> + <div className="inner"> + <div className="line" /> + </div> </div> ); }; - onEmptyToggle (e: any) { - const { rootId, block } = this.props; + return ( + <div + id={`block-children-${block.id}`} + className={cn.join(' ')} + onMouseMove={onMouseMove} + onMouseLeave={onMouseLeave} + > + {children.map((item: any, i: number) => { + const css: any = {}; + const cn = [ c ]; - C.BlockCreate(rootId, block.id, I.BlockPosition.Inner, { type: I.BlockType.Text }, (message: any) => { - focus.set(message.blockId, { from: 0, to: 0 }); - focus.apply(); - }); - }; - -}); + if (isRow) { + css.width = (item.fields.width || 1 / length ) * 100 + '%'; + }; + + if (i == 0) { + cn.push('first'); + }; + if (i == length - 1) { + cn.push('last'); + }; + + return ( + <React.Fragment key={`block-child-${item.id}`}> + {(i > 0) && isRow ? <ColResize index={i} /> : ''} + <Block + key={`block-${item.id}`} + {...props} + block={item} + css={css} + className={cn.join(' ')} + index={index + '-' + i} + /> + </React.Fragment> + ); + })} + </div> + ); + +})); export default ListChildren; \ No newline at end of file diff --git a/src/ts/component/list/menu.tsx b/src/ts/component/list/menu.tsx index f36e36808d..e65afbe6ef 100644 --- a/src/ts/component/list/menu.tsx +++ b/src/ts/component/list/menu.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { Menu } from 'Component'; import { observer } from 'mobx-react'; import { I, S } from 'Lib'; @@ -7,21 +7,17 @@ interface Props { history: any; }; -const ListMenu = observer(class ListMenu extends React.Component<Props> { +const ListMenu: FC<Props> = observer(() => { + const { list } = S.Menu; - render () { - const { list } = S.Menu; - - return ( - <div className="menus"> - {list.map((item: I.Menu, i: number) => ( - <Menu {...this.props} key={item.id} {...item} /> - ))} - <div id="menu-polygon" /> - </div> - ); - }; - + return ( + <div className="menus"> + {list.map((item: I.Menu) => ( + <Menu key={item.id} {...item} /> + ))} + <div id="menu-polygon" /> + </div> + ); }); export default ListMenu; \ No newline at end of file diff --git a/src/ts/component/list/notification.tsx b/src/ts/component/list/notification.tsx index b00d56112c..750621f071 100644 --- a/src/ts/component/list/notification.tsx +++ b/src/ts/component/list/notification.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect } from 'react'; import $ from 'jquery'; import { Notification, Icon } from 'Component'; import { observer } from 'mobx-react'; @@ -6,93 +6,43 @@ import { I, C, S } from 'Lib'; const LIMIT = 5; -const ListNotification = observer(class ListNotification extends React.Component { +const ListNotification = observer(forwardRef(() => { - node = null; - isExpanded = false; + const nodeRef = useRef(null); + const { list } = S.Notification; + const isExpanded = useRef(false); - constructor (props: any) { - super(props); - - this.onShow = this.onShow.bind(this); - this.onHide = this.onHide.bind(this); - this.onClear = this.onClear.bind(this); - this.resize = this.resize.bind(this); - }; - - render () { - const { list } = S.Notification; - - return ( - <div - id="notifications" - ref={node => this.node = node} - className="notifications" - onClick={this.onShow} - > - {list.length ? ( - <div className="head"> - <Icon className="hide" onClick={this.onHide} /> - <Icon className="clear" onClick={this.onClear} /> - </div> - ) : ''} - - <div className="body"> - {list.slice(0, LIMIT).map((item: I.Notification, i: number) => ( - <Notification - {...this.props} - item={item} - key={item.id} - style={{ zIndex: LIMIT - i }} - resize={this.resize} - /> - ))} - </div> - </div> - ); - }; - - componentDidMount (): void { - this.resize(); - }; - - componentDidUpdate (): void { - this.resize(); - }; - - onShow (e: any) { + const onShow = (e: any) => { e.stopPropagation(); - if (this.isExpanded) { + if (isExpanded.current) { return; }; - $(this.node).addClass('isExpanded'); - - this.isExpanded = true; - this.resize(); + $(nodeRef.current).addClass('isExpanded'); + isExpanded.current = true; + resize(); }; - onHide (e: any) { + const onHide = (e: any) => { e.stopPropagation(); - if (!this.isExpanded) { + if (!isExpanded.current) { return; }; - $(this.node).removeClass('isExpanded'); - - this.isExpanded = false; - this.resize(); + $(nodeRef.current).removeClass('isExpanded'); + isExpanded.current = false; + resize(); }; - onClear () { + const onClear = () => { C.NotificationReply(S.Notification.list.map(it => it.id), I.NotificationAction.Close); S.Notification.clear(); }; - resize () { - const node = $(this.node); + const resize = () => { + const node = $(nodeRef.current); const items = node.find('.notification'); let listHeight = 0; @@ -105,8 +55,8 @@ const ListNotification = observer(class ListNotification extends React.Component item = $(item); item.css({ - width: (this.isExpanded ? '100%' : `calc(100% - ${4 * i * 2}px)`), - right: (this.isExpanded ? 0 : 4 * i), + width: (isExpanded.current ? '100%' : `calc(100% - ${4 * i * 2}px)`), + right: (isExpanded.current ? 0 : 4 * i), }); const h = item.outerHeight(); @@ -115,7 +65,7 @@ const ListNotification = observer(class ListNotification extends React.Component firstHeight = h; }; - if (!this.isExpanded) { + if (!isExpanded.current) { if (i > 0) { bottom = firstHeight + 4 * i - h; }; @@ -130,7 +80,7 @@ const ListNotification = observer(class ListNotification extends React.Component height = h; }); - if (!this.isExpanded) { + if (!isExpanded.current) { listHeight = firstHeight + 4 * items.length; } else if (items.length) { @@ -140,7 +90,36 @@ const ListNotification = observer(class ListNotification extends React.Component node.css({ height: listHeight }); }, 50); }; - -}); + + useEffect(() => resize(), [ list ]); + + return ( + <div + id="notifications" + ref={nodeRef} + className="notifications" + onClick={onShow} + > + {list.length ? ( + <div className="head"> + <Icon className="hide" onClick={onHide} /> + <Icon className="clear" onClick={onClear} /> + </div> + ) : ''} + + <div className="body"> + {list.slice(0, LIMIT).map((item: I.Notification, i: number) => ( + <Notification + item={item} + key={item.id} + style={{ zIndex: LIMIT - i }} + resize={resize} + /> + ))} + </div> + </div> + ); + +})); export default ListNotification; \ No newline at end of file diff --git a/src/ts/component/list/object.tsx b/src/ts/component/list/object.tsx index 40d5beb313..3390b0f4fb 100644 --- a/src/ts/component/list/object.tsx +++ b/src/ts/component/list/object.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useImperativeHandle, useEffect } from 'react'; import { observer } from 'mobx-react'; import { I, C, S, U, J, Relation, translate, keyboard, analytics } from 'Lib'; import { Icon, IconObject, Pager, ObjectName, Cell, SelectionTarget } from 'Component'; @@ -23,207 +23,46 @@ interface Props { route: string; }; -interface State { - sortId: string; - sortType: I.SortType; +interface ListObjectRefProps { + getData: (page: number, callBack?: (message: any) => void) => void; }; const PREFIX = 'listObject'; -const LIMIT = 50; - -const ListObject = observer(class ListObject extends React.Component<Props, State> { - - public static defaultProps: Props = { - spaceId: '', - subId: '', - rootId: '', - columns: [], - sources: [], - filters: [], - route: '', - }; - state = { - sortId: '', - sortType: I.SortType.Desc, +const ListObject = observer(forwardRef<ListObjectRefProps, Props>(({ + spaceId = '', + subId = '', + rootId = '', + columns = [], + sources = [], + filters = [], + relationKeys = [], + route = '', +}, ref) => { + + const [ sortId, setSortId ] = React.useState(''); + const [ sortType, setSortType ] = React.useState(I.SortType.Asc); + const { offset, total } = S.Record.getMeta(subId, ''); + const { dateFormat } = S.Common; + + const getColumns = (): Column[] => { + return ([ { relationKey: 'name', name: translate('commonName'), isObject: true } ] as any[]).concat(columns || []); }; - render () { - const { sortId, sortType } = this.state; - const { subId, rootId } = this.props; - const columns = this.getColumns(); - const items = this.getItems(); - const { offset, total } = S.Record.getMeta(subId, ''); - - let pager = null; - if (total && items.length) { - pager = ( - <Pager - offset={offset} - limit={LIMIT} - total={total} - onChange={page => this.getData(page)} - /> - ); - }; - - const Row = (item: any) => { - const cn = [ 'row' ]; - - if (U.Object.isTaskLayout(item.layout) && item.isDone) { - cn.push('isDone'); - }; - if (item.isArchived) { - cn.push('isArchived'); - }; - if (item.isDeleted) { - cn.push('isDeleted'); - }; - if (item.isHidden) { - cn.push('isHidden'); - }; - - return ( - <SelectionTarget - id={item.id} - type={I.SelectType.Record} - className={cn.join(' ')} - onContextMenu={e => this.onContext(e, item.id)} - > - {columns.map(column => { - const cn = [ 'cell', `c-${column.relationKey}` ]; - const cnc = [ 'cellContent' ]; - const value = item[column.relationKey]; - - if (column.className) { - cnc.push(column.className); - }; - - let content = null; - let onClick = null; - - if (value) { - if (column.isObject) { - let object = null; - - if (column.relationKey == 'name') { - object = item; - cn.push('isName'); - cnc.push('isName'); - } else { - object = S.Detail.get(subId, value, []); - }; - - if (!object._empty_) { - onClick = () => U.Object.openConfig(object); - content = ( - <div className="flex"> - <IconObject object={object} /> - <ObjectName object={object} /> - </div> - ); - }; - } else - if (column.isCell) { - content = ( - <Cell - elementId={Relation.cellId(PREFIX, column.relationKey, item.id)} - rootId={rootId} - subId={subId} - block={null} - relationKey={column.relationKey} - getRecord={() => item} - viewType={I.ViewType.Grid} - idPrefix={PREFIX} - iconSize={20} - readonly={true} - arrayLimit={2} - textLimit={150} - /> - ); - } else { - content = column.mapper ? column.mapper(value) : value; - }; - }; - - return ( - <div key={`cell-${column.relationKey}`} className={cn.join(' ')}> - {content ? <div className={cnc.join(' ')} onClick={onClick}>{content}</div> : ''} - </div> - ); - })} - </SelectionTarget> - ); - }; - - return ( - <div className="listObject"> - <div className="table"> - <div className="row isHead"> - {columns.map(column => { - let arrow = null; - - if (sortId == column.relationKey) { - arrow = <Icon className={`sortArrow c${sortType}`} />; - }; - - return ( - <div key={`head-${column.relationKey}`} className="cell isHead" onClick={() => this.onSort(column.relationKey)}> - <div className="name">{column.name}{arrow}</div> - </div> - ); - })} - </div> - - {!items.length ? ( - <div className="row"> - <div className="cell empty">{translate('commonNoObjects')}</div> - </div> - ) : ( - <React.Fragment> - {items.map((item: any, i: number) => ( - <Row key={i} {...item} /> - ))} - </React.Fragment> - )} - </div> - - {pager} - </div> - ); + const getKeys = () => { + return J.Relation.default.concat(getColumns().map(it => it.relationKey)).concat(relationKeys || []); }; - componentDidMount () { - const columns = this.getColumns(); - - if (columns.length) { - this.setState({ sortId: columns[0].relationKey }, () => this.getData(1)); - }; + const getItems = () => { + return S.Record.getRecords(subId, getKeys()); }; - componentWillUnmount(): void { - C.ObjectSearchUnsubscribe([ this.props.subId ]); - }; - - getItems () { - return S.Record.getRecords(this.props.subId, this.getKeys()); - }; - - getKeys () { - return J.Relation.default.concat(this.props.columns.map(it => it.relationKey)); - }; - - getColumns (): Column[] { - return ([ { relationKey: 'name', name: translate('commonName'), isObject: true } ] as any[]).concat(this.props.columns || []); - }; - - getData (page: number, callBack?: (message: any) => void) { - const { sortId, sortType } = this.state; - const { spaceId, subId, sources } = this.props; - const offset = (page - 1) * LIMIT; - const filters = [ + const getData = (page: number, callBack?: (message: any) => void) => { + const limit = J.Constant.limit.listObject + const offset = (page - 1) * limit; + const fl = [ { relationKey: 'layout', condition: I.FilterCondition.NotIn, value: U.Object.excludeFromSet() }, - ].concat(this.props.filters || []); + ].concat(filters || []); S.Record.metaSet(subId, '', { offset }); @@ -231,24 +70,23 @@ const ListObject = observer(class ListObject extends React.Component<Props, Stat spaceId, subId, sorts: [ { relationKey: sortId, type: sortType } ], - keys: this.getKeys(), + keys: getKeys(), sources, - filters, + filters: fl, offset, - limit: LIMIT, + limit, ignoreHidden: true, ignoreDeleted: true, }, callBack); }; - onContext (e: any, id: string): void { + const onContext = (e: any, id: string): void => { e.preventDefault(); e.stopPropagation(); - const { subId, relationKeys } = this.props; const selection = S.Common.getRef('selectionProvider'); - let objectIds = selection ? selection.get(I.SelectType.Record) : []; + let objectIds = selection?.get(I.SelectType.Record) || []; if (!objectIds.length) { objectIds = [ id ]; }; @@ -261,27 +99,180 @@ const ListObject = observer(class ListObject extends React.Component<Props, Stat data: { objectIds, subId, - relationKeys, + relationKeys: getKeys(), allowedLinkTo: true, allowedOpen: true, } }); }; - onSort (relationKey: string): void { - const { route } = this.props; - const { sortId, sortType } = this.state; - + const onSort = (relationKey: string): void => { let type = I.SortType.Asc; if (sortId == relationKey) { type = sortType == I.SortType.Asc ? I.SortType.Desc : I.SortType.Asc; }; - this.setState({ sortId: relationKey, sortType: type }, () => this.getData(1)); + setSortId(relationKey); + setSortType(type); + analytics.event('ObjectListSort', { relationKey, route, type }); }; -}); + const columnList = getColumns(); + const items = getItems(); + + let pager = null; + if (total && items.length) { + pager = ( + <Pager + offset={offset} + limit={J.Constant.limit.listObject} + total={total} + onChange={page => getData(page)} + /> + ); + }; + + const Row = (item: any) => { + const cn = [ 'row' ]; + + if (U.Object.isTaskLayout(item.layout) && item.isDone) { + cn.push('isDone'); + }; + if (item.isArchived) { + cn.push('isArchived'); + }; + if (item.isDeleted) { + cn.push('isDeleted'); + }; + if (item.isHidden) { + cn.push('isHidden'); + }; + + return ( + <SelectionTarget + id={item.id} + type={I.SelectType.Record} + className={cn.join(' ')} + onContextMenu={e => onContext(e, item.id)} + > + {columnList.map(column => { + const cn = [ 'cell', `c-${column.relationKey}` ]; + const cnc = [ 'cellContent' ]; + const value = item[column.relationKey]; + + if (column.className) { + cnc.push(column.className); + }; + + let content = null; + let onClick = null; + + if (value) { + if (column.isObject) { + let object = null; + + if (column.relationKey == 'name') { + object = item; + cn.push('isName'); + cnc.push('isName'); + } else { + object = S.Detail.get(subId, value, []); + }; + + if (!object._empty_) { + onClick = () => U.Object.openConfig(object); + content = ( + <div className="flex"> + <IconObject object={object} /> + <ObjectName object={object} /> + </div> + ); + }; + } else + if (column.isCell) { + content = ( + <Cell + elementId={Relation.cellId(PREFIX, column.relationKey, item.id)} + rootId={rootId} + subId={subId} + block={null} + relationKey={column.relationKey} + getRecord={() => item} + viewType={I.ViewType.Grid} + idPrefix={PREFIX} + iconSize={20} + readonly={true} + arrayLimit={2} + textLimit={150} + /> + ); + } else { + content = column.mapper ? column.mapper(value) : value; + }; + }; + + return ( + <div key={`cell-${column.relationKey}`} className={cn.join(' ')}> + {content ? <div className={cnc.join(' ')} onClick={onClick}>{content}</div> : ''} + </div> + ); + })} + </SelectionTarget> + ); + }; + + useEffect(() => { + setSortId(columnList[0].relationKey); + + return () => { + C.ObjectSearchUnsubscribe([ subId ]); + } + }, []); + + useEffect(() => getData(1), [ sortId, sortType ]); + + useImperativeHandle(ref, () => ({ + getData, + })); + + return ( + <div className="listObject"> + <div className="table"> + <div className="row isHead"> + {columnList.map(column => { + let arrow = null; + + if (sortId == column.relationKey) { + arrow = <Icon className={`sortArrow c${sortType}`} />; + }; + + return ( + <div key={`head-${column.relationKey}`} className="cell isHead" onClick={() => onSort(column.relationKey)}> + <div className="name">{column.name}{arrow}</div> + </div> + ); + })} + </div> + + {!items.length ? ( + <div className="row"> + <div className="cell empty">{translate('commonNoObjects')}</div> + </div> + ) : ( + <> + {items.map((item: any, i: number) => ( + <Row key={i} {...item} /> + ))} + </> + )} + </div> + + {pager} + </div> + ); + +})); export default ListObject; \ No newline at end of file diff --git a/src/ts/component/list/objectManager.tsx b/src/ts/component/list/objectManager.tsx index 6c8c4ac9c1..68549ca651 100644 --- a/src/ts/component/list/objectManager.tsx +++ b/src/ts/component/list/objectManager.tsx @@ -1,411 +1,168 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import React, { forwardRef, useState, useEffect, useImperativeHandle, useRef } from 'react'; import { observer } from 'mobx-react'; import { AutoSizer, CellMeasurer, InfiniteLoader, List, CellMeasurerCache, WindowScroller } from 'react-virtualized'; import { Checkbox, Filter, Icon, IconObject, Loader, ObjectName, EmptySearch, ObjectDescription, Label } from 'Component'; import { I, S, U, J, translate } from 'Lib'; interface Props { - subId: string; - rowLength: number; + subId?: string; + rowLength?: number; buttons: I.ButtonComponent[]; info?: I.ObjectManagerItemInfo, - iconSize: number; - textEmpty: string; + iconSize?: number; + textEmpty?: string; filters?: I.Filter[]; sorts?: I.Sort[]; rowHeight?: number; sources?: string[]; collectionId?: string; isReadonly?: boolean; - ignoreArchived: boolean; - ignoreHidden: boolean; + ignoreArchived?: boolean; + ignoreHidden?: boolean; resize?: () => void; onAfterLoad?: (message: any) => void; }; -interface State { - isLoading: boolean; +interface ListManagerRefProps { + getSelected(): string[]; + setSelection(ids: string[]): void; + selectionClear(): void; }; -const ListObjectManager = observer(class ListObjectManager extends React.Component<Props, State> { - - _isMounted = false; - state = { - isLoading: false, - }; - - public static defaultProps = { - ignoreArchived: true, - ignoreHidden: true, - isReadonly: false, - }; - - top = 0; - offset = 0; - cache: any = null; - refList: List = null; - refFilter: Filter = null; - refCheckbox: Map<string, any> = new Map(); - selected: string[] = []; - timeout = 0; - - constructor (props: Props) { - super(props); - - this.onScroll = this.onScroll.bind(this); - this.onFilterShow = this.onFilterShow.bind(this); - this.onFilterChange = this.onFilterChange.bind(this); - this.onFilterClear = this.onFilterClear.bind(this); - this.onSelectAll = this.onSelectAll.bind(this); - }; - - render () { - if (!this.cache) { - return null; - }; - - const { isLoading } = this.state; - const { buttons, rowHeight, iconSize, info, isReadonly } = this.props; - const items = this.getItems(); - const cnControls = [ 'controls' ]; - const filter = this.getFilterValue(); - - if (filter) { - cnControls.push('withFilter'); - }; - - let textEmpty = String(this.props.textEmpty || ''); - let buttonsList: I.ButtonComponent[] = []; - - if (this.selected.length) { - cnControls.push('withSelected'); - - buttonsList.push({ icon: 'checkbox active', text: translate('commonDeselectAll'), onClick: this.onSelectAll }); - buttonsList = buttonsList.concat(buttons); - } else { - buttonsList.push({ icon: 'checkbox', text: translate('commonSelectAll'), onClick: this.onSelectAll }); - }; - - if (isReadonly) { - buttonsList = []; - }; - - const Info = (item: any) => { - let itemInfo: any = null; - - switch (info) { - default: - case I.ObjectManagerItemInfo.Description: { - itemInfo = <ObjectDescription object={item} />; - break; - }; - - case I.ObjectManagerItemInfo.FileSize: { - itemInfo = <Label text={String(U.File.size(item.sizeInBytes))} />; - break; - }; - }; - - return itemInfo; - }; - - const Button = (item: any) => ( - <div className="element" onClick={item.onClick}> - <Icon className={item.icon} /> - <div className="name">{item.text}</div> - </div> - ); - - const Item = (item: any) => ( - <div className="item"> - {isReadonly ? '' : ( - <Checkbox - ref={ref => this.refCheckbox.set(item.id, ref)} - value={this.selected.includes(item.id)} - onChange={e => this.onClick(e, item)} - /> - )} - <div className="objectClickArea" onClick={() => U.Object.openConfig(item)}> - <IconObject object={item} size={iconSize} /> - - <div className="info"> - <ObjectName object={item} /> - - <Info {...item} /> - </div> - </div> - </div> - ); - - const rowRenderer = (param: any) => { - const item = items[param.index]; - - return ( - <CellMeasurer - key={param.key} - parent={param.parent} - cache={this.cache} - columnIndex={0} - rowIndex={param.index} - hasFixedWidth={() => {}} - > - <div className="row" style={param.style}> - {item.children.map((item: any, i: number) => ( - <Item key={item.id} {...item} /> - ))} - </div> - </CellMeasurer> - ); - }; - - let controls = ( - <div className="controlsWrapper"> - <div className={cnControls.join(' ')}> - <div className="side left"> - {buttonsList.map((item: any, i: number) => ( - <Button key={i} {...item} /> - ))} - </div> - <div className="side right"> - <Icon className="search" onClick={this.onFilterShow} /> - - <div id="filterWrapper" className="filterWrapper"> - <Filter - ref={ref => this.refFilter = ref} - onChange={this.onFilterChange} - onClear={this.onFilterClear} - placeholder={translate('commonSearchPlaceholder')} - /> - </div> - </div> - </div> - </div> - ); - - let content = null; - if (!items.length) { - if (!filter) { - controls = null; - } else { - textEmpty = U.Common.sprintf(translate('popupSearchNoObjects'), filter); - }; - - content = <EmptySearch text={textEmpty} />; - } else { - content = ( - <div className="items"> - {isLoading ? <Loader /> : ( - <InfiniteLoader - rowCount={items.length} - loadMoreRows={() => {}} - isRowLoaded={({ index }) => true} - > - {({ onRowsRendered }) => ( - <WindowScroller scrollElement={$('#popupPage-innerWrap').get(0)}> - {({ height, isScrolling, registerChild, scrollTop }) => ( - <AutoSizer className="scrollArea"> - {({ width, height }) => ( - <List - ref={ref => this.refList = ref} - width={width} - height={height} - deferredMeasurmentCache={this.cache} - rowCount={items.length} - rowHeight={rowHeight || 64} - rowRenderer={rowRenderer} - onRowsRendered={onRowsRendered} - overscanRowCount={10} - onScroll={this.onScroll} - scrollToAlignment="start" - /> - )} - </AutoSizer> - )} - </WindowScroller> - )} - </InfiniteLoader> - )} - </div> - ); - }; - - return ( - <div className="objectManagerWrapper"> - {controls} - {content} - </div> - ); +const ListManager = observer(forwardRef<ListManagerRefProps, Props>(({ + subId = '', + rowLength = 2, + buttons, + info, + iconSize, + textEmpty = '', + filters = [], + sorts = [], + rowHeight = 0, + sources = [], + collectionId = '', + isReadonly = false, + ignoreArchived = true, + ignoreHidden = true, + resize, + onAfterLoad +}, ref) => { + + const nodeRef = useRef(null); + const filterWrapperRef = useRef(null); + const filterRef = useRef(null); + const listRef = useRef(null); + const checkboxRef = useRef(new Map()); + const timeout = useRef(0); + const top = useRef(0); + const cache = useRef(new CellMeasurerCache()); + const [ selected, setSelected ] = useState<string[]>([]); + const [ isLoading, setIsLoading ] = useState(false); + + const onFilterShow = () => { + $(filterWrapperRef.current).addClass('active'); + filterRef.current.focus(); }; - componentDidMount () { - const { resize } = this.props; - - this._isMounted = true; - this.load(); - - if (resize) { - resize(); - }; + const onFilterChange = () => { + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => load(), J.Constant.delay.keyboard); }; - componentDidUpdate () { - const { subId, resize, rowHeight } = this.props; - const records = S.Record.getRecordIds(subId, ''); - const items = this.getItems(); - - if (!this.cache) { - this.cache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: rowHeight || 64, - keyMapper: i => (items[i] || {}).id, - }); - this.forceUpdate(); - }; - - records.forEach(id => { - const check = this.refCheckbox.get(id); - if (check) { - check.setValue(this.selected.includes(id)); - }; - }); - - if (resize) { - resize(); - }; - - if (this.refList) { - this.refList.recomputeRowHeights(); - }; - }; - - componentWillUnmount () { - this._isMounted = false; - window.clearTimeout(this.timeout); - }; - - onFilterShow () { - const node = $(ReactDOM.findDOMNode(this)); - const wrapper = node.find('#filterWrapper'); - - wrapper.addClass('active'); - this.refFilter.focus(); - }; - - onFilterChange () { - window.clearTimeout(this.timeout); - this.timeout = window.setTimeout(() => this.load(), J.Constant.delay.keyboard); - }; - - onFilterClear () { - const node = $(ReactDOM.findDOMNode(this)); - const wrapper = node.find('#filterWrapper'); - + const onFilterClear = () => { S.Menu.closeAll(J.Menu.store); - wrapper.removeClass('active'); + $(filterWrapperRef.current).addClass('active'); }; - onClick (e: React.MouseEvent, item: any) { + const onClick = (e: React.MouseEvent, item: any) => { e.stopPropagation(); - const { subId } = this.props; const records = S.Record.getRecordIds(subId, ''); + + let ids = selected; if (e.shiftKey) { const idx = records.findIndex(id => id == item.id); - if ((idx >= 0) && (this.selected.length > 0)) { - const indexes = this.getSelectedIndexes().filter(i => i != idx); + if ((idx >= 0) && (ids.length > 0)) { + const indexes = getSelectedIndexes().filter(i => i != idx); const closest = U.Common.findClosestElement(indexes, idx); if (isFinite(closest)) { - const [ start, end ] = this.getSelectionRange(closest, idx); - this.selected = this.selected.concat(records.slice(start, end)); + const [ start, end ] = getSelectionRange(closest, idx); + ids = ids.concat(records.slice(start, end)); }; }; } else { - if (this.selected.includes(item.id)) { - this.selected = this.selected.filter(it => it != item.id); - } else { - this.selected.push(item.id); - }; + ids = ids.includes(item.id) ? ids.filter(it => it != item.id) : ids.concat(item.id); }; - this.selected = U.Common.arrayUnique(this.selected); - this.forceUpdate(); + setSelection(ids); }; - getSelectedIndexes () { - const { subId } = this.props; + const getSelectedIndexes = () => { const records = S.Record.getRecordIds(subId, ''); - const indexes = this.selected.map(id => records.findIndex(it => it == id)); + const indexes = selected.map(id => records.findIndex(it => it == id)); return indexes.filter(idx => idx >= 0); }; - getSelectionRange (index1: number, index2: number) { + const getSelectionRange = (index1: number, index2: number) => { const [ start, end ] = (index1 >= index2) ? [ index2, index1 ] : [ index1 + 1, index2 + 1 ]; return [ start, end ]; }; - setSelectedRange (start: number, end: number) { - const { subId } = this.props; + const setSelectedRange = (start: number, end: number) => { const records = S.Record.getRecordIds(subId, ''); if (end > records.length) { end = records.length; }; - this.selected = this.selected.concat(records.slice(start, end)); - this.forceUpdate(); + setSelection(selected.concat(records.slice(start, end))); }; - setSelection (ids: string[]) { - this.selected = ids; - this.forceUpdate(); + const setSelection = (ids: string[]) => { + setSelected(U.Common.arrayUnique(ids)); }; - onSelectAll () { - this.selected.length ? this.selectionClear() : this.selectionAll(); + const onSelectAll = () => { + selected.length ? selectionClear() : selectionAll(); }; - selectionAll () { - const { subId } = this.props; - this.selected = S.Record.getRecordIds(subId, ''); - this.forceUpdate(); + const selectionAll = () => { + setSelection(S.Record.getRecordIds(subId, '')); }; - selectionClear () { - this.selected = []; - this.forceUpdate(); + const selectionClear = () => { + setSelection([]); }; - onScroll ({ scrollTop }) { - this.top = scrollTop; + const onScroll = ({ scrollTop }) => { + top.current = scrollTop; }; - load () { - const { subId, sources, ignoreArchived, ignoreHidden, collectionId, onAfterLoad } = this.props; - const filter = this.getFilterValue(); - const filters = [].concat(this.props.filters || []); - const sorts = [].concat(this.props.sorts || []); + const load = () => { + const filter = getFilterValue(); + const fl = [].concat(filters || []); + const sl = [].concat(sorts || []); if (filter) { filters.push({ relationKey: 'name', condition: I.FilterCondition.Like, value: filter }); }; - this.setState({ isLoading: true }); + setIsLoading(true); U.Data.searchSubscribe({ subId, - sorts, - filters, + sorts: sl, + filters: fl, ignoreArchived, ignoreHidden, sources: sources || [], collectionId: collectionId || '' }, (message) => { - this.setState({ isLoading: false }); + setIsLoading(false); if (onAfterLoad) { onAfterLoad(message); @@ -413,8 +170,7 @@ const ListObjectManager = observer(class ListObjectManager extends React.Compone }); }; - getItems () { - const { subId, rowLength } = this.props; + const getItems = () => { const ret: any[] = []; const records = S.Record.getRecords(subId); @@ -439,10 +195,220 @@ const ListObjectManager = observer(class ListObjectManager extends React.Compone return ret.filter(it => it.children.length > 0); }; - getFilterValue () { - return this.refFilter ? this.refFilter.getValue() : ''; + const getFilterValue = () => { + return String(filterRef.current?.getValue() || ''); + }; + + const items = getItems(); + const cnControls = [ 'controls' ]; + const filter = getFilterValue(); + + if (filter) { + cnControls.push('withFilter'); + }; + + let buttonsList: I.ButtonComponent[] = []; + + if (selected.length) { + cnControls.push('withSelected'); + + buttonsList.push({ icon: 'checkbox active', text: translate('commonDeselectAll'), onClick: onSelectAll }); + buttonsList = buttonsList.concat(buttons); + } else { + buttonsList.push({ icon: 'checkbox', text: translate('commonSelectAll'), onClick: onSelectAll }); }; -}); + if (isReadonly) { + buttonsList = []; + }; + + const Info = (item: any) => { + let itemInfo: any = null; + + switch (info) { + default: + case I.ObjectManagerItemInfo.Description: { + itemInfo = <ObjectDescription object={item} />; + break; + }; + + case I.ObjectManagerItemInfo.FileSize: { + itemInfo = <Label text={String(U.File.size(item.sizeInBytes))} />; + break; + }; + }; + + return itemInfo; + }; + + const Button = (item: any) => ( + <div className="element" onClick={item.onClick}> + <Icon className={item.icon} /> + <div className="name">{item.text}</div> + </div> + ); + + const Item = (item: any) => ( + <div className="item"> + {isReadonly ? '' : ( + <Checkbox + ref={ref => checkboxRef.current.set(item.id, ref)} + value={selected.includes(item.id)} + onChange={e => onClick(e, item)} + /> + )} + <div className="objectClickArea" onClick={() => U.Object.openConfig(item)}> + <IconObject object={item} size={iconSize} /> + + <div className="info"> + <ObjectName object={item} /> + + <Info {...item} /> + </div> + </div> + </div> + ); + + const rowRenderer = (param: any) => { + const item = items[param.index]; + + return ( + <CellMeasurer + key={param.key} + parent={param.parent} + cache={cache.current} + columnIndex={0} + rowIndex={param.index} + hasFixedWidth={() => {}} + > + <div className="row" style={param.style}> + {item.children.map((item: any, i: number) => ( + <Item key={item.id} {...item} /> + ))} + </div> + </CellMeasurer> + ); + }; + + let controls = ( + <div className="controlsWrapper"> + <div className={cnControls.join(' ')}> + <div className="side left"> + {buttonsList.map((item: any, i: number) => ( + <Button key={i} {...item} /> + ))} + </div> + <div className="side right"> + <Icon className="search" onClick={onFilterShow} /> + + <div ref={filterWrapperRef} id="filterWrapper" className="filterWrapper"> + <Filter + ref={filterRef} + onChange={onFilterChange} + onClear={onFilterClear} + placeholder={translate('commonSearchPlaceholder')} + /> + </div> + </div> + </div> + </div> + ); + + let content = null; + if (!items.length) { + if (!filter) { + controls = null; + } else { + textEmpty = U.Common.sprintf(translate('popupSearchNoObjects'), filter); + }; + + content = <EmptySearch text={textEmpty} />; + } else { + content = ( + <div className="items"> + {isLoading ? <Loader /> : ( + <InfiniteLoader + rowCount={items.length} + loadMoreRows={() => {}} + isRowLoaded={({ index }) => true} + > + {({ onRowsRendered }) => ( + <WindowScroller scrollElement={$('#popupPage-innerWrap').get(0)}> + {({ height, isScrolling, registerChild, scrollTop }) => ( + <AutoSizer className="scrollArea"> + {({ width, height }) => ( + <List + ref={listRef} + width={width} + height={height} + deferredMeasurmentCache={cache.current} + rowCount={items.length} + rowHeight={rowHeight || 64} + rowRenderer={rowRenderer} + onRowsRendered={onRowsRendered} + overscanRowCount={10} + onScroll={onScroll} + scrollToAlignment="start" + /> + )} + </AutoSizer> + )} + </WindowScroller> + )} + </InfiniteLoader> + )} + </div> + ); + }; + + useEffect(() => { + load(); + + if (resize) { + resize(); + }; + + return () => { + window.clearTimeout(timeout.current); + }; + }, []); + + useEffect(() => { + const records = S.Record.getRecordIds(subId, ''); + const items = getItems(); + + cache.current = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: rowHeight || 64, + keyMapper: i => (items[i] || {}).id, + }); + + records.forEach(id => { + const check = checkboxRef.current.get(id); + if (check) { + check.setValue(selected.includes(id)); + }; + }); + + if (resize) { + resize(); + }; + + listRef.current?.recomputeRowHeights(); + }); + + useImperativeHandle(ref, () => ({ + getSelected: () => selected, + setSelection, + selectionClear, + })); + + return ( + <div className="objectManagerWrapper"> + {controls} + {content} + </div> + ); +})); -export default ListObjectManager; +export default ListManager; \ No newline at end of file diff --git a/src/ts/component/list/popup.tsx b/src/ts/component/list/popup.tsx index 9a6d380cf9..fab0a6b0b7 100644 --- a/src/ts/component/list/popup.tsx +++ b/src/ts/component/list/popup.tsx @@ -1,27 +1,23 @@ -import * as React from 'react'; +import React, { FC, useEffect } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { Popup } from 'Component'; import { I, S } from 'Lib'; -const ListPopup = observer(class ListPopup extends React.Component<I.PageComponent> { +const ListPopup: FC<I.PageComponent> = observer(() => { + const { list } = S.Popup; - render () { - const { list } = S.Popup; - - return ( - <div className="popups"> - {list.map((item: I.Popup, i: number) => ( - <Popup {...this.props} key={i} {...item} /> - ))} - </div> - ); - }; - - componentDidUpdate () { + useEffect(() => { $('body').toggleClass('overPopup', S.Popup.list.length > 0); - }; - + }, [ list.length ]); + + return ( + <div className="popups"> + {list.map((item: I.Popup, i: number) => ( + <Popup key={i} {...item} /> + ))} + </div> + ); }); export default ListPopup; \ No newline at end of file diff --git a/src/ts/component/list/previewObject.tsx b/src/ts/component/list/previewObject.tsx index 7faae71ed1..82e596b37d 100644 --- a/src/ts/component/list/previewObject.tsx +++ b/src/ts/component/list/previewObject.tsx @@ -1,10 +1,10 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect, useRef, useImperativeHandle } from 'react'; import $ from 'jquery'; import { PreviewObject, Icon } from 'Component'; import { I, U, keyboard, translate } from 'Lib'; interface Props { - offsetX: number; + offsetX?: number; canAdd?: boolean; withBlank?: boolean; blankId?: string; @@ -16,126 +16,32 @@ interface Props { onMenu?: (e: any, item: any) => void; }; -const WIDTH = 344; - -class ListObjectPreview extends React.Component<Props> { - - public static defaultProps = { - offsetX: 0, - canAdd: false, - }; - - node: any = null; - n = 0; - page = 0; - maxPage = 0; - timeout = 0; - refObj: any = {}; - - render () { - const { onAdd, onBlank, onMenu, defaultId, blankId } = this.props; - const items = this.getItems(); - - const ItemAdd = () => ( - <div id="item-add" className="item add" onClick={onAdd}> - <Icon className="plus" /> - <div className="hoverArea" /> - </div> - ); - - const ItemBlank = (item: any) => ( - <div id={`item-${item.id}`} className="previewObject blank" onClick={onBlank}> - {onMenu ? ( - <div id={`item-more-${item.id}`} className="moreWrapper" onClick={e => onMenu(e, item)}> - <Icon className="more" /> - </div> - ) : ''} - - <div className="scroller"> - <div className="heading"> - <div className="name">Blank</div> - </div> - </div> - <div className="border" /> - </div> - ); - - const Item = (item: any) => { - if (item.id == 'add') { - return <ItemAdd />; - }; - - const cn = [ 'item' ]; - - let label = null; - let content = null; - - if (onMenu) { - cn.push('withMenu'); - }; - - if (defaultId == item.id) { - label = <div className="defaultLabel">{translate('commonDefault')}</div>; - }; - - if (item.id == blankId) { - content = <ItemBlank {...item} />; - } else { - content = ( - <PreviewObject - ref={ref => this.refObj[item.id] = ref} - size={I.PreviewSize.Large} - rootId={item.id} - onClick={e => this.onClick(e, item)} - onMore={onMenu ? e => onMenu(e, item) : null} - /> - ); - }; - - return ( - <div id={`item-${item.id}`} className={cn.join(' ')}> - {label} - - <div - className="hoverArea" - onMouseEnter={e => this.onMouseEnter(e, item)} - onMouseLeave={e => this.onMouseLeave(e, item)} - > - {content} - </div> - </div> - ); - }; - - return ( - <div - ref={node => this.node = node} - className="listPreviewObject" - > - <div className="wrap"> - <div id="scroll" className="scroll"> - {items.map((item: any, i: number) => ( - <Item key={i} {...item} index={i} /> - ))} - </div> - </div> - - <Icon id="arrowLeft" className="arrow left" onClick={() => this.onArrow(-1)} /> - <Icon id="arrowRight" className="arrow right" onClick={() => this.onArrow(1)} /> - </div> - ); - }; - - componentDidMount () { - this.resize(); - }; +interface ListPreviewObjectRefProps { + updateItem: (id: string) => void; + onKeyUp: (e: any) => void; +}; - componentDidUpdate () { - this.resize(); - }; +const WIDTH = 344; - getItems () { - const { getItems, canAdd, withBlank, blankId } = this.props; +const ListPreviewObject = forwardRef<ListPreviewObjectRefProps, Props>(({ + offsetX = 0, + canAdd = false, + withBlank = false, + blankId = '', + defaultId = '', + getItems, + onClick, + onAdd, + onBlank, + onMenu, +}, ref) => { + + const nodeRef = useRef(null); + const page = useRef(0); + const n = useRef(0); + const objectRef = useRef(new Map()); + + const getItemsHandler = () => { const items = U.Common.objectCopy(getItems()); if (withBlank) { @@ -147,44 +53,37 @@ class ListObjectPreview extends React.Component<Props> { return items; }; - getMaxPage () { - const node = $(this.node); - const items = this.getItems(); + const getMaxPage = () => { + const node = $(nodeRef.current); + const items = getItemsHandler(); const cnt = Math.floor(node.width() / WIDTH); return Math.max(0, Math.ceil(items.length / cnt) - 1); }; - onMouseEnter (e: any, item: any) { - const items = this.getItems(); + const onMouseEnter = (e: any, item: any) => { + const items = getItemsHandler(); - this.n = items.findIndex(it => it.id == item.id); - this.setActive(); + n.current = items.findIndex(it => it.id == item.id); + setActive(); }; - onMouseLeave (e: any, item: any) { - const node = $(this.node); + const onMouseLeave = (e: any, item: any) => { + const node = $(nodeRef.current); + node.find('.item.hover').removeClass('hover'); node.find('.hoverArea.hover').removeClass('hover'); }; - onClick (e: any, item: any) { - const { onClick } = this.props; - - if (onClick) { - onClick(e, item); - }; - }; - - setActive () { - const items = this.getItems(); - const item = items[this.n]; + const setActive = () => { + const items = getItemsHandler(); + const item = items[n.current]; if (!item) { return; }; - const node = $(this.node); + const node = $(nodeRef.current); node.find('.item.hover').removeClass('hover'); node.find('.hoverArea.hover').removeClass('hover'); @@ -192,74 +91,167 @@ class ListObjectPreview extends React.Component<Props> { node.find(`#item-${item.id} .hoverArea`).addClass('hover'); }; - onKeyUp (e: any) { - const items = this.getItems(); + const onKeyUp = (e: any) => { + const items = getItemsHandler(); keyboard.shortcut('arrowleft, arrowright', e, (pressed: string) => { const dir = pressed == 'arrowleft' ? -1 : 1; - this.n += dir; + n.current += dir; - if (this.n < 0) { - this.n = items.length - 1; + if (n.current < 0) { + n.current = items.length - 1; }; - if (this.n > items.length - 1) { - this.n = 0; + if (n.current > items.length - 1) { + n.current = 0; }; - this.page = Math.floor(this.n / 2); - this.onArrow(0); - this.setActive(); + page.current = Math.floor(n.current / 2); + onArrow(0); + setActive(); }); - keyboard.shortcut('enter, space', e, () => { - this.onClick(e, items[this.n]); - }); + keyboard.shortcut('enter, space', e, () => onClick(e, items[n.current])); }; - onArrow (dir: number) { - const { offsetX } = this.props; - const node = $(this.node); + const onArrow = (dir: number) => { + const node = $(nodeRef.current); const scroll = node.find('#scroll'); const arrowLeft = node.find('#arrowLeft'); const arrowRight = node.find('#arrowRight'); const w = node.width(); - const max = this.getMaxPage(); + const max = getMaxPage(); - this.page += dir; - this.page = Math.min(max, Math.max(0, this.page)); + page.current += dir; + page.current = Math.min(max, Math.max(0, page.current)); - const x = -this.page * (w + 16 + offsetX); + const x = -page.current * (w + 16 + offsetX); arrowLeft.removeClass('dn'); arrowRight.removeClass('dn'); - if (this.page == 0) { + if (page.current == 0) { arrowLeft.addClass('dn'); }; - if (this.page == max) { + if (page.current == max) { arrowRight.addClass('dn'); }; scroll.css({ transform: `translate3d(${x}px,0px,0px` }); }; - updateItem (id: string) { - if (this.refObj[id]) { - this.refObj[id].update(); - }; + const updateItem = (id: string) => { + objectRef.current.get(id)?.update(); }; - resize () { - const node = $(this.node); + const resize = () => { + const node = $(nodeRef.current); const arrowLeft = node.find('#arrowLeft'); const arrowRight = node.find('#arrowRight'); - const isFirst = this.page == 0; - const isLast = this.page == this.getMaxPage(); + const isFirst = page.current == 0; + const isLast = page.current == getMaxPage(); arrowLeft.toggleClass('dn', isFirst); arrowRight.toggleClass('dn', isLast); }; - -}; -export default ListObjectPreview; + const items = getItemsHandler(); + + const ItemAdd = () => ( + <div id="item-add" className="item add" onClick={onAdd}> + <Icon className="plus" /> + <div className="hoverArea" /> + </div> + ); + + const ItemBlank = (item: any) => ( + <div id={`item-${item.id}`} className="previewObject blank" onClick={onBlank}> + {onMenu ? ( + <div id={`item-more-${item.id}`} className="moreWrapper" onClick={e => onMenu(e, item)}> + <Icon className="more" /> + </div> + ) : ''} + + <div className="scroller"> + <div className="heading"> + <div className="name">{translate('commonBlank')}</div> + </div> + </div> + <div className="border" /> + </div> + ); + + const Item = (item: any) => { + if (item.id == 'add') { + return <ItemAdd />; + }; + + const cn = [ 'item' ]; + + let label = null; + let content = null; + + if (onMenu) { + cn.push('withMenu'); + }; + + if (defaultId == item.id) { + label = <div className="defaultLabel">{translate('commonDefault')}</div>; + }; + + if (item.id == blankId) { + content = <ItemBlank {...item} />; + } else { + content = ( + <PreviewObject + ref={ref => objectRef.current.set(item.id, ref)} + size={I.PreviewSize.Large} + rootId={item.id} + onClick={e => onClick(e, item)} + onMore={onMenu ? e => onMenu(e, item) : null} + /> + ); + }; + + return ( + <div id={`item-${item.id}`} className={cn.join(' ')}> + {label} + + <div + className="hoverArea" + onMouseEnter={e => onMouseEnter(e, item)} + onMouseLeave={e => onMouseLeave(e, item)} + > + {content} + </div> + </div> + ); + }; + + useEffect(() => resize()); + + useImperativeHandle(ref, () => ({ + updateItem, + onKeyUp + })); + + return ( + <div + ref={nodeRef} + className="listPreviewObject" + > + <div className="wrap"> + <div id="scroll" className="scroll"> + {items.map((item: any, i: number) => ( + <Item key={i} {...item} index={i} /> + ))} + </div> + </div> + + <Icon id="arrowLeft" className="arrow left" onClick={() => onArrow(-1)} /> + <Icon id="arrowRight" className="arrow right" onClick={() => onArrow(1)} /> + </div> + ); + +}); + +export default ListPreviewObject; \ No newline at end of file diff --git a/src/ts/component/menu/block/add.tsx b/src/ts/component/menu/block/add.tsx index da86b5eb22..80feaa6d49 100644 --- a/src/ts/component/menu/block/add.tsx +++ b/src/ts/component/menu/block/add.tsx @@ -57,9 +57,6 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> </div> ); } else - if (item.isSection) { - content = <div className={[ 'sectionName', (index == 0 ? 'first' : '') ].join(' ')} style={param.style}>{item.name}</div>; - } else if (item.isRelation) { content = ( <div @@ -131,6 +128,7 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> <MenuItemVertical key={item.id + '-' + index} {...item} + index={index} className={cn.join(' ')} withDescription={item.isBlock} onMouseEnter={e => this.onMouseEnter(e, item)} @@ -177,7 +175,7 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> rowRenderer={rowRenderer} onRowsRendered={onRowsRendered} overscanRowCount={20} - scrollToAlignment="center" + scrollToAlignment="start" /> )} </AutoSizer> @@ -339,7 +337,7 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> sections = U.Menu.sectionsFilter(sections, filter.text); }; - + return U.Menu.sectionsMap(sections); }; @@ -411,7 +409,7 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> }; switch (item.itemId) { - case 'move': + case 'move': { menuId = 'searchObject'; menuParam.offsetY = -36; @@ -422,27 +420,60 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> ], }); break; + }; + + case 'date': { + menuId = 'dataviewCalendar'; + menuParam.data = Object.assign(menuParam.data, { + canEdit: true, + value: U.Date.now(), + onChange: (t: number) => { + C.ObjectDateByTimestamp(S.Common.space, t, (message: any) => { + if (message.error.code) { + return; + }; - case 'existingPage': + const target = message.details; + + C.BlockCreate(rootId, blockId, position, U.Data.getLinkBlockParam(target.id, target.layout, true), (message: any) => { + if (message.error.code) { + return; + }; + + focus.set(message.blockId, { from: 0, to: 0 }); + focus.apply(); + + analytics.event('CreateLink'); + close(); + }); + }); + } + }); + break; + }; + + case 'existingPage': { menuId = 'searchObject'; - menuParam.data.canAdd = true; menuParam.data = Object.assign(menuParam.data, { + canAdd: true, type: I.NavigationType.Link, }); break; + }; - case 'existingFile': + case 'existingFile': { menuId = 'searchObject'; - menuParam.data.canAdd = true; menuParam.data = Object.assign(menuParam.data, { + canAdd: true, type: I.NavigationType.Link, filters: [ { relationKey: 'layout', condition: I.FilterCondition.In, value: U.Object.getFileLayouts() }, ], }); break; + }; - case 'turnObject': + case 'turnObject': { menuId = 'typeSuggest'; menuParam.data = Object.assign(menuParam.data, { filter: '', @@ -457,6 +488,7 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> }, }); break; + }; }; @@ -696,24 +728,11 @@ const MenuBlockAdd = observer(class MenuBlockAdd extends React.Component<I.Menu> }; getRowHeight (item: any, index: number) { - if (item.isRelation || item.isRelationAdd) { - return HEIGHT_RELATION; - }; - if (item.isSection && index > 0) { - return HEIGHT_SECTION; - }; - if (item.isBlock) { - return HEIGHT_DESCRIPTION; - }; - return HEIGHT_ITEM; - }; - - recalcIndex () { - const itemsWithSection = this.getItems(true); - const itemsWithoutSection = itemsWithSection.filter(it => !it.isSection); - const active: any = itemsWithoutSection[this.n] || {}; - - return itemsWithSection.findIndex(it => it.id == active.id); + let h = HEIGHT_ITEM + if (item.isRelation || item.isRelationAdd) h = HEIGHT_RELATION; + else if (item.isSection && (index > 0)) h = HEIGHT_SECTION; + else if (item.isBlock) h = HEIGHT_DESCRIPTION; + return h; }; }); diff --git a/src/ts/component/menu/block/background.tsx b/src/ts/component/menu/block/background.tsx index eacc1467a7..2fc49e6128 100644 --- a/src/ts/component/menu/block/background.tsx +++ b/src/ts/component/menu/block/background.tsx @@ -1,79 +1,76 @@ -import * as React from 'react'; +import React, { forwardRef, useImperativeHandle, useRef, useEffect } from 'react'; import $ from 'jquery'; import { MenuItemVertical } from 'Component'; import { I, U, keyboard } from 'Lib'; -class MenuBlockBackground extends React.Component<I.Menu> { - - n = -1; - - constructor (props: I.Menu) { - super(props); - - this.onClick = this.onClick.bind(this); - }; +const MenuBlockColor = forwardRef<I.MenuRef, I.Menu>((props, ref) => { - render () { - const { param } = this.props; - const { data } = param; - const value = String(data.value || ''); - const items = this.getItems(); + const { param, onKeyDown, setActive, close } = props; + const { data } = param; + const { onChange } = data; + const value = String(data.value || ''); + const n = useRef(-1); - let id = 0; - return ( - <div> - {items.map((action: any, i: number) => { - const inner = <div className={'inner bgColor bgColor-' + action.className} />; - return ( - <MenuItemVertical - id={id++} - key={i} - {...action} - icon="color" - inner={inner} - checkbox={action.value == value} - onClick={e => this.onClick(e, action)} - onMouseEnter={e => this.onOver(e, action)} - /> - ); - })} - </div> - ); - }; - - componentDidMount () { - this.rebind(); - }; - - rebind () { - this.unbind(); - $(window).on('keydown.menu', e => this.props.onKeyDown(e)); - window.setTimeout(() => this.props.setActive(), 15); + const rebind = () => { + unbind(); + + $(window).on('keydown.menu', e => onKeyDown(e)); + window.setTimeout(() => setActive(), 15); }; - unbind () { + const unbind = () => { $(window).off('keydown.menu'); }; - getItems () { - return U.Menu.getBgColors(); + const getItems = () => { + let id = 0; + return U.Menu.prepareForSelect(U.Menu.getBgColors().map(it => ({ ...it, id: id++ }))); }; - onOver (e: any, item: any) { + const onOver = (e: any, item: any) => { if (!keyboard.isMouseDisabled) { - this.props.setActive(item, false); + setActive(item, false); }; }; - onClick (e: any, item: any) { - const { param, close } = this.props; - const { data } = param; - const { onChange } = data; - + const onClick = (e: any, item: any) => { close(); onChange(item.value); }; + + useEffect(() => { + rebind(); + return () => unbind(); + }, []); + + useImperativeHandle(ref, () => ({ + rebind, + unbind, + getItems, + getIndex: () => n.current, + setIndex: (i: number) => n.current = i, + onClick, + onOver + }), []); + + const items = getItems(); -}; + return ( + <div> + {items.map((action: any, i: number) => ( + <MenuItemVertical + {...action} + key={i} + icon="color" + inner={<div className={`inner bgColor bgColor-${action.className}`} />} + checkbox={action.value == value} + onClick={e => onClick(e, action)} + onMouseEnter={e => onOver(e, action)} + /> + ))} + </div> + ); + +}); -export default MenuBlockBackground; \ No newline at end of file +export default MenuBlockColor; \ No newline at end of file diff --git a/src/ts/component/menu/block/color.tsx b/src/ts/component/menu/block/color.tsx index 3daebeb2cf..9a68ce0068 100644 --- a/src/ts/component/menu/block/color.tsx +++ b/src/ts/component/menu/block/color.tsx @@ -1,79 +1,76 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useImperativeHandle, useEffect } from 'react'; import $ from 'jquery'; import { MenuItemVertical } from 'Component'; import { I, U, keyboard } from 'Lib'; -class MenuBlockColor extends React.Component<I.Menu> { - - n = -1; - - constructor (props: I.Menu) { - super(props); - - this.onClick = this.onClick.bind(this); - }; +const MenuBlockColor = forwardRef<I.MenuRef, I.Menu>((props, ref) => { - render () { - const { param } = this.props; - const { data } = param; - const value = String(data.value || ''); - const items = this.getItems(); + const { param, onKeyDown, setActive, close } = props; + const { data } = param; + const { onChange } = data; + const value = String(data.value || ''); + const n = useRef(-1); - let id = 0; - return ( - <div> - {items.map((action: any, i: number) => { - const inner = <div className={'inner textColor textColor-' + action.className} />; - return ( - <MenuItemVertical - id={id++} - key={i} - {...action} - icon="color" - inner={inner} - checkbox={action.value == value} - onClick={e => this.onClick(e, action)} - onMouseEnter={e => this.onOver(e, action)} - /> - ); - })} - </div> - ); - }; - - componentDidMount () { - this.rebind(); - }; - - rebind () { - this.unbind(); - $(window).on('keydown.menu', e => this.props.onKeyDown(e)); - window.setTimeout(() => this.props.setActive(), 15); + const rebind = () => { + unbind(); + + $(window).on('keydown.menu', e => onKeyDown(e)); + window.setTimeout(() => setActive(), 15); }; - unbind () { + const unbind = () => { $(window).off('keydown.menu'); }; - getItems () { - return U.Menu.getTextColors(); + const getItems = () => { + let id = 0; + return U.Menu.prepareForSelect(U.Menu.getTextColors().map(it => ({ ...it, id: id++ }))); }; - onOver (e: any, item: any) { + const onOver = (e: any, item: any) => { if (!keyboard.isMouseDisabled) { - this.props.setActive(item, false); + setActive(item, false); }; }; - onClick (e: any, item: any) { - const { param, close } = this.props; - const { data } = param; - const { onChange } = data; - + const onClick = (e: any, item: any) => { close(); onChange(item.value); }; - -}; + + useEffect(() => { + rebind(); + return () => unbind(); + }, []); + + useImperativeHandle(ref, () => ({ + rebind, + unbind, + getItems, + getIndex: () => n.current, + setIndex: (i: number) => n.current = i, + onClick, + onOver, + }), []); + + const items = getItems(); + + return ( + <div> + {items.map((action: any, i: number) => ( + <MenuItemVertical + {...action} + key={i} + icon="color" + inner={<div className={`inner textColor textColor-${action.className}`} />} + checkbox={action.value == value} + onClick={e => onClick(e, action)} + onMouseEnter={e => onOver(e, action)} + /> + ))} + </div> + ); + +}); export default MenuBlockColor; \ No newline at end of file diff --git a/src/ts/component/menu/block/context.tsx b/src/ts/component/menu/block/context.tsx index bcef6a012f..041cc98648 100644 --- a/src/ts/component/menu/block/context.tsx +++ b/src/ts/component/menu/block/context.tsx @@ -182,6 +182,7 @@ const MenuBlockContext = observer(class MenuBlockContext extends React.Component }; const { from, to } = range; + const object = S.Detail.get(rootId, rootId); keyboard.disableContextClose(true); focus.set(blockId, range); @@ -211,6 +212,8 @@ const MenuBlockContext = observer(class MenuBlockContext extends React.Component marks = Mark.toggle(marks, { type, param: '', range: { from, to } }); S.Menu.updateData(this.props.id, { marks }); onChange(marks); + + analytics.event('ChangeTextStyle', { type, count: 1, objectType: object?.type }); break; }; @@ -312,6 +315,7 @@ const MenuBlockContext = observer(class MenuBlockContext extends React.Component S.Menu.updateData(this.props.id, { marks }); onChange(marks); + analytics.event('ChangeTextStyle', { type: newType, count: 1, objectType: object?.type }); window.setTimeout(() => focus.apply(), 15); } }); @@ -349,6 +353,8 @@ const MenuBlockContext = observer(class MenuBlockContext extends React.Component marks = Mark.toggle(marks, { type, param, range: { from, to } }); S.Menu.updateData(this.props.id, { marks }); + + analytics.event('ChangeTextStyle', { type, count: 1, objectType: object?.type }); onChange(marks); }, }); diff --git a/src/ts/component/menu/block/latex.tsx b/src/ts/component/menu/block/latex.tsx index fc87fad416..0fde1e0070 100644 --- a/src/ts/component/menu/block/latex.tsx +++ b/src/ts/component/menu/block/latex.tsx @@ -309,14 +309,6 @@ const MenuBlockLatex = observer(class MenuBlockLatex extends React.Component<I.M return isTemplate ? HEIGHT_ITEM_BIG : HEIGHT_ITEM_SMALL; }; - recalcIndex () { - const itemsWithSection = this.getItems(true); - const itemsWithoutSection = itemsWithSection.filter(it => !it.isSection); - const active: any = itemsWithoutSection[this.n] || {}; - - return itemsWithSection.findIndex(it => it.id == active.id); - }; - resize () { const { param, getId, position } = this.props; const { data } = param; diff --git a/src/ts/component/menu/block/link.tsx b/src/ts/component/menu/block/link.tsx index 53aba1bd8d..66fecb9150 100644 --- a/src/ts/component/menu/block/link.tsx +++ b/src/ts/component/menu/block/link.tsx @@ -64,13 +64,6 @@ const MenuBlockLink = observer(class MenuBlockLink extends React.Component<I.Men if (item.isSection) { content = <div className={[ 'sectionName', (param.index == 0 ? 'first' : '') ].join(' ')} style={param.style}>{item.name}</div>; - } else - if (item.isDiv) { - content = ( - <div className="separator" style={param.style}> - <div className="inner" /> - </div> - ); } else { if ([ 'add', 'link' ].indexOf(item.itemId) >= 0) { cn.push(item.itemId); @@ -96,6 +89,7 @@ const MenuBlockLink = observer(class MenuBlockLink extends React.Component<I.Men description={type ? type.name : undefined} style={param.style} iconSize={40} + isDiv={item.isDiv} className={cn.join(' ')} /> ); diff --git a/src/ts/component/menu/block/link/settings.tsx b/src/ts/component/menu/block/link/settings.tsx index 5aa1bdb7bc..ea706f8e91 100644 --- a/src/ts/component/menu/block/link/settings.tsx +++ b/src/ts/component/menu/block/link/settings.tsx @@ -134,11 +134,16 @@ const MenuBlockLinkSettings = observer(class MenuBlockLinkSettings extends React }; }; - getContent () { + getContent (): any { const { param } = this.props; const { data } = param; const { rootId, blockId } = data; const block = S.Block.getLeaf(rootId, blockId); + + if (!block) { + return {}; + }; + const object = S.Detail.get(rootId, block.getTargetObjectId()); return U.Data.checkLinkSettings(block.content, object.layout); diff --git a/src/ts/component/menu/block/mention.tsx b/src/ts/component/menu/block/mention.tsx index d256ab5188..129f0f3ccd 100644 --- a/src/ts/component/menu/block/mention.tsx +++ b/src/ts/component/menu/block/mention.tsx @@ -51,41 +51,14 @@ const MenuBlockMention = observer(class MenuBlockMention extends React.Component }; const type = S.Record.getTypeById(item.type); + const object = ![ 'add', 'selectDate' ].includes(item.id) ? item : null; const cn = []; - let content = null; - if (item.isDiv) { - content = ( - <div className="separator" style={param.style}> - <div className="inner" /> - </div> - ); - } else { - if (item.id == 'add') { - cn.push('add'); - }; - if (item.isHidden) { - cn.push('isHidden'); - }; - - let object = null; - if (![ 'add', 'selectDate' ].includes(item.id)) { - object = item; - }; - - content = ( - <MenuItemVertical - id={item.id} - object={object} - icon={item.icon} - name={<ObjectName object={item} />} - onMouseEnter={e => this.onOver(e, item)} - onClick={e => this.onClick(e, item)} - caption={type ? type.name : undefined} - style={param.style} - className={cn.join(' ')} - /> - ); + if (item.id == 'add') { + cn.push('add'); + }; + if (item.isHidden) { + cn.push('isHidden'); }; return ( @@ -96,7 +69,18 @@ const MenuBlockMention = observer(class MenuBlockMention extends React.Component columnIndex={0} rowIndex={param.index} > - {content} + <MenuItemVertical + id={item.id} + object={object} + icon={item.icon} + name={<ObjectName object={item} />} + onMouseEnter={e => this.onOver(e, item)} + onClick={e => this.onClick(e, item)} + caption={type?.name} + style={param.style} + isDiv={item.isDiv} + className={cn.join(' ')} + /> </CellMeasurer> ); }; @@ -367,7 +351,9 @@ const MenuBlockMention = observer(class MenuBlockMention extends React.Component data: { rebind: this.rebind, canEdit: true, + canClear: false, value: U.Date.now(), + relationKey: J.Relation.key.mention, onChange: (value: number) => { C.ObjectDateByTimestamp(space, value, (message: any) => { if (!message.error.code) { diff --git a/src/ts/component/menu/block/relation/edit.tsx b/src/ts/component/menu/block/relation/edit.tsx index 8dcae5380c..847ea65fba 100644 --- a/src/ts/component/menu/block/relation/edit.tsx +++ b/src/ts/component/menu/block/relation/edit.tsx @@ -16,7 +16,6 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React super(props); this.onRelationType = this.onRelationType.bind(this); - this.onDateSettings = this.onDateSettings.bind(this); this.onObjectType = this.onObjectType.bind(this); this.onSubmit = this.onSubmit.bind(this); this.onOpen = this.onOpen.bind(this); @@ -43,6 +42,14 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React let canDelete = !noDelete; let opts: any = null; let unlinkText = ''; + let name = ''; + + if (relation) { + name = relation.name; + } else + if (data.filter) { + name = data.filter; + }; if (readonly) { canDuplicate = canDelete = false; @@ -108,7 +115,7 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React <div className="inputWrap"> <Input ref={ref => this.ref = ref} - value={relation ? relation.name : ''} + value={name} onChange={this.onChange} onMouseEnter={this.menuClose} /> @@ -218,11 +225,11 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React e.preventDefault(); e.stopPropagation(); - const { param, getId } = this.props; + const { id, param, getId } = this.props; const { data } = param; const relation = this.getRelation(); - - if (relation) { + + if (relation || S.Menu.isAnimating(id)) { return; }; @@ -246,13 +253,13 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React e.preventDefault(); e.stopPropagation(); - const { param, getSize } = this.props; + const { id ,param, getSize } = this.props; const { data } = param; const { rootId } = data; const { getId } = this.props; const type = S.Record.getTypeType(); - if (!type) { + if (!type || S.Menu.isAnimating(id)) { return; }; @@ -299,27 +306,6 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React }); }; - onDateSettings (e: any) { - e.preventDefault(); - e.stopPropagation(); - - const { param, getId } = this.props; - const { data } = param; - const relation = this.getRelation(); - - if (relation && relation.isReadonlyRelation) { - return; - }; - - this.menuOpen('dataviewDate', { - element: `#${getId()} #item-date-settings`, - onClose: () => { - S.Menu.close('select'); - }, - data, - }); - }; - onKeyDown (e: any) { keyboard.shortcut('enter', e, (pressed: string) => { this.onSubmit(e); @@ -351,7 +337,7 @@ const MenuBlockRelationEdit = observer(class MenuBlockRelationEdit extends React rebind: this.rebind, }); - if (!S.Menu.isOpen(id)) { + if (!S.Menu.isOpen(id) && !S.Menu.isAnimating(id)) { S.Menu.closeAll(J.Menu.relationEdit, () => { S.Menu.open(id, options); }); diff --git a/src/ts/component/menu/button.tsx b/src/ts/component/menu/button.tsx index b85cc8c6c4..444909648d 100644 --- a/src/ts/component/menu/button.tsx +++ b/src/ts/component/menu/button.tsx @@ -1,59 +1,18 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; import { MenuItemVertical } from 'Component'; import { I } from 'Lib'; -const MenuButton = observer(class MenuButton extends React.Component<I.Menu> { +const MenuButton = observer(forwardRef<I.MenuRef, I.Menu>((props, ref) => { + const { param, close } = props; + const { data } = param; + const { disabled, onSelect, noClose } = data; - _isMounted = false; - - constructor (props: I.Menu) { - super(props); - - this.onSelect = this.onSelect.bind(this); - }; - - render () { - const { param } = this.props; - const { data } = param; - const { disabled } = data; - const items = this.getItems(); - - return ( - <div className="items"> - {items.map((item: any, i: number) => ( - <MenuItemVertical - key={i} - {...item} - className={disabled ? 'disabled' : ''} - onClick={e => this.onSelect(e, item)} - /> - ))} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentWillUnmount () { - this._isMounted = false; + const getItems = () => { + return props.param.data.options || []; }; - getItems () { - const { param } = this.props; - const { data } = param; - const { options } = data; - - return options || []; - }; - - onSelect (e: any, item: any) { - const { param, close } = this.props; - const { data } = param; - const { disabled, onSelect, noClose } = data; - + const onClick = (e: any, item: any) => { if (!noClose) { close(); }; @@ -63,6 +22,20 @@ const MenuButton = observer(class MenuButton extends React.Component<I.Menu> { }; }; -}); + const items = getItems(); + + return ( + <div className="items"> + {items.map((item: any, i: number) => ( + <MenuItemVertical + key={i} + {...item} + className={disabled ? 'disabled' : ''} + onClick={e => onClick(e, item)} + /> + ))} + </div> + ); +})); export default MenuButton; \ No newline at end of file diff --git a/src/ts/component/menu/dataview/calendar.tsx b/src/ts/component/menu/dataview/calendar.tsx index 94b7d9c4f9..8b3188bb59 100644 --- a/src/ts/component/menu/dataview/calendar.tsx +++ b/src/ts/component/menu/dataview/calendar.tsx @@ -1,22 +1,39 @@ import * as React from 'react'; -import { I, S, U, translate } from 'Lib'; +import { I, S, U, J, translate, keyboard } from 'Lib'; import { Select } from 'Component'; import { observer } from 'mobx-react'; -const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> { - +interface State { + dotMap: Map<string, boolean>; + selectedDate: ReturnType<typeof U.Date.getCalendarDateParam>; +}; + +enum ArrowDirection { + Up = 'arrowup', + Down = 'arrowdown', + Left = 'arrowleft', + Right = 'arrowright', +} + +const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu, State> { + originalValue = 0; refMonth: any = null; refYear: any = null; - + + state: Readonly<State> = { + dotMap: new Map(), + selectedDate: null, + }; + render () { const { param } = this.props; const { data, classNameWrap } = param; const { value, isEmpty, canEdit, canClear = true } = data; + const { dotMap, selectedDate } = this.state; const items = this.getData(); const { m, y } = U.Date.getCalendarDateParam(value); const todayParam = U.Date.getCalendarDateParam(this.originalValue); - const now = U.Date.now(); const tomorrow = now + 86400; const dayToday = U.Date.today(); @@ -41,14 +58,14 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> <div className="head"> <div className="sides"> <div className="side left"> - <Select + <Select ref={ref => this.refMonth = ref} id="month" - value={String(m || '')} - options={months} - onChange={m => this.setValue(U.Date.timestamp(y, m, 1), false, false)} - menuParam={{ - classNameWrap, + value={String(m || '')} + options={months} + onChange={m => this.setValue(U.Date.timestamp(y, m, 1), false, false)} + menuParam={{ + classNameWrap, width: 124, }} /> @@ -91,18 +108,30 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> if (!isEmpty && (todayParam.d == item.d) && (todayParam.m == item.m) && (todayParam.y == item.y)) { cn.push('active'); }; + + if (selectedDate && (selectedDate.d == item.d) && (selectedDate.m == item.m) && (selectedDate.y == item.y)) { + cn.push('selected'); + }; + + const check = dotMap.get([ item.d, item.m, item.y ].join('-')); return ( - <div - key={i} + <div + key={i} id={[ 'day', item.d, item.m, item.y ].join('-')} - className={cn.join(' ')} - onClick={(e: any) => { - e.stopPropagation(); - this.setValue(U.Date.timestamp(item.y, item.m, item.d), true, true); + className={cn.join(' ')} + onClick={e => this.onClick(e, item)} + onMouseEnter={() => { + if (!keyboard.isMouseDisabled) { + this.setState({ selectedDate: item }); + }; }} + onMouseLeave={() => this.setState({ selectedDate: null })} onContextMenu={e => this.onContextMenu(e, item)} > - {item.d} + <div className="inner"> + {item.d} + {check ? <div className="bullet" /> : ''} + </div> </div> ); })} @@ -130,12 +159,86 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> componentDidMount(): void { const { param } = this.props; const { data } = param; - const { value } = data; + const { value, noKeyboard } = data; this.originalValue = value; + + const selectedDate = U.Date.getCalendarDateParam(value); + this.setState({ + selectedDate, + }); + + this.initDotMap(); + if (!noKeyboard) { + this.rebind(); + }; this.forceUpdate(); }; + componentWillUnmount () { + const { param } = this.props; + const { data } = param; + const { noKeyboard } = data; + + if (!noKeyboard) { + this.unbind(); + }; + }; + + rebind () { + this.unbind(); + $(window).on('keydown.menu', e => this.onKeyDown(e)); + }; + + unbind () { + $(window).off('keydown.menu'); + }; + + onKeyDown = (e: any) => { + e.stopPropagation(); + keyboard.disableMouse(true); + + keyboard.shortcut('arrowup, arrowdown, arrowleft, arrowright', e, (pressed: string) => { + e.preventDefault(); + + this.onArrow(pressed as ArrowDirection); + }); + + const { selectedDate } = this.state; + + if (selectedDate) { + keyboard.shortcut('enter', e, () => this.onClick(e, selectedDate)); + }; + }; + + onArrow = (dir: ArrowDirection) => { + const num = [ ArrowDirection.Up, ArrowDirection.Down ].includes(dir) ? 7 : 1; + const d = [ ArrowDirection.Up, ArrowDirection.Left ].includes(dir) ? -1 : 1; + const daysDelta = num * d; + + const { param } = this.props; + const { data } = param; + const { value } = data; + const currentMonth = U.Date.getCalendarDateParam(value).m; + + const { selectedDate } = this.state; + if (!selectedDate) { + return; + }; + + const newDateValue = U.Date.timestamp(selectedDate.y, selectedDate.m, selectedDate.d, 12) + daysDelta * 86400; + const newCalendarDate = U.Date.getCalendarDateParam(newDateValue); + + const hasAnotherMonth = newCalendarDate.m != currentMonth; + if (hasAnotherMonth) { + this.setValue(newDateValue, false, false); + }; + + this.setState({ + selectedDate: newCalendarDate, + }); + }; + componentDidUpdate () { const { param } = this.props; const { data } = param; @@ -148,11 +251,49 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> this.props.position(); }; + initDotMap () { + const { param } = this.props; + const { data } = param; + const { getDotMap } = data; + const items = this.getData(); + + if (!getDotMap || !items.length) { + return; + }; + + const first = items[0]; + const last = items[items.length - 1]; + const start = U.Date.timestamp(first.y, first.m, first.d); + const end = U.Date.timestamp(last.y, last.m, last.d); + + getDotMap(start, end, dotMap => this.setState({ dotMap })); + }; + + onClick = (e: any, item: any) => { + e.stopPropagation(); + + this.setOrOpenDate(item); + }; + + setOrOpenDate = (item: any) => { + const { param } = this.props; + const { data } = param; + const { canEdit, relationKey } = data; + const ts = U.Date.timestamp(item.y, item.m, item.d); + + if (canEdit) { + this.setValue(ts, true, true); + } else { + U.Object.openDateByTimestamp(relationKey, ts); + }; + }; + onContextMenu (e: any, item: any) { e.preventDefault(); const { getId, param } = this.props; - const { className, classNameWrap } = param; + const { className, classNameWrap, data } = param; + const { relationKey } = data; S.Menu.open('select', { element: `#${getId()} #${[ 'day', item.d, item.m, item.y ].join('-')}`, @@ -161,11 +302,11 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> className, classNameWrap, data: { - options: [ + options: [ { id: 'open', icon: 'expand', name: translate('commonOpenObject') }, ], onSelect: () => { - U.Object.openDateByTimestamp(U.Date.timestamp(item.y, item.m, item.d)); + U.Object.openDateByTimestamp(relationKey, U.Date.timestamp(item.y, item.m, item.d)); } } }); @@ -182,7 +323,7 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> S.Menu.updateData(id, { value }); - if (save) { + if (save && onChange) { onChange(value); }; @@ -195,11 +336,11 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> const { param } = this.props; const { data } = param; const { value } = data; - + return U.Date.getCalendarMonth(value); }; - stepMonth (value: number, dir: number) { + stepMonth = (value: number, dir: number) => { const { m, y } = U.Date.getCalendarDateParam(value); let nY = y; @@ -216,7 +357,7 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu> return U.Date.timestamp(nY, nM, 1); }; - + }); export default MenuCalendar; diff --git a/src/ts/component/menu/dataview/calendar/day.tsx b/src/ts/component/menu/dataview/calendar/day.tsx index 4ff741b74b..e8d6a7e7ad 100644 --- a/src/ts/component/menu/dataview/calendar/day.tsx +++ b/src/ts/component/menu/dataview/calendar/day.tsx @@ -10,7 +10,8 @@ const MenuCalendarDay = observer(class MenuCalendarDay extends React.Component<I render () { const { param, getId } = this.props; const { data } = param; - const { y, m, d, hideIcon, className, fromWidget, readonly, onCreate } = data; + const { y, m, d, hideIcon, className, fromWidget, relationKey } = data; + const timestamp = U.Date.timestamp(y, m, d); const items = this.getItems(); const cn = [ 'wrap' ]; const menuId = getId(); @@ -19,8 +20,7 @@ const MenuCalendarDay = observer(class MenuCalendarDay extends React.Component<I let size = 16; if (fromWidget) { - const w = Number(U.Date.date('N', U.Date.timestamp(y, m, d))) + 1; - label = `${translate(`day${w}`)} ${d}`; + label = `${U.Date.date('l, M j', timestamp)}`; size = 18; }; @@ -60,7 +60,7 @@ const MenuCalendarDay = observer(class MenuCalendarDay extends React.Component<I return ( <div className={cn.join(' ')}> - <div className="number"> + <div className="number" onClick={() => U.Object.openDateByTimestamp(relationKey, timestamp, 'config')}> <div className="inner">{label}</div> </div> <div className="items"> diff --git a/src/ts/component/menu/dataview/context.tsx b/src/ts/component/menu/dataview/context.tsx index 0689025c6c..847c6a5863 100644 --- a/src/ts/component/menu/dataview/context.tsx +++ b/src/ts/component/menu/dataview/context.tsx @@ -21,25 +21,15 @@ class MenuContext extends React.Component<I.Menu> { <div id={'section-' + item.id} className="section"> {item.name ? <div className="name">{item.name}</div> : ''} <div className="items"> - {item.children.map((action: any, i: number) => { - if (action.isDiv) { - return ( - <div key={i} className="separator"> - <div className="inner" /> - </div> - ); - }; - - return ( - <MenuItemVertical - key={i} - {...action} - icon={action.icon || action.id} - onMouseEnter={e => this.onMouseEnter(e, action)} - onClick={e => this.onClick(e, action)} - /> - ); - })} + {item.children.map((action: any, i: number) => ( + <MenuItemVertical + key={i} + {...action} + icon={action.icon || action.id} + onMouseEnter={e => this.onMouseEnter(e, action)} + onClick={e => this.onClick(e, action)} + /> + ))} </div> </div> ); @@ -271,6 +261,7 @@ class MenuContext extends React.Component<I.Menu> { filter: '', filters: [ { relationKey: 'recommendedLayout', condition: I.FilterCondition.In, value: U.Object.getPageLayouts() }, + { relationKey: 'uniqueKey', condition: I.FilterCondition.NotEqual, value: J.Constant.typeKey.template }, ], onClick: (item: any) => { C.ObjectListSetObjectType(objectIds, item.uniqueKey); @@ -445,6 +436,10 @@ class MenuContext extends React.Component<I.Menu> { }; case 'createWidget': { + if (!first) { + break; + }; + const firstBlock = S.Block.getFirstBlock(S.Block.widgets, 1, it => it.isWidget()); Action.createWidgetFromObject(first.id, first.id, firstBlock?.id, I.BlockPosition.Top, analytics.route.addWidgetMenu); @@ -468,4 +463,4 @@ class MenuContext extends React.Component<I.Menu> { }; -export default MenuContext; +export default MenuContext; \ No newline at end of file diff --git a/src/ts/component/menu/dataview/date.tsx b/src/ts/component/menu/dataview/date.tsx deleted file mode 100644 index 076b57cfe2..0000000000 --- a/src/ts/component/menu/dataview/date.tsx +++ /dev/null @@ -1,200 +0,0 @@ -import * as React from 'react'; -import $ from 'jquery'; -import { observer } from 'mobx-react'; -import { MenuItemVertical } from 'Component'; -import { I, C, S, U, keyboard, translate } from 'Lib'; - -const MenuDataviewDate = observer(class MenuDataviewDate extends React.Component<I.Menu> { - - _isMounted = false; - n = -1; - - constructor (props: I.Menu) { - super(props); - - this.rebind = this.rebind.bind(this); - }; - - render () { - const sections = this.getSections(); - - const Section = (item: any) => ( - <div> - {item.name ? <div className="sectionName">{item.name}</div> : ''} - <div className="items"> - {item.children.map((action: any, i: number) => ( - <MenuItemVertical - key={i} - {...action} - onMouseEnter={e => this.onMouseEnter(e, action)} - /> - ))} - </div> - </div> - ); - - return ( - <div className="items"> - {sections.map((item: any, i: number) => ( - <Section key={i} {...item} /> - ))} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - this.rebind(); - }; - - componentDidUpdate () { - this.props.setActive(); - this.props.position(); - }; - - componentWillUnmount () { - this._isMounted = false; - }; - - rebind () { - this.unbind(); - $(window).on('keydown.menu', e => this.props.onKeyDown(e)); - window.setTimeout(() => this.props.setActive(), 15); - }; - - unbind () { - $(window).off('keydown.menu'); - }; - - getSections () { - const { param } = this.props; - const { data } = param; - const { getView, relationId } = data; - const relation = S.Record.getRelationById(relationId); - const dateOptions = this.getOptions('dateFormat'); - const timeOptions = this.getOptions('timeFormat'); - - if (!relation) { - return []; - }; - - let df = null; - let tf = null; - let dateFormat = null; - let timeFormat = null; - - if (getView) { - const view = getView(); - const vr = view.getRelation(relation.relationKey); - - if (vr) { - df = vr.dateFormat; - tf = vr.timeFormat; - }; - } else { - df = relation.dateFormat; - tf = relation.timeFormat; - }; - - dateFormat = dateOptions.find(it => it.id == df) || dateOptions[0]; - timeFormat = timeOptions.find(it => it.id == tf) || timeOptions[0]; - - let sections = [ - { - id: 'date', name: translate('menuDataviewDateDateFormat'), children: [ - { id: 'dateFormat', name: dateFormat?.name, arrow: true } - ] - }, - { - id: 'time', name: translate('menuDataviewDateTimeFormat'), children: [ - { id: 'timeFormat', name: timeFormat?.name, arrow: true } - ] - }, - ]; - - sections = U.Menu.sectionsMap(sections); - return sections; - }; - - getItems () { - const sections = this.getSections(); - - let items: any[] = []; - for (const section of sections) { - items = items.concat(section.children); - }; - - return items; - }; - - getOptions (key: string) { - let options: any[] = []; - switch (key) { - case 'dateFormat': { - options = U.Menu.dateFormatOptions(); - break; - }; - - case 'timeFormat': { - options = U.Menu.timeFormatOptions(); - break; - }; - }; - return options; - }; - - onOver (e: any, item: any) { - const { param, getId, getSize, close } = this.props; - const { data, classNameWrap } = param; - const { rootId, blockId, relationKey, getView } = data; - const options = this.getOptions(item.itemId); - - let relation = null; - let view = null; - let value = null; - - if (getView) { - view = getView(); - relation = view.getRelation(relationKey); - } else { - relation = S.Record.getRelationByKey(relationKey); - }; - - if (relation) { - value = options.find(it => it.id == relation[item.itemId]); - } else if (options.length) { - value = options[0]; - }; - - S.Menu.open('select', { - element: `#${getId()} #item-${item.id}`, - offsetX: getSize().width, - offsetY: -38, - isSub: true, - passThrough: true, - classNameWrap, - data: { - rebind: this.rebind, - value: value.id, - options, - onSelect: (e: any, el: any) => { - if (view) { - relation[item.itemId] = el.id; - C.BlockDataviewViewRelationReplace(rootId, blockId, view.id, relationKey, relation); - }; - close(); - } - } - }); - }; - - onMouseEnter (e: any, item: any) { - if (!keyboard.isMouseDisabled) { - this.props.setActive(item, false); - this.onOver(e, item); - }; - }; - -}); - -export default MenuDataviewDate; \ No newline at end of file diff --git a/src/ts/component/menu/dataview/file/list.tsx b/src/ts/component/menu/dataview/file/list.tsx index 71b966a267..cccf3e20e6 100644 --- a/src/ts/component/menu/dataview/file/list.tsx +++ b/src/ts/component/menu/dataview/file/list.tsx @@ -58,27 +58,6 @@ const MenuDataviewFileList = observer(class MenuDataviewFileList extends React.C const type = S.Record.getTypeById(item.type); - let content = null; - if (item.isDiv) { - content = ( - <div className="separator" style={param.style}> - <div className="inner" /> - </div> - ); - } else { - content = ( - <MenuItemVertical - id={item.id} - object={item} - name={item.name} - onMouseEnter={e => this.onOver(e, item)} - onClick={e => this.onClick(e, item)} - caption={type ? type.name : undefined} - style={param.style} - /> - ); - }; - return ( <CellMeasurer key={param.key} @@ -87,7 +66,15 @@ const MenuDataviewFileList = observer(class MenuDataviewFileList extends React.C columnIndex={0} rowIndex={param.index} > - {content} + <MenuItemVertical + {...item} + object={item} + name={item.name} + onMouseEnter={e => this.onOver(e, item)} + onClick={e => this.onClick(e, item)} + caption={type?.name} + style={param.style} + /> </CellMeasurer> ); }; diff --git a/src/ts/component/menu/dataview/filter/values.tsx b/src/ts/component/menu/dataview/filter/values.tsx index 7a6eecc783..b4a8fd9ba7 100644 --- a/src/ts/component/menu/dataview/filter/values.tsx +++ b/src/ts/component/menu/dataview/filter/values.tsx @@ -629,7 +629,10 @@ const MenuDataviewFilterValues = observer(class MenuDataviewFilterValues extends }; onCalendar (value: number) { - const { getId } = this.props; + const { getId, param } = this.props; + const { data } = param; + const { getView, itemId } = data; + const item = getView().getFilter(itemId); S.Menu.open('dataviewCalendar', { element: `#${getId()} #value`, @@ -638,6 +641,7 @@ const MenuDataviewFilterValues = observer(class MenuDataviewFilterValues extends rebind: this.rebind, value, canEdit: true, + relationKey: item.relationKey, onChange: (value: number) => { this.onChange('value', value); }, diff --git a/src/ts/component/menu/dataview/relation/edit.tsx b/src/ts/component/menu/dataview/relation/edit.tsx index 0305928764..5f9acca57d 100644 --- a/src/ts/component/menu/dataview/relation/edit.tsx +++ b/src/ts/component/menu/dataview/relation/edit.tsx @@ -41,6 +41,14 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component }; let opts = null; + let name = ''; + + if (relation) { + name = relation.name; + } else + if (data.filter) { + name = data.filter; + }; if (isObject && !isReadonly && (!relation || !relation.isReadonlyValue)) { const length = this.objectTypes.length; @@ -100,7 +108,7 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component <div className="inputWrap"> <Input ref={ref => this.ref = ref} - value={relation ? relation.name : ''} + value={name} onChange={this.onChange} onMouseEnter={this.menuClose} /> @@ -117,7 +125,7 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component <div className="name">{translate('menuDataviewRelationEditRelationType')}</div> <MenuItemVertical id="relation-type" - icon={this.format === null ? undefined : 'relation ' + Relation.className(this.format)} + icon={this.format === null ? undefined : `relation ${Relation.className(this.format)}`} name={this.format === null ? translate('menuDataviewRelationEditSelectRelationType') : translate('relationName' + this.format)} onMouseEnter={this.onRelationType} readonly={isReadonly} @@ -259,7 +267,7 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component }, { children: [ - canCalculate ? { id: 'calculate', icon: 'relation c-number', name: translate('menuDataviewRelationEditCalculate'), arrow: true } : null, + canCalculate ? { id: 'calculate', icon: 'relation c-number', name: translate('commonCalculate'), arrow: true } : null, ] }, ]); @@ -341,7 +349,7 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component return; }; - const options = Relation.formulaByType(relation.format).filter(it => it.section == item.id); + const options = Relation.formulaByType(relation.relationKey, relation.format).filter(it => it.section == item.id); S.Menu.closeAll([ 'select2' ], () => { S.Menu.open('select2', { @@ -508,11 +516,11 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component e.preventDefault(); e.stopPropagation(); - const { param, getId } = this.props; + const { id, param, getId } = this.props; const { data } = param; const relation = this.getViewRelation(); - if (relation) { + if (relation || S.Menu.isAnimating(id)) { return; }; @@ -536,11 +544,11 @@ const MenuRelationEdit = observer(class MenuRelationEdit extends React.Component e.preventDefault(); e.stopPropagation(); - const { param, getSize } = this.props; + const { id, param, getSize } = this.props; const { data } = param; const { rootId } = data; - if (this.isReadonly()) { + if (this.isReadonly() || S.Menu.isAnimating(id)) { return; }; diff --git a/src/ts/component/menu/dataview/sort.tsx b/src/ts/component/menu/dataview/sort.tsx index f33b6dece3..90ca9e5f61 100644 --- a/src/ts/component/menu/dataview/sort.tsx +++ b/src/ts/component/menu/dataview/sort.tsx @@ -45,8 +45,8 @@ const MenuSort = observer(class MenuSort extends React.Component<I.Menu> { const sortCnt = items.length; const typeOptions = [ - { id: String(I.SortType.Asc), name: translate('commonAscending') }, - { id: String(I.SortType.Desc), name: translate('commonDescending') }, + { id: I.SortType.Asc, name: translate('commonAscending') }, + { id: I.SortType.Desc, name: translate('commonDescending') }, ]; const Handle = SortableHandle(() => ( diff --git a/src/ts/component/menu/dataview/template/list.tsx b/src/ts/component/menu/dataview/template/list.tsx index 5b3bc45bd3..b9c089518c 100644 --- a/src/ts/component/menu/dataview/template/list.tsx +++ b/src/ts/component/menu/dataview/template/list.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import $ from 'jquery'; import { Icon, Title, PreviewObject, IconObject } from 'Component'; -import { I, C, S, U, J, translate, keyboard } from 'Lib'; +import { I, C, S, U, J, translate, keyboard, sidebar } from 'Lib'; import { observer } from 'mobx-react'; -const TEMPLATE_WIDTH = 230; +const TEMPLATE_WIDTH = 224; const MenuTemplateList = observer(class MenuTemplateList extends React.Component<I.Menu> { @@ -24,7 +24,6 @@ const MenuTemplateList = observer(class MenuTemplateList extends React.Component this.onType = this.onType.bind(this); this.setCurrent = this.setCurrent.bind(this); this.getTemplateId = this.getTemplateId.bind(this); - this.updateRowLength = this.updateRowLength.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.rebind = this.rebind.bind(this); }; @@ -140,6 +139,7 @@ const MenuTemplateList = observer(class MenuTemplateList extends React.Component componentWillUnmount () { C.ObjectSearchUnsubscribe([ this.getSubId() ]); + this.unbind(); }; rebind () { @@ -350,13 +350,6 @@ const MenuTemplateList = observer(class MenuTemplateList extends React.Component }); }; - updateRowLength (n: number) { - const node = $(this.node); - const items = node.find('.items'); - - items.css({ 'grid-template-columns': `repeat(${n}, 1fr)` }); - }; - beforePosition () { const { param, getId } = this.props; const { data } = param; diff --git a/src/ts/component/menu/dataview/view/settings.tsx b/src/ts/component/menu/dataview/view/settings.tsx index 657029c55a..28034fc806 100644 --- a/src/ts/component/menu/dataview/view/settings.tsx +++ b/src/ts/component/menu/dataview/view/settings.tsx @@ -243,7 +243,7 @@ const MenuViewSettings = observer(class MenuViewSettings extends React.Component const layoutSettings = [ { id: 'layout', name: translate('menuDataviewObjectTypeEditLayout'), subComponent: 'dataviewViewLayout', caption: Dataview.defaultViewName(type) }, isBoard ? { id: 'group', name: translate('libDataviewGroups'), subComponent: 'dataviewGroupList' } : null, - { id: 'relations', name: translate('libDataviewRelations'), subComponent: 'dataviewRelationList', caption: relationCnt.join(', ') }, + { id: 'relations', name: translate('commonRelations'), subComponent: 'dataviewRelationList', caption: relationCnt.join(', ') }, ]; const tools = [ { id: 'filter', name: translate('menuDataviewViewFilter'), subComponent: 'dataviewFilterList', caption: filterCnt ? U.Common.sprintf(translate('menuDataviewViewApplied'), filterCnt) : '' }, diff --git a/src/ts/component/menu/help.tsx b/src/ts/component/menu/help.tsx index 9631982f06..bddd618491 100644 --- a/src/ts/component/menu/help.tsx +++ b/src/ts/component/menu/help.tsx @@ -1,72 +1,22 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import { MenuItemVertical, Button, ShareTooltip } from 'Component'; -import { I, S, U, J, Onboarding, keyboard, analytics, Action, Highlight, Storage, translate, Preview } from 'Lib'; +import { I, S, U, J, keyboard, analytics, Action, Highlight, translate } from 'Lib'; -class MenuHelp extends React.Component<I.Menu> { +const MenuHelp = forwardRef<I.MenuRef, I.Menu>((props, ref) => { + const { setActive, close, getId, onKeyDown } = props; + const n = useRef(-1); - n = -1; - - constructor (props: I.Menu) { - super(props); - - this.onClick = this.onClick.bind(this); - }; - - render () { - const items = this.getItems(); - - return ( - <React.Fragment> - <div className="items"> - {items.map((item: any, i: number) => { - let content = null; - - if (item.isDiv) { - content = ( - <div key={i} className="separator"> - <div className="inner" /> - </div> - ); - } else { - content = ( - <MenuItemVertical - key={i} - {...item} - onMouseEnter={e => this.onMouseEnter(e, item)} - onClick={e => this.onClick(e, item)} - /> - ); - }; - - return content; - })} - </div> - <ShareTooltip /> - </React.Fragment> - ); - }; - - componentDidMount () { - this.rebind(); - Preview.shareTooltipHide(); - Highlight.showAll(); - }; - - componentWillUnmount () { - this.unbind(); - }; - - rebind () { - this.unbind(); - $(window).on('keydown.menu', e => this.props.onKeyDown(e)); - window.setTimeout(() => this.props.setActive(), 15); + const rebind = () => { + unbind(); + $(window).on('keydown.menu', e => onKeyDown(e)); + window.setTimeout(() => setActive(), 15); }; - unbind () { + const unbind = () => { $(window).off('keydown.menu'); }; - getItems () { + const getItems = () => { const btn = <Button className="c16" text={U.Common.getElectron().version.app} />; return [ @@ -84,17 +34,13 @@ class MenuHelp extends React.Component<I.Menu> { ].map(it => ({ ...it, name: translate(U.Common.toCamelCase(`menuHelp-${it.id}`)) })); }; - onMouseEnter (e: any, item: any) { + const onMouseEnter = (e: any, item: any) => { if (!keyboard.isMouseDisabled) { - this.props.setActive(item, false); + setActive(item, false); }; }; - onClick (e: any, item: any) { - const { getId, close } = this.props; - const isGraph = keyboard.isMainGraph(); - const home = U.Space.getDashboard(); - + const onClick = (e: any, item: any) => { close(); analytics.event(U.Common.toUpperCamelCase([ getId(), item.id ].join('-')), { route: analytics.route.menuHelp }); @@ -102,12 +48,12 @@ class MenuHelp extends React.Component<I.Menu> { switch (item.id) { case 'whatsNew': { - S.Popup.open('help', { preventResize: true, data: { document: item.document } }); + S.Popup.open('help', { data: { document: item.document } }); break; }; case 'shortcut': { - S.Popup.open('shortcut', { preventResize: true }); + S.Popup.open('shortcut', {}); break; }; @@ -134,6 +80,40 @@ class MenuHelp extends React.Component<I.Menu> { }; -}; + const items = getItems(); -export default MenuHelp; + useEffect(() => { + rebind(); + Highlight.showAll(); + return () => unbind(); + }, []); + + useImperativeHandle(ref, () => ({ + rebind, + unbind, + getItems, + getIndex: () => n.current, + setIndex: (i: number) => n.current = i, + onClick, + }), []); + + return ( + <> + <div className="items"> + {items.map((item: any, i: number) => ( + <MenuItemVertical + key={i} + {...item} + onMouseEnter={e => onMouseEnter(e, item)} + onClick={e => onClick(e, item)} + /> + ))} + </div> + + <ShareTooltip route={analytics.route.menuHelp} /> + </> + ); + +}); + +export default MenuHelp; \ No newline at end of file diff --git a/src/ts/component/menu/index.tsx b/src/ts/component/menu/index.tsx index 5a70a5d023..316b7bf8b9 100644 --- a/src/ts/component/menu/index.tsx +++ b/src/ts/component/menu/index.tsx @@ -8,6 +8,7 @@ import { I, S, U, J, keyboard, analytics, Storage, sidebar } from 'Lib'; import MenuHelp from './help'; import MenuOnboarding from './onboarding'; import MenuParticipant from './participant'; +import MenuPublish from './publish'; import MenuSelect from './select'; import MenuButton from './button'; @@ -63,7 +64,6 @@ import MenuDataviewCalendar from './dataview/calendar'; import MenuDataviewCalendarDay from './dataview/calendar/day'; import MenuDataviewOptionList from './dataview/option/list'; import MenuDataviewOptionEdit from './dataview/option/edit'; -import MenuDataviewDate from './dataview/date'; import MenuDataviewText from './dataview/text'; import MenuDataviewSource from './dataview/source'; import MenuDataviewContext from './dataview/context'; @@ -88,6 +88,7 @@ const Components: any = { help: MenuHelp, onboarding: MenuOnboarding, participant: MenuParticipant, + publish: MenuPublish, select: MenuSelect, button: MenuButton, @@ -143,7 +144,6 @@ const Components: any = { dataviewViewLayout: MenuDataviewViewLayout, dataviewCalendar: MenuDataviewCalendar, dataviewCalendarDay: MenuDataviewCalendarDay, - dataviewDate: MenuDataviewDate, dataviewText: MenuDataviewText, dataviewSource: MenuDataviewSource, dataviewContext: MenuDataviewContext, @@ -450,14 +450,7 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { }; getBorderBottom () { - const { id } = this.props; - - let ret = Number(window.AnytypeGlobalConfig?.menuBorderBottom) || 80; - if ([ 'help', 'onboarding', 'searchObjectWidgetAdd' ].includes(id)) { - ret = 16; - }; - - return ret; + return Number(window.AnytypeGlobalConfig?.menuBorderBottom) || J.Size.menuBorder; }; getBorderLeft () { @@ -745,6 +738,22 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { }; }; + getIndex (): number { + if (!this.ref) { + return -1; + }; + + return this.ref.getIndex ? this.ref.getIndex() : this.ref.n; + }; + + setIndex (n: number) { + if (!this.ref) { + return; + }; + + this.ref.setIndex ? this.ref.setIndex(n) : this.ref.n = n; + }; + onKeyDown (e: any) { if (!this.ref || !this.ref.getItems || keyboard.isComposition) { return; @@ -759,17 +768,18 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { const refInput = this.ref.refFilter || this.ref.refName; const shortcutClose = [ 'escape' ]; const shortcutSelect = [ 'tab', 'enter' ]; - + + let index = this.getIndex(); let ret = false; if (refInput) { - if (refInput.isFocused && (this.ref.n < 0)) { + if (refInput.isFocused && (index < 0)) { keyboard.shortcut('arrowleft, arrowright', e, () => ret = true); keyboard.shortcut('arrowdown', e, () => { refInput.blur(); - this.ref.n = 0; + this.setIndex(0); this.setActive(null, true); ret = true; @@ -793,7 +803,7 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { return; }; - this.ref.n = this.ref.getItems().length - 1; + this.setIndex(this.ref.getItems().length - 1); this.setActive(null, true); refInput.blur(); @@ -801,9 +811,10 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { }); } else { keyboard.shortcut('arrowup', e, () => { - if (!this.ref.n) { - this.ref.n = -1; + if (index < 0) { refInput.focus(); + + this.setIndex(-1); this.setActive(null, true); ret = true; @@ -832,26 +843,29 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { const items = this.ref.getItems(); const l = items.length; - const item = items[this.ref.n]; + + index = this.getIndex(); + const item = items[index]; const onArrow = (dir: number) => { - this.ref.n += dir; + index += dir; - if (this.ref.n < 0) { - if ((this.ref.n == -1) && refInput) { - this.ref.n = -1; + if (index < 0) { + if ((index == -1) && refInput) { + index = -1; refInput.focus(); } else { - this.ref.n = l - 1; + index = l - 1; }; }; - if (this.ref.n > l - 1) { - this.ref.n = 0; + if (index > l - 1) { + index = 0; }; - const item = items[this.ref.n]; + this.setIndex(index); + const item = items[index]; if (!item) { return; }; @@ -861,7 +875,7 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { return; }; - this.setActive(null, true); + this.setActive(null, true, dir); if (!item.arrow && this.ref.onOver) { this.ref.onOver(e, item); @@ -899,7 +913,7 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { keyboard.shortcut('backspace', e, () => { e.preventDefault(); - this.ref.n--; + this.setIndex(index - 1); this.checkIndex(); this.ref.onRemove(e, item); this.setActive(null, true); @@ -917,56 +931,62 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> { }; onSortMove (dir: number) { - const n = this.ref.n; + const index = this.getIndex(); - this.ref.n = n + dir; + this.setIndex(index + dir); this.checkIndex(); - this.ref.onSortEnd({ oldIndex: n, newIndex: this.ref.n }); + this.ref.onSortEnd({ oldIndex: index, newIndex: index + dir }); }; checkIndex () { const items = this.ref.getItems(); + + let index = this.getIndex(); + index = Math.max(0, index); + index = Math.min(items.length - 1, index); - this.ref.n = Math.max(0, this.ref.n); - this.ref.n = Math.min(items.length - 1, this.ref.n); + this.setIndex(index); }; - setActive (item?: any, scroll?: boolean) { + setActive (item?: any, scroll?: boolean, dir?: number) { + dir = dir || 1; + if (!this.ref || !this.ref.getItems) { return; }; const refInput = this.ref.refFilter || this.ref.refName; - if ((this.ref.n == -1) && refInput) { + + let index = this.getIndex(); + if ((index < 0) && refInput) { refInput.focus(); }; const items = this.ref.getItems(); if (item && (undefined !== item.id)) { - this.ref.n = items.findIndex(it => it.id == item.id); + index = items.findIndex(it => it.id == item.id); }; if (this.ref.refList && scroll) { - let idx = this.ref.n; - if (this.ref.recalcIndex) { - idx = this.ref.recalcIndex(); - }; - this.ref.refList.scrollToRow(Math.max(0, idx)); + this.ref.refList.scrollToRow(Math.max(0, index)); }; - const next = items[this.ref.n]; + const next = items[index]; if (!next) { return; }; if (next.isDiv || next.isSection) { - this.ref.n++; - if (items[this.ref.n]) { - this.setActive(items[this.ref.n], scroll); + index += dir; + this.setIndex(index); + if (items[index]) { + this.setActive(items[index], scroll); }; } else { this.setHover(next, scroll); }; + + this.setIndex(index); }; setHover (item?: any, scroll?: boolean) { diff --git a/src/ts/component/menu/item/vertical.tsx b/src/ts/component/menu/item/vertical.tsx index 45574151eb..f0fd1e7628 100644 --- a/src/ts/component/menu/item/vertical.tsx +++ b/src/ts/component/menu/item/vertical.tsx @@ -9,13 +9,27 @@ class MenuItemVertical extends React.Component<I.MenuItem> { render () { const { - id, icon, object, inner, name, description, caption, color, arrow, checkbox, isActive, withDescription, withSwitch, withSelect, withMore, + icon, object, inner, name, description, caption, color, arrow, checkbox, isActive, withDescription, withSwitch, withSelect, withMore, className, style, iconSize, switchValue, selectValue, options, readonly, onClick, onSwitch, onSelect, onMouseEnter, onMouseLeave, onMore, - selectMenuParam, subComponent, note, sortArrow + selectMenuParam, subComponent, note, sortArrow, isDiv, isSection, index } = this.props; - const cn = [ 'item' ]; + const id = this.props.id || ''; + const cn = []; const withArrow = arrow || subComponent; + if (isDiv) { + cn.push('separator'); + } else + if (isSection) { + cn.push('sectionName'); + + if (!index) { + cn.push('first'); + }; + } else { + cn.push('item'); + }; + let hasClick = true; let iconMainElement = null; let iconSideElement = null; @@ -93,6 +107,12 @@ class MenuItemVertical extends React.Component<I.MenuItem> { }; let content = null; + if (isDiv) { + content = <div className="inner" />; + } else + if (isSection) { + content = name; + } else if (withDescription) { content = ( <React.Fragment> @@ -158,7 +178,7 @@ class MenuItemVertical extends React.Component<I.MenuItem> { return ( <div ref={node => this.node = node} - id={'item-' + id} + id={`item-${id}`} className={cn.join(' ')} onMouseDown={hasClick ? onClick : undefined} onMouseEnter={onMouseEnter} diff --git a/src/ts/component/menu/object.tsx b/src/ts/component/menu/object.tsx index f56345005c..eaeffb0341 100644 --- a/src/ts/component/menu/object.tsx +++ b/src/ts/component/menu/object.tsx @@ -87,6 +87,7 @@ class MenuObject extends React.Component<I.Menu> { }; getSections () { + const { config } = S.Common; const { param } = this.props; const { data } = param; const { blockId, rootId, isFilePreview } = data; @@ -113,16 +114,16 @@ class MenuObject extends React.Component<I.Menu> { let template = null; let setDefaultTemplate = null; - let pageExport = { id: 'pageExport', icon: 'export', name: translate('menuObjectExport') }; let print = { id: 'print', name: translate('menuObjectPrint'), caption: `${cmd} + P` }; let linkTo = { id: 'linkTo', icon: 'linkTo', name: translate('commonLinkTo'), arrow: true }; let addCollection = { id: 'addCollection', icon: 'collection', name: translate('commonAddToCollection'), arrow: true }; let search = { id: 'search', name: translate('menuObjectSearchOnPage'), caption: `${cmd} + F` }; let history = { id: 'history', name: translate('commonVersionHistory'), caption: (U.Common.isPlatformMac() ? `${cmd} + Y` : `Ctrl + H`) }; + let createWidget = { id: 'createWidget', icon: 'createWidget', name: translate('menuObjectCreateWidget') }; let pageCopy = { id: 'pageCopy', icon: 'copy', name: translate('commonDuplicate') }; let pageLink = { id: 'pageLink', icon: 'link', name: translate('commonCopyLink') }; let pageReload = { id: 'pageReload', icon: 'reload', name: translate('menuObjectReloadFromSource') }; - let createWidget = { id: 'createWidget', icon: 'createWidget', name: translate('menuObjectCreateWidget') }; + let pageExport = { id: 'pageExport', icon: 'export', name: translate('menuObjectExport') }; let downloadFile = { id: 'downloadFile', icon: 'download', name: translate('commonDownload') }; let openFile = { id: 'openFile', icon: 'expand', name: translate('menuObjectDownloadOpen') }; let openObject = { id: 'openAsObject', icon: 'expand', name: translate('commonOpenObject') }; @@ -176,7 +177,7 @@ class MenuObject extends React.Component<I.Menu> { const allowedSearch = !isFilePreview && !isInSetLayouts; const allowedHistory = !object.isArchived && !isInFileOrSystemLayouts && !isParticipant && !isDate && !object.templateIsBundled; const allowedFav = canWrite && !object.isArchived && !object.templateIsBundled; - const allowedLock = canWrite && !object.isArchived && S.Block.checkFlags(rootId, rootId, [ I.RestrictionObject.Details ]); + const allowedLock = canWrite && !object.isArchived && S.Block.checkFlags(rootId, rootId, [ I.RestrictionObject.Details ]) && !isInFileOrSystemLayouts; const allowedLinkTo = canWrite && !object.isArchived; const allowedAddCollection = canWrite && !object.isArchived; const allowedPageLink = !object.isArchived; @@ -381,6 +382,7 @@ class MenuObject extends React.Component<I.Menu> { const block = S.Block.getLeaf(rootId, blockId); const object = this.getObject(); const route = analytics.route.menuObject; + const space = U.Space.getSpaceview(); if (item.arrow) { return; @@ -442,7 +444,7 @@ class MenuObject extends React.Component<I.Menu> { S.Popup.open('export', { data: { objectIds: [ rootId ], allowHtml: true, route } }); break; }; - + case 'pageArchive': { Action.archive([ object.id ], route, () => { if (onArchive) { @@ -486,8 +488,24 @@ class MenuObject extends React.Component<I.Menu> { }; case 'pageLink': { - U.Common.copyToast(translate('commonLink'), `${J.Constant.protocol}://${U.Object.universalRoute(object)}`); - analytics.event('CopyLink', { route }); + const link = `${J.Constant.protocol}://${U.Object.universalRoute(object)}`; + const cb = (link: string) => { + U.Common.copyToast(translate('commonLink'), link); + analytics.event('CopyLink', { route }); + }; + + if (space.isShared) { + U.Space.getInvite(S.Common.space, (cid: string, key: string) => { + if (cid && key) { + cb(link + `&cid=${cid}&key=${key}`); + } else { + cb(link); + }; + }); + } else { + cb(link); + }; + break; }; @@ -567,4 +585,4 @@ class MenuObject extends React.Component<I.Menu> { }; -export default MenuObject; \ No newline at end of file +export default MenuObject; diff --git a/src/ts/component/menu/onboarding.tsx b/src/ts/component/menu/onboarding.tsx index 3d2bd538e5..b1c6742f56 100644 --- a/src/ts/component/menu/onboarding.tsx +++ b/src/ts/component/menu/onboarding.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; -import { Button, Icon, Label, EmailCollectionForm } from 'Component'; +import { Button, Icon, Label, EmailCollection } from 'Component'; import { I, C, S, U, J, Onboarding, analytics, keyboard, translate } from 'Lib'; import ReactCanvasConfetti from 'react-canvas-confetti'; @@ -90,7 +90,7 @@ const MenuOnboarding = observer(class MenuSelect extends React.Component<I.Menu, /> ) : ''} {withEmailForm ? ( - <EmailCollectionForm onStepChange={position} onComplete={() => close()} /> + <EmailCollection onStepChange={position} onComplete={() => close()} /> ) : ''} <div className={[ 'bottom', withSteps ? 'withSteps' : '' ].join(' ')}> @@ -170,11 +170,11 @@ const MenuOnboarding = observer(class MenuSelect extends React.Component<I.Menu, }; showElements () { - this.props.param.hiddenElements.forEach(el => $(el).css({ visibility: 'visible' })); + this.props.param.hiddenElements.forEach(el => $(el).removeClass('isOnboardingHidden')); }; hideElements () { - this.props.param.hiddenElements.forEach(el => $(el).css({ visibility: 'hidden' })); + this.props.param.hiddenElements.forEach(el => $(el).addClass('isOnboardingHidden')); }; initDimmer () { @@ -250,12 +250,10 @@ const MenuOnboarding = observer(class MenuSelect extends React.Component<I.Menu, onClose () { const { param, close } = this.props; - const { data, onClose } = param; + const { data } = param; const { key, current, isPopup } = data; const section = this.getSection(); - - - let menuParam = Onboarding.getParam(section, {}, isPopup); + const menuParam = Onboarding.getParam(section, {}, isPopup); close(); diff --git a/src/ts/component/menu/participant.tsx b/src/ts/component/menu/participant.tsx index 9bc3275b0d..9e30324147 100644 --- a/src/ts/component/menu/participant.tsx +++ b/src/ts/component/menu/participant.tsx @@ -1,50 +1,32 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { observer } from 'mobx-react'; import { ObjectName, ObjectDescription, Label, IconObject, EmptySearch } from 'Component'; import { I, U, translate } from 'Lib'; -const MenuParticipant = observer(class MenuParticipant extends React.Component<I.Menu> { +const MenuParticipant = observer(forwardRef<I.MenuRef, I.Menu>((props: I.Menu, ref: any) => { + useEffect(() => load(), []); - render () { - const object = this.getObject(); - - if (!object) { - return <EmptySearch text={translate('commonNotFound')} />; - }; - - const relationKey = object.globalName ? 'globalName': 'identity'; - - return ( - <React.Fragment> - <IconObject object={object} size={96} /> - <ObjectName object={object} /> - <Label text={U.Common.shorten(object[relationKey], 150)} /> - <ObjectDescription object={object} /> - </React.Fragment> - ); - }; - - componentDidMount () { - this.load(); - }; - - getObject () { - return this.props.param.data.object; - }; - - load () { - const object = this.getObject(); - if (!object) { - return; - }; + const { param } = props; + const { data } = param; + const { object } = data; + const load = () => { U.Object.getById(object.id, { keys: U.Data.participantRelationKeys() }, (object: any) => { if (object) { - this.props.param.data.object = object; + props.param.data.object = object; }; }); }; -}); + return object ? ( + <> + <IconObject object={object} size={96} /> + <ObjectName object={object} /> + <Label text={U.Common.shorten(object.resolvedName, 150)} /> + <ObjectDescription object={object} /> + </> + ) : <EmptySearch text={translate('commonNotFound')} />; + +})); export default MenuParticipant; \ No newline at end of file diff --git a/src/ts/component/menu/publish.tsx b/src/ts/component/menu/publish.tsx new file mode 100644 index 0000000000..3c609082d1 --- /dev/null +++ b/src/ts/component/menu/publish.tsx @@ -0,0 +1,71 @@ +import React, { forwardRef, useRef, useState } from 'react'; +import { Title, Input, Label, Switch, Button, Icon, Loader } from 'Component'; +import { J, U, I, S, Action, translate } from 'Lib'; + +const MenuPublish = forwardRef<I.MenuRef, I.Menu>((props, ref) => { + + const { param, close } = props; + const { data } = param; + const { rootId } = data; + const inputRef = useRef(null); + const space = U.Space.getSpaceview(); + const object = S.Detail.get(rootId, rootId, []); + const [ isLoading, setIsLoading ] = useState(false); + const participant = U.Space.getMyParticipant(); + const domain = U.Common.sprintf(J.Url.publish, participant.resolvedName); + const items = [ + (!space.isPersonal ? { + id: 'space', name: translate('popupSettingsSpaceIndexShareShareTitle'), onClick: () => { + S.Popup.open('settings', { data: { page: 'spaceShare', isSpace: true }, className: 'isSpace' }); + close(); + }, + } : null), + { + id: 'export', name: translate('popupExportTitle'), onClick: () => { + S.Popup.open('export', { data: { objectIds: [ rootId ], allowHtml: true } }); + close(); + }, + }, + ].filter(it => it); + + const onPublish = () => { + setIsLoading(true); + Action.publish(rootId, inputRef.current.getValue(), () => setIsLoading(false)); + }; + + return ( + <> + {isLoading ? <Loader /> : ''} + <Title text={translate('menuPublishTitle')} /> + <Input value={domain} readonly={true} /> + <Input + ref={inputRef} + value={U.Common.slug(object.name)} + focusOnMount={true} + /> + <Label className="small" text="https:/any.copp/kjshdfkjahsjdkhAJDH*78/rem-koolhaas-architects" /> + + <div className="flex"> + <Label text={translate('menuPublishLabel')} /> + <div className="value"> + <Switch /> + </div> + </div> + + <Button text={translate('menuPublishButton')} className="c36" onClick={onPublish} /> + + <div className="outer"> + {items.map((item, index) => ( + <div key={index} className="item" onClick={item.onClick}> + <Icon className={item.id} /> + <div className="name">{item.name}</div> + <Icon className="arrow" /> + </div> + ))} + </div> + </> + ); + +}); + +export default MenuPublish; diff --git a/src/ts/component/menu/quickCapture.tsx b/src/ts/component/menu/quickCapture.tsx index f3409c9507..68628d479e 100644 --- a/src/ts/component/menu/quickCapture.tsx +++ b/src/ts/component/menu/quickCapture.tsx @@ -201,14 +201,6 @@ class MenuQuickCapture extends React.Component<I.Menu, State> { this.unbind(); $(window).on('keydown.menu', e => this.onKeyDown(e)); window.setTimeout(() => setActive(), 15); - - if (S.Common.navigationMenu == I.NavigationMenuMode.Hover) { - $(`#${getId()}`).off(`mouseleave`).on(`mouseleave`, () => { - if (!this.state.isExpanded) { - close(); - }; - }); - }; }; unbind () { @@ -463,8 +455,9 @@ class MenuQuickCapture extends React.Component<I.Menu, State> { const object = message.details; U.Object.openAuto(object); + U.Object.setLastUsedDate(object.id, U.Date.now()); - analytics.createObject(object.type, object.layout, analytics.route.navigation, message.middleTime); + analytics.createObject(object.type, object.layout, '', message.middleTime); analytics.event('SelectObjectType', { objectType: object.type }); }); }; @@ -506,7 +499,7 @@ class MenuQuickCapture extends React.Component<I.Menu, State> { const canPin = type.isInstalled; const canDefault = type.isInstalled && !U.Object.isInSetLayouts(item.recommendedLayout) && (type.id != S.Common.type); const canDelete = type.isInstalled && S.Block.isAllowed(item.restrictions, [ I.RestrictionObject.Delete ]); - const route = analytics.route.navigation; + const route = ''; let options: any[] = [ canPin ? { id: 'pin', name: (isPinned ? translate('menuQuickCaptureUnpin') : translate('menuQuickCapturePin')) } : null, diff --git a/src/ts/component/menu/relation/suggest.tsx b/src/ts/component/menu/relation/suggest.tsx index 90a64f7add..39b9019146 100644 --- a/src/ts/component/menu/relation/suggest.tsx +++ b/src/ts/component/menu/relation/suggest.tsx @@ -67,20 +67,11 @@ const MenuRelationSuggest = observer(class MenuRelationSuggest extends React.Com <div className="name">{item.name}</div> </div> ); - } else - if (item.isDiv) { - content = ( - <div className="separator" style={param.style}> - <div className="inner" /> - </div> - ); - } else - if (item.isSection) { - content = <div className={[ 'sectionName', (param.index == 0 ? 'first' : '') ].join(' ')} style={param.style}>{item.name}</div>; } else { content = ( <MenuItemVertical {...item} + index={param.index} className={item.isHidden ? 'isHidden' : ''} style={param.style} onMouseEnter={e => this.onMouseEnter(e, item)} @@ -433,6 +424,15 @@ const MenuRelationSuggest = observer(class MenuRelationSuggest extends React.Com const { data, classNameWrap } = param; const { rootId, blockId, menuIdEdit, addCommand, ref, noInstall } = data; const object = S.Detail.get(rootId, rootId, [ 'type' ], true); + const onAdd = (item: any) => { + close(); + + if (addCommand && item) { + addCommand(rootId, blockId, item); + }; + + U.Object.setLastUsedDate(item.id, U.Date.now()); + }; if (item.id == 'add') { S.Menu.open(menuIdEdit, { @@ -445,23 +445,18 @@ const MenuRelationSuggest = observer(class MenuRelationSuggest extends React.Com ...data, rebind: this.rebind, onChange: () => close(), + addCommand: (rootId: string, blockId: string, item: any) => onAdd(item), } }); - } else - if (addCommand) { - const cb = (item: any) => { - close(); - addCommand(rootId, blockId, item); - }; - + } else { if (item.isInstalled || noInstall) { - cb(item); + onAdd(item); if (!noInstall) { analytics.event('AddExistingRelation', { format: item.format, type: ref, objectType: object.type, relationKey: item.relationKey }); }; } else { - Action.install(item, true, message => cb(message.details)); + Action.install(item, true, message => onAdd(message.details)); }; }; }; diff --git a/src/ts/component/menu/search/object.tsx b/src/ts/component/menu/search/object.tsx index c6da12e540..decb7db17d 100644 --- a/src/ts/component/menu/search/object.tsx +++ b/src/ts/component/menu/search/object.tsx @@ -65,50 +65,45 @@ const MenuSearchObject = observer(class MenuSearchObject extends React.Component const type = S.Record.getTypeById(item.type); const checkbox = value && value.length && value.includes(item.id); const cn = []; + const props = { + ...item, + object: (item.isAdd || item.isSection ? undefined : item), + }; - let content = null; + if (item.isAdd) { + cn.push('add'); + props.isAdd = true; + }; + if (item.isHidden) { + cn.push('isHidden'); + }; - if (item.isSection) { - content = <div className={[ 'sectionName', (param.index == 0 ? 'first' : '') ].join(' ')} style={param.style}>{item.name}</div>; - } else - if (item.isDiv) { - content = ( - <div className="separator" style={param.style}> - <div className="inner" /> - </div> - ); + if (isBig && !item.isAdd) { + props.withDescription = true; + props.iconSize = 40; } else { - const props = { - ...item, - object: (item.isAdd ? undefined : item), - }; - - if (item.isAdd) { - cn.push('add'); - props.isAdd = true; - }; - if (item.isHidden) { - cn.push('isHidden'); - }; - - if (isBig && !item.isAdd) { - props.withDescription = true; - props.iconSize = 40; - } else { - props.caption = (type ? type.name : undefined); - }; + props.caption = (type ? type.name : undefined); + }; - if (undefined !== item.caption) { - props.caption = item.caption; - }; + if (undefined !== item.caption) { + props.caption = item.caption; + }; - if (noIcon) { - props.object = undefined; - }; + if (noIcon) { + props.object = undefined; + }; - content = ( + return ( + <CellMeasurer + key={param.key} + parent={param.parent} + cache={this.cache} + columnIndex={0} + rowIndex={param.index} + > <MenuItemVertical {...props} + index={param.index} name={<ObjectName object={item} />} onMouseEnter={e => this.onMouseEnter(e, item)} onClick={e => this.onClick(e, item)} @@ -117,18 +112,6 @@ const MenuSearchObject = observer(class MenuSearchObject extends React.Component checkbox={checkbox} className={cn.join(' ')} /> - ); - } - - return ( - <CellMeasurer - key={param.key} - parent={param.parent} - cache={this.cache} - columnIndex={0} - rowIndex={param.index} - > - {content} </CellMeasurer> ); }; diff --git a/src/ts/component/menu/select.tsx b/src/ts/component/menu/select.tsx index d75fb06809..86b41ae68b 100644 --- a/src/ts/component/menu/select.tsx +++ b/src/ts/component/menu/select.tsx @@ -46,22 +46,6 @@ const MenuSelect = observer(class MenuSelect extends React.Component<I.Menu> { const cn = []; let content = null; - if (item.isSection) { - cn.push('sectionName'); - - if (!item.index) { - cn.push('first'); - }; - - content = <div className={cn.join(' ')} style={item.style}>{item.name}</div>; - } else - if (item.isDiv) { - content = ( - <div className="separator" style={item.style}> - <div className="inner" /> - </div> - ); - } else if (item.id == 'add') { content = ( <div @@ -437,7 +421,7 @@ const MenuSelect = observer(class MenuSelect extends React.Component<I.Menu> { if (withFilter) { height += 60; }; - if (!withFilter || noScroll) { + if (!withFilter) { height += 16; }; diff --git a/src/ts/component/menu/smile.tsx b/src/ts/component/menu/smile.tsx index f49652b1dc..635367eca3 100644 --- a/src/ts/component/menu/smile.tsx +++ b/src/ts/component/menu/smile.tsx @@ -640,7 +640,7 @@ const MenuSmile = observer(class MenuSmile extends React.Component<I.Menu, State const { close } = this.props; const { tab } = this.state; - const checkFilter = () => this.refFilter && this.refFilter.isFocused; + const checkFilter = () => this.refFilter && this.refFilter.isFocused(); e.stopPropagation(); keyboard.disableMouse(true); diff --git a/src/ts/component/menu/smile/skin.tsx b/src/ts/component/menu/smile/skin.tsx index 4035c230e1..740fccaacd 100644 --- a/src/ts/component/menu/smile/skin.tsx +++ b/src/ts/component/menu/smile/skin.tsx @@ -1,135 +1,109 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect } from 'react'; import { IconObject } from 'Component'; import { I, U, keyboard } from 'Lib'; import $ from 'jquery'; const SKINS = [ 1, 2, 3, 4, 5, 6 ]; -class MenuSmileSkin extends React.Component<I.Menu> { +const MenuSmileSkin = forwardRef<{}, I.Menu>((props, ref) => { - ref = null; - node: any = null; - n: number = 0; + const nodeRef = useRef(null); + const n = useRef(0); + const { param, close } = props; + const { data } = param; + const { smileId, onSelect } = data; - state = { - filter: '' + const rebind = () => { + unbind(); + $(window).on('keydown.menu', e => onKeyDown(e)); }; - constructor (props: I.Menu) { - super(props); - - this.onMouseEnter = this.onMouseEnter.bind(this); - }; - - render () { - const { param } = this.props; - const { data } = param; - const { smileId } = data; - - const Item = (item: any) => ( - <div - id={`skin-${item.skin}`} - className="item" - onMouseDown={e => this.onClick(e, item.skin)} - onMouseEnter={e => this.onMouseEnter(e, item.skin)} - > - <IconObject size={32} object={{ iconEmoji: U.Smile.nativeById(smileId, item.skin) }} /> - </div> - ); - - return ( - <div ref={(node: any) => this.node = node}> - {SKINS.map((skin: any, i: number) => ( - <Item key={i} skin={skin} /> - ))} - </div> - ); - }; - - componentDidMount () { - this.rebind(); - this.setActive(); - }; - - componentWillUnmount () { - const { param } = this.props; - const { data } = param; - const { rebind } = data; - - this.unbind(); - - if (rebind) { - rebind(); - }; - }; - - rebind () { - this.unbind(); - $(window).on('keydown.menu', e => this.onKeyDown(e)); - }; - - unbind () { + const unbind = () => { $(window).off('keydown.menu'); }; - onClick (e: any, id: number) { + const onClick = (e: any, id: number) => { e.preventDefault(); e.stopPropagation(); - const { param, close } = this.props; - const { data } = param; - const { onSelect } = data; - onSelect(id); close(); }; - onMouseEnter (e: any, id: number) { + const onMouseEnter = (e: any, id: number) => { if (!keyboard.isMouseDisabled) { - this.n = SKINS.indexOf(id); - this.setActive(); + n.current = SKINS.indexOf(id); + setActive(); }; }; - onKeyDown (e) { - const { param, close } = this.props; - const { data } = param; - const { onSelect } = data; - + const onKeyDown = (e) => { keyboard.shortcut('arrowleft, arrowright, arrowup, arrowdown', e, (pressed) => { e.preventDefault(); const dir = [ 'arrowleft', 'arrowup' ].includes(pressed) ? -1 : 1; - this.n += dir; - if (this.n < 0) { - this.n = SKINS.length - 1; + n.current += dir; + if (n.current < 0) { + n.current = SKINS.length - 1; } else - if (this.n >= SKINS.length) { - this.n = 0; + if (n.current >= SKINS.length) { + n.current = 0; }; - this.setActive(); + setActive(); }); keyboard.shortcut('enter, space, tab', e, () => { e.preventDefault(); e.stopPropagation(); - if (SKINS[this.n]) { - onSelect(SKINS[this.n]); + if (SKINS[n.current]) { + onSelect(SKINS[n.current]); close(); }; }); }; - setActive () { - const node = $(this.node); + const setActive = () => { + const node = $(nodeRef.current); node.find('.active').removeClass('active'); - node.find(`#skin-${SKINS[this.n]}`).addClass('active'); + node.find(`#skin-${SKINS[n.current]}`).addClass('active'); }; -}; + const Item = (item: any) => ( + <div + id={`skin-${item.skin}`} + className="item" + onMouseDown={e => onClick(e, item.skin)} + onMouseEnter={e => onMouseEnter(e, item.skin)} + > + <IconObject size={32} object={{ iconEmoji: U.Smile.nativeById(smileId, item.skin) }} /> + </div> + ); + + useEffect(() => { + rebind(); + setActive(); + + return () => { + unbind(); + + if (data.rebind) { + data.rebind(); + }; + }; + }, []); + + return ( + <div ref={nodeRef}> + {SKINS.map((skin: any, i: number) => ( + <Item key={i} skin={skin} /> + ))} + </div> + ); + +}); export default MenuSmileSkin; \ No newline at end of file diff --git a/src/ts/component/menu/syncStatus.tsx b/src/ts/component/menu/syncStatus.tsx index c9a998201f..161a7a7930 100644 --- a/src/ts/component/menu/syncStatus.tsx +++ b/src/ts/component/menu/syncStatus.tsx @@ -83,7 +83,7 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M </div> <div className="side right"> <Icon className={icon} /> - <Icon className="more" /> + <Icon className="more" onClick={e => this.onContextMenu(e, item)} /> </div> </div> ); @@ -92,9 +92,15 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M const rowRenderer = ({ index, key, style, parent }) => { const item = items[index]; + console.log(item); + let content = null; if (item.isSection) { - content = <div className={[ 'sectionName', (index == 0 ? 'first' : '') ].join(' ')} style={style}>{translate(U.Common.toCamelCase([ 'common', item.id ].join('-')))}</div>; + content = ( + <div className={[ 'sectionName', (index == 0 ? 'first' : '') ].join(' ')} style={style}> + {item.name} + </div> + ); } else { content = ( <div className="row" style={style}> @@ -175,12 +181,15 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M }; onContextMenu (e, item) { + e.stopPropagation(); + const { param } = this.props; const { classNameWrap } = param; - const node = $(this.node); const canWrite = U.Space.canMyParticipantWrite(); const canDelete = S.Block.isAllowed(item.restrictions, [ I.RestrictionObject.Delete ]); - const element = node.find(`#item-${item.id}`); + const element = $(e.currentTarget); + const node = $(this.node); + const itemElement = node.find(`#item-${item.id}`); const options: any[] = [ { id: 'open', name: translate('commonOpen') } ]; @@ -194,6 +203,8 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M element, horizontal: I.MenuDirection.Center, offsetY: 4, + onOpen: () => itemElement.addClass('hover'), + onClose: () => itemElement.removeClass('hover'), data: { options, onSelect: (e, option) => { diff --git a/src/ts/component/menu/syncStatus/info.tsx b/src/ts/component/menu/syncStatus/info.tsx index a921cf359b..a18d51d9bf 100644 --- a/src/ts/component/menu/syncStatus/info.tsx +++ b/src/ts/component/menu/syncStatus/info.tsx @@ -1,67 +1,24 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { MenuItemVertical, Title, Label } from 'Component'; import { I, S, keyboard, Renderer } from 'Lib'; -class MenuSyncStatusInfo extends React.Component<I.Menu> { +const MenuSyncStatusInfo = forwardRef<{}, I.Menu>((props, ref) => { - n = -1; - - constructor (props: I.Menu) { - super(props); - - this.getItems = this.getItems.bind(this); - this.onClick = this.onClick.bind(this); - this.onMouseEnter = this.onMouseEnter.bind(this); - }; - - render () { - const { param } = this.props; - const { data } = param; - const { title, message } = data; - const items = this.getItems(); - - return ( - <React.Fragment> - <div className="data"> - <Title text={title} /> - <Label text={message} /> - </div> - - {items.length ? ( - <div className="items"> - {items.map((item: any, i: number) => ( - <MenuItemVertical - key={i} - {...item} - onClick={e => this.onClick(e, item)} - onMouseEnter={e => this.onMouseEnter(e, item)} - /> - ))} - </div> - ) : ''} - </React.Fragment> - ); - }; - - componentDidMount () { - this.rebind(); + const { param, onKeyDown, setActive } = props; + const { data } = param; + const { title, message, buttons } = data; + + const rebind = () => { + unbind(); + $(window).on('keydown.menu', e => onKeyDown(e)); + window.setTimeout(() => setActive(), 15); }; - componentWillUnmount () { - this.unbind(); - }; - - rebind () { - this.unbind(); - $(window).on('keydown.menu', e => this.props.onKeyDown(e)); - window.setTimeout(() => this.props.setActive(), 15); - }; - - unbind () { + const unbind = () => { $(window).off('keydown.menu'); }; - onClick (e, item) { + const onClick = (e, item) => { S.Menu.closeAll(); switch (item.id) { @@ -69,6 +26,7 @@ class MenuSyncStatusInfo extends React.Component<I.Menu> { Renderer.send('updateCheck'); break; }; + case 'upgradeMembership': { S.Popup.open('membership', { data: { tier: I.TierType.Builder } }); break; @@ -76,22 +34,45 @@ class MenuSyncStatusInfo extends React.Component<I.Menu> { }; }; - onMouseEnter (e: any, item: any) { - const { setActive } = this.props; - + const onMouseEnter = (e: any, item: any) => { if (!keyboard.isMouseDisabled) { setActive(item, false); }; }; - getItems () { - const { param } = this.props; - const { data } = param; - const { buttons } = data; - - return buttons; + const getItems = () => { + return buttons || []; }; -}; + const items = getItems(); + + useEffect(() => { + rebind(); + return () => unbind(); + }, []); + + return ( + <> + <div className="data"> + <Title text={title} /> + <Label text={message} /> + </div> + + {items.length ? ( + <div className="items"> + {items.map((item: any, i: number) => ( + <MenuItemVertical + key={i} + {...item} + onClick={e => onClick(e, item)} + onMouseEnter={e => onMouseEnter(e, item)} + /> + ))} + </div> + ) : ''} + </> + ); + +}); -export default MenuSyncStatusInfo; +export default MenuSyncStatusInfo; \ No newline at end of file diff --git a/src/ts/component/menu/type/suggest.tsx b/src/ts/component/menu/type/suggest.tsx index 8bf8685ce5..c05aca3a12 100644 --- a/src/ts/component/menu/type/suggest.tsx +++ b/src/ts/component/menu/type/suggest.tsx @@ -67,20 +67,11 @@ const MenuTypeSuggest = observer(class MenuTypeSuggest extends React.Component<I <div className="name">{item.name}</div> </div> ); - } else - if (item.isDiv) { - content = ( - <div className="separator" style={param.style}> - <div className="inner" /> - </div> - ); - } else - if (item.isSection) { - content = <div className={[ 'sectionName', (param.index == 0 ? 'first' : '') ].join(' ')} style={param.style}>{item.name}</div>; } else { content = ( <MenuItemVertical {...item} + index={param.index} className={item.isHidden ? 'isHidden' : ''} style={param.style} onMouseEnter={e => this.onMouseEnter(e, item)} @@ -444,6 +435,8 @@ const MenuTypeSuggest = observer(class MenuTypeSuggest extends React.Component<I if (onClick) { onClick(S.Detail.mapper(item)); }; + + U.Object.setLastUsedDate(item.id, U.Date.now()); }; if (item.id == 'add') { diff --git a/src/ts/component/menu/widget.tsx b/src/ts/component/menu/widget.tsx index ef0a63a777..0f83e8610b 100644 --- a/src/ts/component/menu/widget.tsx +++ b/src/ts/component/menu/widget.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; -import { MenuItemVertical, Button } from 'Component'; +import { MenuItemVertical, Button, Icon } from 'Component'; import { I, C, S, U, J, keyboard, translate, Action, analytics } from 'Lib'; const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> { @@ -22,6 +22,7 @@ const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> { this.save = this.save.bind(this); this.rebind = this.rebind.bind(this); + this.onMouseLeave = this.onMouseLeave.bind(this); if (isEditing) { this.layout = layout; @@ -40,16 +41,39 @@ const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> { const Section = item => ( <div id={'section-' + item.id} className="section"> {item.name ? <div className="name">{item.name}</div> : ''} - <div className="items"> - {item.children.map((action, i) => ( - <MenuItemVertical - key={i} - {...action} - onMouseEnter={e => this.onMouseEnter(e, action)} - onClick={e => this.onClick(e, action)} - /> - ))} - </div> + + {item.options ? ( + <div className="options"> + {item.options.map((option, i) => { + const withIcon = option.icon; + const cn = [ + 'option', + item.value == option.id ? 'active' : '', + withIcon ? 'withIcon' : '', + ]; + + return ( + <div className={cn.join(' ')} key={i} onClick={e => this.onOptionClick(e, option, item)}> + {withIcon ? <Icon className={option.icon} /> : option.name} + </div> + ); + })} + </div> + ) : ''} + + {item.children.length ? ( + <div className="items"> + {item.children.map((action, i) => ( + <MenuItemVertical + key={i} + {...action} + onMouseEnter={e => this.onMouseEnter(e, action)} + onMouseLeave={this.onMouseLeave} + onClick={e => this.onClick(e, action)} + /> + ))} + </div> + ) : ''} </div> ); @@ -129,41 +153,33 @@ const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> { const { isEditing } = data; const hasLimit = ![ I.WidgetLayout.Link, I.WidgetLayout.Tree ].includes(this.layout) || U.Menu.isSystemWidget(this.target?.id); - let sourceName = translate('menuWidgetChooseSource'); - let layoutName = translate('menuWidgetWidgetType'); + const sections: any[] = [{ + id: 'layout', + name: translate('commonAppearance'), + children: [], + options: U.Menu.prepareForSelect(U.Menu.getWidgetLayoutOptions(this.target?.id, this.target?.layout)), + value: this.layout, + }]; - if (this.target) { - sourceName = U.Object.name(this.target); - }; - - if (this.layout !== null) { - layoutName = translate(`widget${this.layout}Name`); + if (hasLimit) { + sections.push({ + id: 'limit', + name: translate('menuWidgetNumberOfObjects'), + children: [], + options: U.Menu.getWidgetLimitOptions(this.layout), + value: this.limit, + }); }; - const sections: any[] = [ - { - id: 'source', name: translate('menuWidgetWidgetSource'), children: [ - { id: 'source', name: sourceName, arrow: true, object: this.target } - ] - }, - { - id: 'layout', name: translate('commonAppearance'), children: [ - { id: 'layout', name: layoutName, arrow: true }, - hasLimit ? { id: 'limit', name: translate('menuWidgetNumberOfObjects'), arrow: true, caption: this.limit } : null, - ] - }, - ]; - if (isEditing) { sections.push({ children: [ - { id: 'remove', name: translate('menuWidgetRemoveWidget'), icon: 'removeWidget' }, - { id: 'edit', name: translate('menuWidgetEditWidgets'), icon: 'source' } + { id: 'remove', name: translate('menuWidgetRemoveWidget'), icon: 'removeWidget' } ], }); }; - return U.Menu.sectionsMap(sections); + return sections; }; checkState () { @@ -204,134 +220,53 @@ const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> { onMouseEnter (e: React.MouseEvent, item): void { if (!keyboard.isMouseDisabled) { this.props.setActive(item, false); - this.onOver(e, item); }; }; - onOver (e: React.MouseEvent, item) { - if (!item.arrow) { - S.Menu.closeAll(J.Menu.widget); - return; - }; + onMouseLeave () { + $(this.node).find('.hover').removeClass('hover'); + }; - const { getId, getSize, param, close } = this.props; - const { data, className, classNameWrap } = param; + onOptionClick (e: React.MouseEvent, option: any, section: any) { + const { param, close } = this.props; + const { data } = param; const { blockId, isEditing } = data; const { widgets } = S.Block; - const menuParam: Partial<I.MenuParam> = { - menuKey: item.itemId, - element: `#${getId()} #item-${item.id}`, - offsetX: getSize().width, - vertical: I.MenuDirection.Center, - className, - classNameWrap, - isSub: true, - data: { - rebind: this.rebind, - } as any, - }; - let menuId = ''; + switch (section.id) { + case 'layout': { + this.layout = Number(option.id); + this.checkState(); + this.forceUpdate(); - switch (item.itemId) { - case 'source': { - const templateType = S.Record.getTemplateType(); - const filters: I.Filter[] = [ - { relationKey: 'layout', condition: I.FilterCondition.NotIn, value: U.Object.getSystemLayouts() }, - { relationKey: 'type', condition: I.FilterCondition.NotEqual, value: templateType?.id }, - ]; - - menuId = 'searchObject'; - menuParam.data = Object.assign(menuParam.data, { - route: analytics.route.widget, - filters, - value: this.target ? this.target.id : '', - canAdd: true, - dataChange: (context: any, items: any[]) => { - const reg = new RegExp(U.Common.regexEscape(context.filter), 'gi'); - const fixed: any[] = U.Menu.getFixedWidgets().filter(it => it.name.match(reg)); - - return !items.length ? fixed : fixed.concat([ { isDiv: true } ]).concat(items); - }, - onSelect: (target: any, isNew: boolean) => { - this.target = target; - this.checkState(); - this.forceUpdate(); - - if (isEditing && this.target) { - if (isNew) { - U.Object.openConfig(target); - }; - - C.BlockWidgetSetTargetId(widgets, blockId, this.target.id, () => { - C.BlockWidgetSetLayout(widgets, blockId, this.layout, () => close()); - }); - }; - - analytics.event('ChangeWidgetSource', { - layout: this.layout, - route: isEditing ? 'Inner' : 'AddWidget', - params: { target }, - }); - }, - }); - break; - }; + if (isEditing) { + C.BlockWidgetSetLayout(widgets, blockId, this.layout, () => close()); + }; - case 'layout': { - menuId = 'select'; - menuParam.width = 320; - menuParam.data = Object.assign(menuParam.data, { - options: U.Menu.getWidgetLayoutOptions(this.target?.id, this.target?.layout), - value: this.layout, - onSelect: (e, option) => { - this.layout = Number(option.id); - this.checkState(); - this.forceUpdate(); - - if (isEditing) { - C.BlockWidgetSetLayout(widgets, blockId, this.layout, () => close()); - }; - - analytics.event('ChangeWidgetLayout', { - layout: this.layout, - route: isEditing ? 'Inner' : 'AddWidget', - params: { target: this.target }, - }); - }, + analytics.event('ChangeWidgetLayout', { + layout: this.layout, + route: isEditing ? 'Inner' : 'AddWidget', + params: { target: this.target }, }); break; }; + case 'limit': { + this.limit = Number(option.id); + this.checkState(); + this.forceUpdate(); + + if (isEditing) { + C.BlockWidgetSetLimit(widgets, blockId, this.limit, () => close()); + }; - case 'limit': - menuId = 'select'; - menuParam.data = Object.assign(menuParam.data, { - options: U.Menu.getWidgetLimitOptions(this.layout), - value: String(this.limit || ''), - onSelect: (e, option) => { - this.limit = Number(option.id); - this.checkState(); - this.forceUpdate(); - - if (isEditing) { - C.BlockWidgetSetLimit(widgets, blockId, this.limit, () => close()); - }; - - analytics.event('ChangeWidgetLimit', { - limit: this.limit, - layout: this.layout, - route: isEditing ? 'Inner' : 'AddWidget', - params: { target: this.target }, - }); - }, + analytics.event('ChangeWidgetLimit', { + limit: this.limit, + layout: this.layout, + route: isEditing ? 'Inner' : 'AddWidget', + params: { target: this.target }, }); break; - }; - - if (menuId && !S.Menu.isOpen(menuId, item.itemId)) { - S.Menu.closeAll(J.Menu.widget, () => { - S.Menu.open(menuId, menuParam); - }); + }; }; }; @@ -348,11 +283,6 @@ const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> { case 'remove': Action.removeWidget(blockId, target); break; - - case 'edit': - setEditing(true); - analytics.event('EditWidget'); - break; }; close(); diff --git a/src/ts/component/notification/index.tsx b/src/ts/component/notification/index.tsx index 4acec36409..162d9116aa 100644 --- a/src/ts/component/notification/index.tsx +++ b/src/ts/component/notification/index.tsx @@ -1,121 +1,60 @@ -import * as React from 'react'; +import React, { FC, useRef, useState, useEffect } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { Icon, Title, Label, Button, Error } from 'Component'; import { I, C, S, U, J, translate, Action, analytics } from 'Lib'; -interface State { - error: string; -}; - -const Notification = observer(class Notification extends React.Component<I.NotificationComponent, State> { - - _isMounted = false; - node: any = null; - timeout = 0; - state = { - error: '', - }; - - constructor (props: I.NotificationComponent) { - super(props); - - this.onButton = this.onButton.bind(this); - this.onDelete = this.onDelete.bind(this); - }; - - render () { - const { error } = this.state; - const { item, style } = this.props; - const { space } = S.Common; - const { id, type, payload, title, text } = item; - const { errorCode, spaceId } = payload; - const spaceview = U.Space.getSpaceviewBySpaceId(spaceId); - const participant = U.Space.getMyParticipant(spaceId); - const spaceCheck = spaceview && (spaceview.isAccountRemoving || spaceview.isAccountDeleted); - const participantCheck = participant && (participant.isRemoving || participant.isJoining); - - let buttons = []; - - switch (type) { - case I.NotificationType.Gallery: - case I.NotificationType.Import: { - if (!errorCode && (spaceId != space)) { - buttons = buttons.concat([ - { id: 'spaceSwitch', text: translate('notificationButtonSpaceSwitch') } - ]); - }; - break; - }; - - case I.NotificationType.Join: { +const Notification: FC<I.NotificationComponent> = observer((props) => { + + const nodeRef = useRef(null); + const timeout = useRef(0); + const [ error, setError ] = useState(''); + const { item, style, resize } = props; + const { space } = S.Common; + const { id, type, payload, title, text } = item; + const { errorCode, spaceId } = payload; + const spaceview = U.Space.getSpaceviewBySpaceId(spaceId); + const participant = U.Space.getMyParticipant(spaceId); + const spaceCheck = spaceview && (spaceview.isAccountRemoving || spaceview.isAccountDeleted); + const participantCheck = participant && (participant.isRemoving || participant.isJoining); + + let buttons = []; + + switch (type) { + case I.NotificationType.Gallery: + case I.NotificationType.Import: { + if (!errorCode && (spaceId != space)) { buttons = buttons.concat([ - { id: 'request', text: translate('notificationButtonRequest') }, - { id: 'spaceSwitch', text: translate('notificationButtonSpaceSwitch'), color: 'blank' }, + { id: 'spaceSwitch', text: translate('notificationButtonSpaceSwitch') } ]); - break; }; - - case I.NotificationType.Leave: { - buttons = buttons.concat([ - { id: 'approve', text: translate('commonApprove') } - ]); - break; - }; - + break; }; - // Check that space is not removed - if (spaceCheck || participantCheck) { - buttons = buttons.filter(it => ![ 'spaceSwitch' ].includes(it.id)); + case I.NotificationType.Join: { + buttons = buttons.concat([ + { id: 'request', text: translate('notificationButtonRequest') }, + { id: 'spaceSwitch', text: translate('notificationButtonSpaceSwitch'), color: 'blank' }, + ]); + break; }; - return ( - <div - id={`notification-${id}`} - ref={node => this.node = node} - className="notification" - style={style} - > - <Icon className="delete" onClick={this.onDelete} /> - <div className="content"> - {title ? <Title text={title} /> : ''} - {text ? <Label text={text} /> : ''} - <Error text={error} /> - - {buttons.length ? ( - <div className="buttons"> - {buttons.map((item: any, i: number) => ( - <Button key={i} className="c28" {...item} onClick={e => this.onButton(e, item.id)} /> - ))} - </div> - ) : ''} - </div> - </div> - ); - }; - - componentDidMount (): void { - const { resize } = this.props; - const node = $(this.node); - - node.addClass('from'); - this.timeout = window.setTimeout(() => { - node.removeClass('from'); - window.setTimeout(() => resize(), J.Constant.delay.notification); - }, 40); + case I.NotificationType.Leave: { + buttons = buttons.concat([ + { id: 'approve', text: translate('commonApprove') } + ]); + break; + }; }; - componentWillUnmount (): void { - window.clearTimeout(this.timeout); + // Check that space is not removed + if (spaceCheck || participantCheck) { + buttons = buttons.filter(it => ![ 'spaceSwitch' ].includes(it.id)); }; - onButton (e: any, action: string) { + const onButton = (e: any, action: string) => { e.stopPropagation(); - const { item } = this.props; - const { payload } = item; - switch (action) { case 'spaceSwitch': { U.Router.switchSpace(payload.spaceId, '', true, {}); @@ -143,7 +82,7 @@ const Notification = observer(class Notification extends React.Component<I.Notif case 'approve': { Action.leaveApprove(payload.spaceId, [ payload.identity ], payload.identityName, analytics.route.notification, (message: any) => { if (message.error.code) { - this.setState({ error: message.error.description }); + setError(message.error.description); }; }); break; @@ -151,24 +90,62 @@ const Notification = observer(class Notification extends React.Component<I.Notif }; - this.onDelete(e); + onDelete(e); }; - onDelete (e: any): void { + const onDelete = (e: any): void => { e.stopPropagation(); - const { item, resize } = this.props; - const node = $(this.node); + $(nodeRef.current).addClass('to'); - node.addClass('to'); - this.timeout = window.setTimeout(() => { + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => { C.NotificationReply([ item.id ], I.NotificationAction.Close); S.Notification.delete(item.id); resize(); }, J.Constant.delay.notification); }; - + + useEffect(() => { + const node = $(nodeRef.current); + + node.addClass('from'); + + timeout.current = window.setTimeout(() => { + node.removeClass('from'); + window.setTimeout(() => resize(), J.Constant.delay.notification); + }, 40); + + return () => { + window.clearTimeout(timeout.current); + }; + }, []); + + return ( + <div + id={`notification-${id}`} + ref={nodeRef} + className="notification" + style={style} + > + <Icon className="delete" onClick={onDelete} /> + <div className="content"> + {title ? <Title text={title} /> : ''} + {text ? <Label text={text} /> : ''} + <Error text={error} /> + + {buttons.length ? ( + <div className="buttons"> + {buttons.map((item: any, i: number) => ( + <Button key={i} className="c28" {...item} onClick={e => onButton(e, item.id)} /> + ))} + </div> + ) : ''} + </div> + </div> + ); + }); -export default Notification; +export default Notification; \ No newline at end of file diff --git a/src/ts/component/page/auth/deleted.tsx b/src/ts/component/page/auth/deleted.tsx index e6e6f794ea..3bdd2d3bb0 100644 --- a/src/ts/component/page/auth/deleted.tsx +++ b/src/ts/component/page/auth/deleted.tsx @@ -1,113 +1,18 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { observer } from 'mobx-react'; import { PieChart } from 'react-minimal-pie-chart'; -import { Frame, Title, Label, Error, Button } from 'Component'; +import { Frame, Title, Label, Button } from 'Component'; import { I, C, S, U, Action, Survey, analytics, translate } from 'Lib'; import CanvasWorkerBridge from './animation/canvasWorkerBridge'; import { OnboardStage } from './animation/constants'; -interface State { - error: string; -}; - const DAYS = 30; -const PageAuthDeleted = observer(class PageAuthDeleted extends React.Component<I.PageComponent, State> { - - state = { - error: '' - }; - - constructor (props: I.PageComponent) { - super(props); - - this.onRemove = this.onRemove.bind(this); - this.onExport = this.onExport.bind(this); - this.onCancel = this.onCancel.bind(this); - this.onLogout = this.onLogout.bind(this); - }; - - render () { - const { account } = S.Auth; - if (!account) { - return null; - }; - - const { error } = this.state; - const duration = Math.max(0, account.status.date - U.Date.now()); - const days = Math.max(1, Math.floor(duration / 86400)); - const dt = `${days} ${U.Common.plural(days, translate('pluralDay'))}`; - - // Deletion Status - let status: I.AccountStatusType = account.status.type; - if ((status == I.AccountStatusType.PendingDeletion) && !duration) { - status = I.AccountStatusType.Deleted; - }; - - // UI Elements - let showPie = false; - let title = ''; - let description = ''; - let cancelButton = null; +const PageAuthDeleted = observer(forwardRef<{}, I.PageComponent>(() => { - switch (status) { - case I.AccountStatusType.PendingDeletion: { - showPie = true; - title = U.Common.sprintf(translate('pageAuthDeletedAccountDeletionTitle'), dt); - description = translate('authDeleteDescription'); - cancelButton = <Button type="input" text={translate('authDeleteCancelButton')} onClick={this.onCancel} />; - break; - }; - - case I.AccountStatusType.StartedDeletion: - case I.AccountStatusType.Deleted: { - title = translate('authDeleteTitleDeleted'); - description = translate('authDeleteDescriptionDeleted'); - break; - }; - }; - - return ( - <div> - <CanvasWorkerBridge state={OnboardStage.Void} /> - - <Frame> - {showPie ? ( - <div className="animation pie"> - <div className="inner"> - <PieChart - totalValue={DAYS} - startAngle={270} - lengthAngle={-360} - data={[ { title: '', value: days, color: '#d4d4d4' } ]} - /> - </div> - </div> - ) : null} - - <Title className="animation" text={title} /> - <Label className="animation" text={description} /> - <Error className="animation" text={error} /> - - <div className="animation buttons"> - {cancelButton} - <Button color="blank" text={translate('authDeleteExportButton')} onClick={this.onExport} /> - <div className="remove" onClick={this.onRemove}>{translate('authDeleteRemoveButton')}</div> - </div> - </Frame> - - <div className="animation small bottom" onClick={this.onLogout}> - {translate('popupSettingsLogout')} - </div> - </div> - ); - }; + const { account } = S.Auth; - componentDidMount() { - window.setTimeout(() => Survey.check(I.SurveyType.Delete), S.Popup.getTimeout()); - }; - - onRemove () { + const onRemove = () => { S.Popup.open('confirm', { data: { title: translate('authDeleteRemovePopupTitle'), @@ -121,7 +26,7 @@ const PageAuthDeleted = observer(class PageAuthDeleted extends React.Component<I }); }; - onExport () { + const onExport = () => { Action.export('', [], I.ExportType.Markdown, { zip: true, nested: true, @@ -132,7 +37,7 @@ const PageAuthDeleted = observer(class PageAuthDeleted extends React.Component<I }); }; - onCancel () { + const onCancel = () => { C.AccountRevertDeletion((message) => { S.Auth.accountSetStatus(message.status); U.Space.openDashboard('route'); @@ -140,7 +45,7 @@ const PageAuthDeleted = observer(class PageAuthDeleted extends React.Component<I }); }; - onLogout () { + const onLogout = () => { U.Router.go('/', { replace: true, animate: true, @@ -149,7 +54,82 @@ const PageAuthDeleted = observer(class PageAuthDeleted extends React.Component<I }, }); }; - -}); -export default PageAuthDeleted; + // UI Elements + let showPie = false; + let title = ''; + let description = ''; + let cancelButton = null; + let days = 0; + + if (account) { + const duration = Math.max(0, account.status.date - U.Date.now()); + + days = Math.max(1, Math.floor(duration / 86400)); + const dt = `${days} ${U.Common.plural(days, translate('pluralDay'))}`; + + // Deletion Status + let status: I.AccountStatusType = account.status.type; + if ((status == I.AccountStatusType.PendingDeletion) && !duration) { + status = I.AccountStatusType.Deleted; + }; + + switch (status) { + case I.AccountStatusType.PendingDeletion: { + showPie = true; + title = U.Common.sprintf(translate('pageAuthDeletedAccountDeletionTitle'), dt); + description = translate('authDeleteDescription'); + cancelButton = <Button type="input" text={translate('authDeleteCancelButton')} onClick={onCancel} />; + break; + }; + + case I.AccountStatusType.StartedDeletion: + case I.AccountStatusType.Deleted: { + title = translate('authDeleteTitleDeleted'); + description = translate('authDeleteDescriptionDeleted'); + break; + }; + }; + }; + + useEffect(() => { + window.setTimeout(() => Survey.check(I.SurveyType.Delete), S.Popup.getTimeout()); + }, []); + + return account ? ( + <> + <CanvasWorkerBridge state={OnboardStage.Void} /> + + <Frame> + {showPie ? ( + <div className="animation pie"> + <div className="inner"> + <PieChart + totalValue={DAYS} + startAngle={270} + lengthAngle={-360} + data={[ { title: '', value: days, color: '#d4d4d4' } ]} + /> + </div> + </div> + ) : null} + + <Title className="animation" text={title} /> + <Label className="animation" text={description} /> + + <div className="animation buttons"> + {cancelButton} + <Button color="blank" text={translate('authDeleteExportButton')} onClick={onExport} /> + <div className="remove" onClick={onRemove}>{translate('authDeleteRemoveButton')}</div> + </div> + </Frame> + + <div className="animation small bottom" onClick={onLogout}> + {translate('popupSettingsLogout')} + </div> + </> + ) : null; + +})); + +export default PageAuthDeleted; \ No newline at end of file diff --git a/src/ts/component/page/auth/login.tsx b/src/ts/component/page/auth/login.tsx index adf6f7aa39..e2c6392b62 100644 --- a/src/ts/component/page/auth/login.tsx +++ b/src/ts/component/page/auth/login.tsx @@ -1,129 +1,70 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useRef, useEffect } from 'react'; import { observer } from 'mobx-react'; import { Frame, Error, Button, Header, Icon, Phrase } from 'Component'; -import { I, C, S, U, J, translate, keyboard, Animation, Renderer, analytics, Preview, Storage } from 'Lib'; +import { I, C, S, U, J, translate, keyboard, Animation, Renderer, analytics, Storage } from 'Lib'; -interface State { - error: string; -}; +const PageAuthLogin = observer(forwardRef<{}, I.PageComponent>((props, ref: any) => { -const PageAuthLogin = observer(class PageAuthLogin extends React.Component<I.PageComponent, State> { + const nodeRef = useRef(null); + const phraseRef = useRef(null); + const submitRef = useRef(null); + const [ error, setError ] = useState(''); + const isSelecting = useRef(false); + const { accounts } = S.Auth; + const length = accounts.length; - node = null; - refPhrase = null; - refSubmit = null; - isSelecting = false; - - state = { - error: '', - }; - - constructor (props: I.PageComponent) { - super(props); - - this.onSubmit = this.onSubmit.bind(this); - this.onCancel = this.onCancel.bind(this); - this.onKeyDownPhrase = this.onKeyDownPhrase.bind(this); - this.onForgot = this.onForgot.bind(this); - }; - - render () { - const { error } = this.state; - const { accounts } = S.Auth; - const length = accounts.length; - - return ( - <div ref={ref => this.node = ref}> - <Header {...this.props} component="authIndex" /> - <Icon className="arrow back" onClick={this.onCancel} /> - - <Frame> - <form className="form" onSubmit={this.onSubmit}> - <Error text={error} className="animation" /> - - <div className="animation"> - <Phrase - ref={ref => this.refPhrase = ref} - onKeyDown={this.onKeyDownPhrase} - isHidden={true} - placeholder={translate('phrasePlaceholder')} - /> - </div> - <div className="buttons"> - <div className="animation"> - <Button ref={ref => this.refSubmit = ref} text={translate('authLoginSubmit')} onClick={this.onSubmit} /> - </div> - - <div className="animation"> - <div className="small" onClick={this.onForgot}>{translate('authLoginLostPhrase')}</div> - </div> - </div> - </form> - </Frame> - </div> - ); - }; - - componentDidMount () { - Animation.to(); - this.focus(); - }; - - componentDidUpdate () { - this.focus(); - this.select(); + const focus = () => { + phraseRef.current?.focus(); }; - focus () { - if (this.refPhrase) { - this.refPhrase.focus(); - }; + const getPhrase = () => { + return String(phraseRef.current?.getValue() || ''); }; - onSubmit (e: any) { + const onSubmit = (e: any) => { e.preventDefault(); - const phrase = this.refPhrase.getValue(); + const phrase = getPhrase(); const length = phrase.split(' ').length; if (length < J.Constant.count.phrase.word) { - this.setError({ code: 1, description: translate('pageAuthLoginShortPhrase')}); + setErrorHandler(1, translate('pageAuthLoginShortPhrase')); return; }; - this.refSubmit?.setLoading(true); + submitRef.current?.setLoading(true); C.WalletRecover(S.Common.dataPath, phrase, (message: any) => { - if (this.setError({ ...message.error, description: translate('pageAuthLoginInvalidPhrase')})) { + if (setErrorHandler(message.error.code, translate('pageAuthLoginInvalidPhrase'))) { return; }; S.Auth.accountListClear(); U.Data.createSession(phrase, '', () => { C.AccountRecover(message => { - this.setError(message.error); + setErrorHandler(message.error.code, message.error.description); }); }); }); }; - select () { + const select = () => { const { accounts, networkConfig } = S.Auth; - if (this.isSelecting || !accounts.length) { + if (isSelecting.current || !accounts.length) { return; }; - this.isSelecting = true; + isSelecting.current = true; const { mode, path } = networkConfig; const account = accounts[0]; S.Auth.accountSet(account); - Renderer.send('keytarSet', account.id, this.refPhrase.getValue()); + Renderer.send('keytarSet', account.id, getPhrase()); C.AccountSelect(account.id, S.Common.dataPath, mode, path, (message: any) => { - if (this.setError(message.error) || !message.account) { - this.isSelecting = false; + if (setErrorHandler(message.error.code, message.error.description) || !message.account) { + isSelecting.current = false; return; }; @@ -131,22 +72,14 @@ const PageAuthLogin = observer(class PageAuthLogin extends React.Component<I.Pag S.Common.configSet(message.account.config, false); const spaceId = Storage.get('spaceId'); - const shareTooltip = Storage.get('shareTooltip'); - const routeParam = { - replace: true, - onRouteChange: () => { - if (!shareTooltip) { - Preview.shareTooltipShow(); - }; - }, - }; + const routeParam = { replace: true }; if (spaceId) { U.Router.switchSpace(spaceId, '', false, routeParam); } else { Animation.from(() => { U.Data.onAuthWithoutSpace(routeParam); - this.isSelecting = false; + isSelecting.current = false; }); }; @@ -156,42 +89,44 @@ const PageAuthLogin = observer(class PageAuthLogin extends React.Component<I.Pag }); }; - setError (error: { description: string, code: number}) { - if (!error.code) { + const setErrorHandler = (code: number, text: string) => { + if (!code) { return false; }; - if (error.code == J.Error.Code.FAILED_TO_FIND_ACCOUNT_INFO) { + if (code == J.Error.Code.FAILED_TO_FIND_ACCOUNT_INFO) { U.Router.go('/auth/setup/select', {}); return; }; - this.setState({ error: error.description }); - this.refPhrase?.setError(true); - this.refSubmit?.setLoading(false); + if (code == J.Error.Code.ACCOUNT_STORE_NOT_MIGRATED) { + U.Router.go('/auth/migrate', {}); + return; + }; + + setError(text); + phraseRef.current?.setError(true); + submitRef.current?.setLoading(false); S.Auth.accountListClear(); - - return U.Common.checkErrorCommon(error.code); + return U.Common.checkErrorCommon(code); }; - onKeyDownPhrase (e: React.KeyboardEvent) { - const { error } = this.state; - + const onKeyDownPhrase = (e: React.KeyboardEvent) => { if (error) { - this.refPhrase?.setError(false); - this.setState({ error: '' }); + phraseRef.current?.setError(false); + setError(''); }; - keyboard.shortcut('enter', e, () => this.onSubmit(e)); + keyboard.shortcut('enter', e, () => onSubmit(e)); }; - onCancel () { + const onCancel = () => { S.Auth.logout(true, false); Animation.from(() => U.Router.go('/', { replace: true })); }; - onForgot () { + const onForgot = () => { const platform = U.Common.getPlatform(); S.Popup.open('confirm', { @@ -206,6 +141,47 @@ const PageAuthLogin = observer(class PageAuthLogin extends React.Component<I.Pag }); }; -}); + useEffect(() => { + Animation.to(); + focus(); + }, []); + + useEffect(() => { + focus(); + select(); + }); + + return ( + <div ref={nodeRef}> + <Header {...props} component="authIndex" /> + <Icon className="arrow back" onClick={onCancel} /> + + <Frame> + <form className="form" onSubmit={onSubmit}> + <Error text={error} className="animation" /> + + <div className="animation"> + <Phrase + ref={phraseRef} + onKeyDown={onKeyDownPhrase} + isHidden={true} + placeholder={translate('phrasePlaceholder')} + /> + </div> + <div className="buttons"> + <div className="animation"> + <Button ref={submitRef} text={translate('authLoginSubmit')} onClick={onSubmit} /> + </div> + + <div className="animation"> + <div className="small" onClick={onForgot}>{translate('authLoginLostPhrase')}</div> + </div> + </div> + </form> + </Frame> + </div> + ); + +})); -export default PageAuthLogin; +export default PageAuthLogin; \ No newline at end of file diff --git a/src/ts/component/page/auth/migrate.tsx b/src/ts/component/page/auth/migrate.tsx new file mode 100644 index 0000000000..03f0463698 --- /dev/null +++ b/src/ts/component/page/auth/migrate.tsx @@ -0,0 +1,51 @@ +import React, { forwardRef, useEffect, useState } from 'react'; +import { observer } from 'mobx-react'; +import { Frame, Error, ProgressBar, Button } from 'Component'; +import { I, C, S, U, Storage, translate } from 'Lib'; + +const PageAuthMigrate = observer(forwardRef<{}, I.PageComponent>((props, ref) => { + + const { dataPath } = S.Common; + const accountId = Storage.get('accountId'); + const [ error, setError ] = useState(''); + const types = [ I.ProgressType.Migrate ]; + const list = S.Progress.getList(it => types.includes(it.type)); + const progress = list.length ? list[0] : null; + const segments = []; + + if (progress) { + segments.push({ name: '', caption: '', percent: progress.current / progress.total, isActive: true }); + }; + + const onCancel = () => { + C.AccountMigrateCancel(accountId, (message: any) => { + if (message.error.code) { + setError(message.error.description); + }; + }); + }; + + useEffect(() => { + S.Auth.clearAll(); + + C.AccountMigrate(accountId, dataPath, (message: any) => { + if (message.error.code) { + setError(message.error.description); + return; + }; + + U.Router.go('/auth/setup/init', { replace: true }); + }); + }, []); + + return ( + <Frame> + <Error text={error} /> + <ProgressBar segments={segments} /> + <Button text={translate('commonCancel')} onClick={onCancel} /> + </Frame> + ); + +})); + +export default PageAuthMigrate; \ No newline at end of file diff --git a/src/ts/component/page/auth/onboard.tsx b/src/ts/component/page/auth/onboard.tsx index 3e4375be26..4f1d59aabb 100644 --- a/src/ts/component/page/auth/onboard.tsx +++ b/src/ts/component/page/auth/onboard.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect } from 'react'; import { observer } from 'mobx-react'; import { Frame, Title, Label, Button, DotIndicator, Phrase, Icon, Input, Error } from 'Component'; -import { I, C, S, U, J, translate, Animation, analytics, keyboard, Renderer, Storage, Onboarding } from 'Lib'; +import { I, C, S, U, J, translate, Animation, analytics, keyboard, Renderer, Onboarding } from 'Lib'; import CanvasWorkerBridge from './animation/canvasWorkerBridge'; enum Stage { @@ -10,235 +10,80 @@ enum Stage { Soul = 2, }; -type State = { - stage: Stage; - phraseVisible: boolean; - error: string; -}; - -const PageAuthOnboard = observer(class PageAuthOnboard extends React.Component<I.PageComponent, State> { - - node: HTMLDivElement = null; - refFrame: any = null; - refPhrase: any = null; - refNext: any = null; - refName: any = null; - - state: State = { - stage: Stage.Vault, - phraseVisible: false, - error: '', - }; - - constructor (props: I.PageComponent) { - super(props); - - this.onForward = this.onForward.bind(this); - this.onBack = this.onBack.bind(this); - this.onCopy = this.onCopy.bind(this); - this.onShowPhrase = this.onShowPhrase.bind(this); - this.setError = this.setError.bind(this); - }; - - render () { - const { stage, phraseVisible, error } = this.state; - const cnb = []; - - if (!this.canMoveForward()) { - cnb.push('disabled'); - }; - - let content = null; - let buttons = null; - let more = null; - - switch (stage) { - case Stage.Vault: { - buttons = ( - <div className="animation"> - <Button ref={ref => this.refNext = ref} className={cnb.join(' ')} text={translate('authOnboardVaultButton')} onClick={this.onForward} /> - </div> - ); - break; - }; - - case Stage.Phrase: { - const text = phraseVisible ? translate('commonNext') : translate('authOnboardPhraseSubmit'); - - content = ( - <Phrase - ref={ref => this.refPhrase = ref} - className="animation" - readonly={true} - isHidden={!phraseVisible} - onCopy={this.onCopy} - onClick={this.onCopy} - /> - ); - - buttons = ( - <React.Fragment> - <div className="animation"> - <Button ref={ref => this.refNext = ref} className={cnb.join(' ')} text={text} onClick={this.onShowPhrase} /> - </div> - - {!phraseVisible ? ( - <div className="animation"> - <Button color="blank" text={translate('commonSkip')} onClick={this.onForward} /> - </div> - ) : ''} - </React.Fragment> - ); - - if (!phraseVisible) { - more = <div className="moreInfo animation">{translate('authOnboardMoreInfo')}</div>; - }; - break; - }; +const PageAuthOnboard = observer(forwardRef<{}, I.PageComponent>(() => { - case Stage.Soul: { - content = ( - <div className="inputWrapper animation"> - <Input - ref={ref => this.refName = ref} - focusOnMount={true} - placeholder={translate('defaultNamePage')} - maxLength={255} - /> - </div> - ); - - buttons = ( - <div className="animation"> - <Button ref={ref => this.refNext = ref} className={cnb.join(' ')} text={translate('authOnboardSoulButton')} onClick={this.onForward} /> - </div> - ); - break; - }; - }; - - return ( - <div - ref={ref => this.node = ref} - className={`stage${Stage[stage]}`} - > - {this.canMoveBack() ? <Icon className="arrow back" onClick={this.onBack} /> : ''} - - <Frame ref={ref => this.refFrame = ref}> - <DotIndicator className="animation" index={stage} count={3} /> - <Title className="animation" text={translate(`authOnboard${Stage[stage]}Title`)} /> - <Label id="label" className="animation" text={translate(`authOnboard${Stage[stage]}Label`)} /> - - {content} - - <Error className="animation" text={error} /> - <div className="buttons">{buttons}</div> - {more} - </Frame> - - <CanvasWorkerBridge state={0} /> - </div> - ); - }; + const { account } = S.Auth; + const nodeRef = useRef(null); + const frameRef = useRef(null); + const phraseRef = useRef(null); + const nextRef = useRef(null); + const nameRef = useRef(null); + const [ stage, setStage ] = useState(Stage.Vault); + const [ phraseVisible, setPhraseVisible ] = useState(false); + const [ error, setError ] = useState(''); + const cnb = []; - componentDidMount (): void { - const { stage } = this.state; - - Animation.to(); - this.rebind(); - - analytics.event('ScreenOnboarding', { step: Stage[stage] }); - }; - - componentDidUpdate (_, prevState): void { - const { stage } = this.state; - const { account } = S.Auth; - - if (prevState.stage != stage) { - Animation.to(); - analytics.event('ScreenOnboarding', { step: Stage[stage] }); - }; - - if (account && (stage == Stage.Phrase)) { - Renderer.send('keytarGet', account.id).then(value => this.refPhrase?.setValue(value)); - }; - - this.refFrame?.resize(); - this.rebind(); - }; - - componentWillUnmount (): void { - this.unbind(); - }; - - unbind () { + const unbind = () => { $(window).off('keydown.onboarding'); }; - rebind () { - const node = $(this.node); + const rebind = () => { + const node = $(nodeRef.current); const tooltipPhrase = node.find('#tooltipPhrase'); - this.unbind(); - $(window).on('keydown.onboarding', e => this.onKeyDown(e)); - - tooltipPhrase.off('click').on('click', () => this.onPhraseTooltip()); + unbind(); + $(window).on('keydown.onboarding', e => onKeyDown(e)); + tooltipPhrase.off('click').on('click', () => onPhraseTooltip()); }; - onKeyDown (e) { - keyboard.shortcut('enter', e, this.onForward); + const onKeyDown = e => { + keyboard.shortcut('enter', e, () => { + e.preventDefault(); + onForward(); + }); }; - /** Guard to prevent illegal state change */ - canMoveForward (): boolean { - return !!Stage[this.state.stage] && !this.refNext?.isLoading(); + // Guard to prevent illegal state change + const canMoveForward = (): boolean => { + return !!Stage[stage] && !nextRef.current?.isLoading(); }; - /** Guard to prevent illegal state change */ - canMoveBack (): boolean { - return this.state.stage <= Stage.Soul; + // Guard to prevent illegal state change + const canMoveBack = (): boolean => { + return stage <= Stage.Soul; }; - /** Moves the Onboarding Flow one stage forward if possible */ - onForward () { - if (!this.canMoveForward()) { + // Moves the Onboarding Flow one stage forward if possible + const onForward = () => { + if (!canMoveForward()) { return; }; - const { stage } = this.state; - const { account } = S.Auth; - if (stage == Stage.Vault) { const cb = () => { Animation.from(() => { - this.setState({ stage: stage + 1 }); - this.refNext?.setLoading(false); + nextRef.current?.setLoading(false); + setStage(stage + 1); }); }; if (account) { cb(); } else { - this.refNext?.setLoading(true); - U.Data.accountCreate(this.setError, cb); + nextRef.current?.setLoading(true); + U.Data.accountCreate(setErrorHandler, cb); }; }; - if (!account) { - return; - }; - if (stage == Stage.Phrase) { - Animation.from(() => { - this.setState({ stage: stage + 1 }); - }); + Animation.from(() => setStage(stage + 1)); }; if (stage == Stage.Soul) { - const name = this.refName.getValue(); + const name = nameRef.current?.getValue(); const cb = () => { Animation.from(() => { - this.refNext?.setLoading(false); + nextRef.current?.setLoading(false); const routeParam = { replace: true, @@ -266,7 +111,7 @@ const PageAuthOnboard = observer(class PageAuthOnboard extends React.Component<I C.WorkspaceSetInfo(S.Common.space, details, () => { if (name) { - this.refNext?.setLoading(true); + nextRef.current?.setLoading(true); U.Object.setName(S.Block.profile, name, cb); } else { cb(); @@ -276,49 +121,164 @@ const PageAuthOnboard = observer(class PageAuthOnboard extends React.Component<I }; }; - /** Moves the Onboarding Flow one stage backward, or exits it entirely */ - onBack () { - if (!this.canMoveBack()) { + // Moves the Onboarding Flow one stage backward, or exits it entirely + const onBack = () => { + if (!canMoveBack()) { return; }; - const { stage } = this.state; - if (stage == Stage.Vault) { Animation.from(() => U.Router.go('/', { replace: true })); } else { - this.setState({ stage: stage - 1 }); + setStage(stage - 1); }; }; - onShowPhrase () { - const { stage, phraseVisible } = this.state; - + const onShowPhrase = () => { if (phraseVisible) { - this.onForward(); + onForward(); } else { - this.refPhrase.onToggle(); - this.setState({ phraseVisible: true }); + phraseRef.current?.onToggle(); + setPhraseVisible(true); analytics.event('ClickOnboarding', { type: 'ShowAndCopy', step: Stage[stage] }); }; }; - onCopy () { - U.Common.copyToast(translate('commonPhrase'), this.refPhrase.getValue()); + const onCopy = () => { + U.Common.copyToast(translate('commonPhrase'), phraseRef.current?.getValue()); analytics.event('KeychainCopy', { type: 'Onboarding' }); }; - onPhraseTooltip () { + const onPhraseTooltip = () => { S.Popup.open('phrase', {}); - analytics.event('ClickOnboarding', { type: 'MoreInfo', step: Stage[this.state.stage] }); + analytics.event('ClickOnboarding', { type: 'MoreInfo', step: Stage[stage] }); + }; + + const setErrorHandler = (error: string) => { + nextRef.current?.setLoading(false); + setError(error); }; - setError (error: string) { - this.refNext?.setLoading(false); - this.setState({ error }); + if (!canMoveForward()) { + cnb.push('disabled'); }; -}); + let content = null; + let buttons = null; + let more = null; + + switch (stage) { + case Stage.Vault: { + buttons = ( + <div className="animation"> + <Button ref={nextRef} className={cnb.join(' ')} text={translate('authOnboardVaultButton')} onClick={onForward} /> + </div> + ); + break; + }; + + case Stage.Phrase: { + const text = phraseVisible ? translate('commonNext') : translate('authOnboardPhraseSubmit'); + + content = ( + <Phrase + ref={phraseRef} + className="animation" + readonly={true} + isHidden={!phraseVisible} + onCopy={onCopy} + onClick={onCopy} + /> + ); + + buttons = ( + <React.Fragment> + <div className="animation"> + <Button ref={nextRef} className={cnb.join(' ')} text={text} onClick={onShowPhrase} /> + </div> + + {!phraseVisible ? ( + <div className="animation"> + <Button color="blank" text={translate('commonSkip')} onClick={onForward} /> + </div> + ) : ''} + </React.Fragment> + ); + + if (!phraseVisible) { + more = <div className="moreInfo animation">{translate('authOnboardMoreInfo')}</div>; + }; + break; + }; + + case Stage.Soul: { + content = ( + <div className="inputWrapper animation"> + <Input + ref={nameRef} + focusOnMount={true} + placeholder={translate('defaultNamePage')} + maxLength={255} + /> + </div> + ); + + buttons = ( + <div className="animation"> + <Button ref={nextRef} className={cnb.join(' ')} text={translate('authOnboardSoulButton')} onClick={onForward} /> + </div> + ); + break; + }; + }; + + const init = () => { + Animation.to(); + frameRef.current.resize(); + rebind(); + }; + + useEffect(() => { + init(); + return () => unbind(); + }, []); + + useEffect(() => { + init(); + + if (account && (stage == Stage.Phrase)) { + Renderer.send('keytarGet', account.id).then(value => phraseRef.current?.setValue(value)); + }; + }, [ stage ]); + + useEffect(() => { + analytics.event('ScreenOnboarding', { step: Stage[stage] }); + }); + + return ( + <div + ref={nodeRef} + className={`stage${Stage[stage]}`} + > + {canMoveBack() ? <Icon className="arrow back" onClick={onBack} /> : ''} + + <Frame ref={frameRef}> + <DotIndicator className="animation" index={stage} count={3} /> + <Title className="animation" text={translate(`authOnboard${Stage[stage]}Title`)} /> + <Label id="label" className="animation" text={translate(`authOnboard${Stage[stage]}Label`)} /> + + {content} + + <Error className="animation" text={error} /> + <div className="buttons">{buttons}</div> + {more} + </Frame> + + <CanvasWorkerBridge state={0} /> + </div> + ); + +})); -export default PageAuthOnboard; +export default PageAuthOnboard; \ No newline at end of file diff --git a/src/ts/component/page/auth/pinCheck.tsx b/src/ts/component/page/auth/pinCheck.tsx index 89b05e3ad9..9b314a88fe 100644 --- a/src/ts/component/page/auth/pinCheck.tsx +++ b/src/ts/component/page/auth/pinCheck.tsx @@ -1,68 +1,28 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect } from 'react'; import { Frame, Title, Error, Pin } from 'Component'; import { I, S, U, Storage, translate, keyboard } from 'Lib'; import { observer } from 'mobx-react'; -interface State { - error: string; -}; +const PageAuthPinCheck = observer(forwardRef<{}, I.PageComponent>(() => { -const PageAuthPinCheck = observer(class PageAuthPinCheck extends React.Component<I.PageComponent, State> { - - ref = null; - state = { - error: '' - }; - - constructor (props: I.PageComponent) { - super(props); - - this.onSuccess = this.onSuccess.bind(this); - this.onError = this.onError.bind(this); - }; - - render () { - const { error } = this.state; - - return ( - <div> - <Frame> - <Title text={translate('authPinCheckTitle')} /> - <Pin - ref={ref => this.ref = ref} - expectedPin={Storage.getPin()} - onSuccess={this.onSuccess} - onError={this.onError} - /> - <Error text={error} /> - </Frame> - </div> - ); - }; - - componentDidMount () { - this.rebind(); - }; - - componentWillUnmount () { - this.unbind(); - }; + const pinRef = useRef(null); + const [ error, setError ] = useState(''); - unbind () { + const unbind = () => { $(window).off('focus.pin'); }; - rebind () { - this.unbind(); - $(window).on('focus.pin', () => this.ref.focus()); + const rebind = () => { + unbind(); + $(window).on('focus.pin', () => pinRef.current.focus()); }; - onError () { - this.ref.reset(); - this.setState({ error: translate('authPinCheckError') }); + const onError = () => { + pinRef.current.reset(); + setError(translate('authPinCheckError')); }; - onSuccess () { + const onSuccess = () => { const { account } = S.Auth; const { redirect } = S.Common; const routeParam = { replace: true, animate: true }; @@ -75,7 +35,25 @@ const PageAuthPinCheck = observer(class PageAuthPinCheck extends React.Component U.Router.go('/', routeParam); }; }; - -}); -export default PageAuthPinCheck; + useEffect(() => { + rebind(); + return () => unbind(); + }, []); + + return ( + <Frame> + <Title text={translate('authPinCheckTitle')} /> + <Pin + ref={pinRef} + expectedPin={Storage.getPin()} + onSuccess={onSuccess} + onError={onError} + /> + <Error text={error} /> + </Frame> + ); + +})); + +export default PageAuthPinCheck; \ No newline at end of file diff --git a/src/ts/component/page/auth/select.tsx b/src/ts/component/page/auth/select.tsx index ec9debab21..97406ee6bc 100644 --- a/src/ts/component/page/auth/select.tsx +++ b/src/ts/component/page/auth/select.tsx @@ -1,67 +1,50 @@ -import * as React from 'react'; -import { Frame, Label, Button, Header, Footer, Error } from 'Component'; -import { I, U, translate, Animation, analytics } from 'Lib'; +import React, { forwardRef, useRef, useEffect } from 'react'; +import { Frame, Label, Button, Header, Footer } from 'Component'; +import { I, U, S, translate, Animation, analytics } from 'Lib'; -interface State { - error: string; -}; +const PageAuthSelect = forwardRef<{}, I.PageComponent>((props, ref) => { -class PageAuthSelect extends React.Component<I.PageComponent, State> { + const nodeRef = useRef(null); - node = null; - state = { - error: '', + const onLogin = () => { + Animation.from(() => U.Router.go('/auth/login', {})); }; - constructor (props: I.PageComponent) { - super(props); - - this.onLogin = this.onLogin.bind(this); - this.onRegister = this.onRegister.bind(this); + const onRegister = () => { + Animation.from(() => U.Router.go('/auth/onboard', {})); }; - render () { - const { error } = this.state; - - return ( - <div ref={ref => this.node = ref}> - <Header {...this.props} component="authIndex" /> - <Frame> - <div className="logo animation" /> - <Label className="descr animation" text={translate('authSelectLabel')} /> - <Error text={error} /> + useEffect(() => { + S.Auth.clearAll(); - <div className="buttons"> - <div className="animation"> - <Button text={translate('authSelectSignup')} onClick={this.onRegister} /> - </div> - <div className="animation"> - <Button text={translate('authSelectLogin')} color="blank" onClick={this.onLogin} /> - </div> - </div> - </Frame> - <Footer {...this.props} className="animation" component="authDisclaimer" /> - </div> - ); - }; - - componentDidMount (): void { Animation.to(() => { - U.Common.renderLinks($(this.node)); + U.Common.renderLinks($(nodeRef.current)); analytics.removeContext(); analytics.event('ScreenIndex'); }); - }; - - onLogin () { - Animation.from(() => U.Router.go('/auth/login', {})); - }; - - onRegister () { - Animation.from(() => U.Router.go('/auth/onboard', {})); - }; + }, []); + + return ( + <div ref={nodeRef}> + <Header {...props} component="authIndex" /> + <Frame> + <div className="logo animation" /> + <Label className="descr animation" text={translate('authSelectLabel')} /> + + <div className="buttons"> + <div className="animation"> + <Button text={translate('authSelectSignup')} onClick={onRegister} /> + </div> + <div className="animation"> + <Button text={translate('authSelectLogin')} color="blank" onClick={onLogin} /> + </div> + </div> + </Frame> + <Footer {...props} className="animation" component="authDisclaimer" /> + </div> + ); -}; +}); export default PageAuthSelect; \ No newline at end of file diff --git a/src/ts/component/page/auth/setup.tsx b/src/ts/component/page/auth/setup.tsx index 5b88f9404b..800526c320 100644 --- a/src/ts/component/page/auth/setup.tsx +++ b/src/ts/component/page/auth/setup.tsx @@ -1,109 +1,16 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useEffect } from 'react'; import { observer } from 'mobx-react'; import { Frame, Title, Label, Button, Footer, Icon, Loader } from 'Component'; -import { I, S, C, U, J, Storage, translate, Action, Animation, analytics, Renderer } from 'Lib'; +import { I, S, C, U, J, Storage, translate, Action, Animation, analytics, Renderer, Survey } from 'Lib'; -interface State { - index: number; - error: { description: string, code: number }; -}; +const PageAuthSetup = observer(forwardRef<{}, I.PageComponent>((props, ref) => { -const PageAuthSetup = observer(class PageAuthSetup extends React.Component<I.PageComponent, State> { + const [ error, setError ] = useState<I.Error>({ code: 0, description: '' }); + const cn = [ 'animation' ]; + const { match } = props; + const { account } = S.Auth; - node = null; - refFrame = null; - i = 0; - state = { - index: 0, - error: null, - }; - - constructor (props: I.PageComponent) { - super(props); - - this.onCancel = this.onCancel.bind(this); - this.onBackup = this.onBackup.bind(this); - this.setError = this.setError.bind(this); - }; - - render () { - const error = this.state.error || {}; - const back = <Icon className="arrow back" onClick={this.onCancel} />; - const cn = [ 'animation' ]; - - let loader = null; - let title = ''; - let label = ''; - let buttonText = translate('commonBack'); - let buttonClick = this.onCancel; - - if (error.code) { - if (error.code == J.Error.Code.FAILED_TO_FIND_ACCOUNT_INFO) { - title = translate('pageAuthSetupImportTitle'); - label = translate('pageAuthSetupImportText'); - buttonText = translate('pageAuthSetupImportBackup'); - buttonClick = this.onBackup; - cn.push('fromBackup'); - } else { - title = translate('commonError'); - label = error.description; - buttonText = translate('commonBack'); - buttonClick = this.onCancel; - }; - } else { - title = translate('pageAuthSetupEntering'); - loader = <Loader className="animation" />; - }; - - return ( - <div - ref={node => this.node = node} - className="wrapper" - > - <Footer {...this.props} component="authIndex" /> - - <Frame ref={ref => this.refFrame = ref}> - {back} - - {title ? <Title className={cn.join(' ')} text={title} /> : ''} - {label ? <Label className={cn.join(' ')} text={label} /> : ''} - {loader} - - <div className="buttons"> - <div className="animation"> - <Button text={buttonText} className="c28" onClick={buttonClick} /> - </div> - </div> - </Frame> - </div> - ); - }; - - componentDidMount () { - const { match } = this.props; - const { account } = S.Auth; - - switch (match?.params?.id) { - case 'init': { - this.init(); - break; - }; - - case 'select': { - this.select(account.id, true); - break; - }; - - }; - - Animation.to(); - }; - - componentDidUpdate (): void { - Animation.to(); - }; - - init () { + const init = () => { const { dataPath } = S.Common; const accountId = Storage.get('accountId'); @@ -114,17 +21,17 @@ const PageAuthSetup = observer(class PageAuthSetup extends React.Component<I.Pag Renderer.send('keytarGet', accountId).then((phrase: string) => { C.WalletRecover(dataPath, phrase, (message: any) => { - if (this.setError(message.error)) { + if (setErrorHandler(message.error)) { return; }; if (phrase) { U.Data.createSession(phrase, '' ,(message: any) => { - if (this.setError(message.error)) { + if (setErrorHandler(message.error)) { return; }; - this.select(accountId, false); + select(accountId, false); }); } else { U.Router.go('/auth/select', { replace: true }); @@ -133,7 +40,7 @@ const PageAuthSetup = observer(class PageAuthSetup extends React.Component<I.Pag }); }; - select (accountId: string, animate: boolean) { + const select = (accountId: string, animate: boolean) => { const { networkConfig } = S.Auth; const { dataPath } = S.Common; const { mode, path } = networkConfig; @@ -141,7 +48,7 @@ const PageAuthSetup = observer(class PageAuthSetup extends React.Component<I.Pag C.AccountSelect(accountId, dataPath, mode, path, (message: any) => { const { account } = message; - if (this.setError(message.error) || !account) { + if (setErrorHandler(message.error) || !account) { return; }; @@ -149,36 +56,119 @@ const PageAuthSetup = observer(class PageAuthSetup extends React.Component<I.Pag S.Common.configSet(account.config, false); const spaceId = Storage.get('spaceId'); + const routeParam = { + replace: true, + onFadeIn: () => { + [ + I.SurveyType.Register, + I.SurveyType.Object, + I.SurveyType.Pmf, + ].forEach(it => Survey.check(it)); + }, + }; + if (spaceId) { - U.Router.switchSpace(spaceId, '', false, {}); + U.Router.switchSpace(spaceId, '', false, routeParam); } else { - U.Data.onAuthWithoutSpace({ replace: true }); + U.Data.onAuthWithoutSpace(routeParam); }; U.Data.onInfo(account.info); U.Data.onAuthOnce(false); + analytics.event('SelectAccount', { middleTime: message.middleTime }); }); }; - setError (error: { description: string, code: number}) { + const setErrorHandler = (error: I.Error) => { if (!error.code) { return false; }; - this.setState({ error }); + if (error.code == J.Error.Code.ACCOUNT_STORE_NOT_MIGRATED) { + U.Router.go('/auth/migrate', {}); + return; + }; + + setError(error); return U.Common.checkErrorCommon(error.code); }; - onBackup () { - Action.restoreFromBackup(this.setError); + const onBackup = () => { + Action.restoreFromBackup(setErrorHandler); }; - onCancel () { + const onCancel = () => { S.Auth.logout(true, false); Animation.from(() => U.Router.go('/', { replace: true })); }; -}); + const back = <Icon className="arrow back" onClick={onCancel} />; + + let loader = null; + let title = ''; + let label = ''; + let buttonText = translate('commonBack'); + let buttonClick = onCancel; + + if (error.code) { + if (error.code == J.Error.Code.FAILED_TO_FIND_ACCOUNT_INFO) { + title = translate('pageAuthSetupImportTitle'); + label = translate('pageAuthSetupImportText'); + buttonText = translate('pageAuthSetupImportBackup'); + buttonClick = onBackup; + cn.push('fromBackup'); + } else { + title = translate('commonError'); + label = error.description; + buttonText = translate('commonBack'); + buttonClick = onCancel; + }; + } else { + title = translate('pageAuthSetupEntering'); + loader = <Loader className="animation" />; + }; + + useEffect(() => { + switch (match?.params?.id) { + case 'init': { + init(); + break; + }; + + case 'select': { + select(account.id, true); + break; + }; + }; + }, []); + + useEffect(() => { + Animation.to(); + }); + + return ( + <div + className="wrapper" + > + <Frame> + {back} + + {title ? <Title className={cn.join(' ')} text={title} /> : ''} + {label ? <Label className={cn.join(' ')} text={label} /> : ''} + {loader} + + <div className="buttons"> + <div className="animation"> + <Button text={buttonText} className="c28" onClick={buttonClick} /> + </div> + </div> + </Frame> + + <Footer {...props} component="authIndex" /> + </div> + ); + +})); -export default PageAuthSetup; +export default PageAuthSetup; \ No newline at end of file diff --git a/src/ts/component/page/elements/children.tsx b/src/ts/component/page/elements/children.tsx index 270311f847..34e766f6cb 100644 --- a/src/ts/component/page/elements/children.tsx +++ b/src/ts/component/page/elements/children.tsx @@ -1,25 +1,23 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { Block } from 'Component'; import { observer } from 'mobx-react'; import { I, S } from 'Lib'; -const Children = observer(class Children extends React.Component<I.BlockComponent> { - - render () { - const { rootId } = this.props; - const childrenIds = S.Block.getChildrenIds(rootId, rootId); - const children = S.Block.getChildren(rootId, rootId, it => !it.isLayoutHeader()); - const length = childrenIds.length; +const Children = observer(forwardRef<{}, I.BlockComponent>((props, ref) => { - return ( - <React.Fragment> - {children.map((block: I.Block, i: number) => ( - <Block key={`block-${block.id}`} {...this.props} block={block} index={i} className={i == 0 ? 'isFirst' : ''} /> - ))} - </React.Fragment> - ); - }; + const { rootId } = props; + const childrenIds = S.Block.getChildrenIds(rootId, rootId); + const children = S.Block.getChildren(rootId, rootId, it => !it.isLayoutHeader()); + const length = childrenIds.length; -}); + return ( + <> + {children.map((block: I.Block, i: number) => ( + <Block key={`block-${block.id}`} {...props} block={block} index={i} className={i == 0 ? 'isFirst' : ''} /> + ))} + </> + ); + +})); export default Children; \ No newline at end of file diff --git a/src/ts/component/page/elements/head/banner.tsx b/src/ts/component/page/elements/head/banner.tsx index 1402a6d0c6..bf0b7ce853 100644 --- a/src/ts/component/page/elements/head/banner.tsx +++ b/src/ts/component/page/elements/head/banner.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC, useEffect, useRef } from 'react'; import $ from 'jquery'; import { IconObject, Label, ObjectName } from 'Component'; import { I, C, S, U, J, Action, translate, analytics, Onboarding } from 'Lib'; @@ -10,100 +10,22 @@ interface Props { isPopup?: boolean; }; -class HeaderBanner extends React.Component<Props> { +const HeaderBanner: FC<Props> = ({ + type, + object, + count = 0, + isPopup, +}) => { - node: any = null; + const nodeRef = useRef(null); + const cn = [ 'headerBanner' ]; + const canWrite = U.Space.canMyParticipantWrite(); - constructor (props: Props) { - super(props); - - this.onTemplateMenu = this.onTemplateMenu.bind(this); - }; - - render () { - const { type, object, count } = this.props; - const cn = [ 'headerBanner' ]; - - let label = ''; - let target = null; - let action = null; - let onClick = null; - - switch (type) { - case I.BannerType.IsArchived: { - label = translate('deletedBanner'); - action = ( - <div - className="action" - onClick={() => Action.restore([ object.id ], analytics.route.banner)} - > - {translate('deletedBannerRestore')} - </div> - ); - break; - }; - - case I.BannerType.IsTemplate: { - const targetObjectType = S.Record.getTypeById(object.targetObjectType); - - label = translate('templateBannner'); - if (targetObjectType) { - target = ( - <div className="typeName" onClick={() => U.Object.openAuto(targetObjectType)}> - {translate('commonOf')} - <IconObject size={18} object={targetObjectType} /> - <ObjectName object={targetObjectType} /> - </div> - ); - }; - break; - }; - - case I.BannerType.TemplateSelect: { - cn.push('withMenu'); - - if (count) { - label = U.Common.sprintf(translate('selectTemplateBannerWithNumber'), count, U.Common.plural(count, translate('pluralTemplate'))); - } else { - label = translate('selectTemplateBanner'); - }; - - onClick = this.onTemplateMenu; - break; - }; - }; - - return ( - <div - ref={node => this.node = node} - id="headerBanner" - className={cn.join(' ')} - onClick={onClick} - > - <div className="content"> - <Label text={label} /> - {target} - </div> - - {action} - </div> - ); - }; - - componentDidMount (): void { - const { type, isPopup } = this.props; - - if (type == I.BannerType.TemplateSelect) { - Onboarding.start('templateSelect', isPopup); - }; - }; - - onTemplateMenu () { - const { object, isPopup } = this.props; + const onTemplateMenu = () => { const { sourceObject } = object; const type = S.Record.getTypeById(object.type); const templateId = sourceObject || J.Constant.templateId.blank; - const node = $(this.node); + const node = $(nodeRef.current); if (!type || S.Menu.isOpen('dataviewTemplateList')) { return; @@ -146,6 +68,79 @@ class HeaderBanner extends React.Component<Props> { }); }; + let label = ''; + let target = null; + let action = null; + let onClick = null; + + switch (type) { + case I.BannerType.IsArchived: { + label = translate('deletedBanner'); + if (canWrite) { + action = ( + <div + className="action" + onClick={() => Action.restore([ object.id ], analytics.route.banner)} + > + {translate('deletedBannerRestore')} + </div> + ); + }; + break; + }; + + case I.BannerType.IsTemplate: { + const targetObjectType = S.Record.getTypeById(object.targetObjectType); + + label = translate('templateBannner'); + if (targetObjectType) { + target = ( + <div className="typeName" onClick={() => U.Object.openAuto(targetObjectType)}> + {translate('commonOf')} + <IconObject size={18} object={targetObjectType} /> + <ObjectName object={targetObjectType} /> + </div> + ); + }; + break; + }; + + case I.BannerType.TemplateSelect: { + cn.push('withMenu'); + + if (count) { + label = U.Common.sprintf(translate('selectTemplateBannerWithNumber'), count, U.Common.plural(count, translate('pluralTemplate'))); + } else { + label = translate('selectTemplateBanner'); + }; + + onClick = onTemplateMenu; + break; + }; + }; + + useEffect(() => { + if (type == I.BannerType.TemplateSelect) { + Onboarding.start('templateSelect', isPopup); + }; + }, []); + + return ( + <div + ref={nodeRef} + id="headerBanner" + className={cn.join(' ')} + onClick={onClick} + > + <div className="content"> + <Label text={label} /> + {target} + </div> + + {action} + </div> + ); + }; export default HeaderBanner; \ No newline at end of file diff --git a/src/ts/component/page/elements/head/editor.tsx b/src/ts/component/page/elements/head/editor.tsx index e536d34771..4c1f276ac9 100644 --- a/src/ts/component/page/elements/head/editor.tsx +++ b/src/ts/component/page/elements/head/editor.tsx @@ -31,14 +31,12 @@ const PageHeadEditor = observer(class PageHeadEditor extends React.Component<Pro }; const check = U.Data.checkDetails(rootId); - const object = S.Detail.get(rootId, rootId, [ 'layoutAlign' ], true); + const object = S.Detail.get(rootId, rootId, [ 'layout', 'layoutAlign' ], true); const header = S.Block.getLeaf(rootId, 'header'); const cover = new M.Block({ id: rootId + '-cover', type: I.BlockType.Cover, hAlign: object.layoutAlign, childrenIds: [], fields: {}, content: {} }); const icon: any = new M.Block({ id: rootId + '-icon', type: I.BlockType.IconPage, hAlign: object.layoutAlign, childrenIds: [], fields: {}, content: {} }); - const isHuman = U.Object.isHumanLayout(object.layout); - const isParticipant = U.Object.isParticipantLayout(object.layout); - if (isHuman || isParticipant) { + if (U.Object.isInHumanLayouts(object.layout)) { icon.type = I.BlockType.IconUser; }; diff --git a/src/ts/component/page/elements/head/simple.tsx b/src/ts/component/page/elements/head/simple.tsx index cccc0644a3..95d9bd5580 100644 --- a/src/ts/component/page/elements/head/simple.tsx +++ b/src/ts/component/page/elements/head/simple.tsx @@ -9,7 +9,9 @@ interface Props { isContextMenuDisabled?: boolean; readonly?: boolean; noIcon?: boolean; + relationKey?: string; onCreate?: () => void; + getDotMap?: (start: number, end: number, callback: (res: Map<string, boolean>) => void) => void; }; const EDITORS = [ @@ -114,6 +116,10 @@ const HeadSimple = observer(class Controls extends React.Component<Props> { button = <Button id="button-install" text={translate('pageHeadSimpleInstall')} color={color} className={cn.join(' ')} onClick={onClick} />; }; + + if (!canWrite) { + button = null; + }; }; if (isDate) { @@ -126,10 +132,6 @@ const HeadSimple = observer(class Controls extends React.Component<Props> { ); }; - if (!canWrite) { - button = null; - }; - return ( <div ref={node => this.node = node} className={cn.join(' ')}> <div className="side left"> @@ -286,25 +288,25 @@ const HeadSimple = observer(class Controls extends React.Component<Props> { const { rootId } = this.props; const object = S.Detail.get(rootId, rootId); - let sources: string[] = []; + let sources: any[] = []; switch (object.layout) { case I.ObjectLayout.Type: { - sources = S.Record.getTypes().map(it => it.sourceObject); + sources = S.Record.getTypes(); break; }; case I.ObjectLayout.Relation: { - sources = S.Record.getRelations().map(it => it.sourceObject); + sources = S.Record.getRelations(); break; }; }; - return sources.includes(rootId); + return sources.map(it => it.sourceObject).includes(rootId); }; onCalendar = () => { - const { rootId } = this.props; + const { rootId, getDotMap, relationKey } = this.props; const object = S.Detail.get(rootId, rootId); S.Menu.open('dataviewCalendar', { @@ -314,7 +316,9 @@ const HeadSimple = observer(class Controls extends React.Component<Props> { value: object.timestamp, canEdit: true, canClear: false, - onChange: (value: number) => U.Object.openDateByTimestamp(value), + relationKey, + onChange: (value: number) => U.Object.openDateByTimestamp(relationKey, value), + getDotMap, }, }); @@ -322,10 +326,10 @@ const HeadSimple = observer(class Controls extends React.Component<Props> { }; changeDate = (dir: number) => { - const { rootId } = this.props; + const { rootId, relationKey } = this.props; const object = S.Detail.get(rootId, rootId); - U.Object.openDateByTimestamp(object.timestamp + dir * 86400); + U.Object.openDateByTimestamp(relationKey, object.timestamp + dir * 86400); analytics.event(dir > 0 ? 'ClickDateForward' : 'ClickDateBack'); }; diff --git a/src/ts/component/page/index.tsx b/src/ts/component/page/index.tsx index 7ed6edabe5..89ab32ff28 100644 --- a/src/ts/component/page/index.tsx +++ b/src/ts/component/page/index.tsx @@ -11,6 +11,7 @@ import PageAuthPinCheck from './auth/pinCheck'; import PageAuthSetup from './auth/setup'; import PageAuthOnboard from './auth/onboard'; import PageAuthDeleted from './auth/deleted'; +import PageAuthMigrate from './auth/migrate'; import PageMainBlank from './main/blank'; import PageMainEmpty from './main/empty'; @@ -23,7 +24,6 @@ import PageMainMedia from './main/media'; import PageMainRelation from './main/relation'; import PageMainGraph from './main/graph'; import PageMainNavigation from './main/navigation'; -import PageMainCreate from './main/create'; import PageMainArchive from './main/archive'; import PageMainImport from './main/import'; import PageMainInvite from './main/invite'; @@ -42,6 +42,7 @@ const Components = { 'auth/setup': PageAuthSetup, 'auth/onboard': PageAuthOnboard, 'auth/deleted': PageAuthDeleted, + 'auth/migrate': PageAuthMigrate, 'main/blank': PageMainBlank, 'main/empty': PageMainEmpty, @@ -53,7 +54,6 @@ const Components = { 'main/relation': PageMainRelation, 'main/graph': PageMainGraph, 'main/navigation': PageMainNavigation, - 'main/create': PageMainCreate, 'main/archive': PageMainArchive, 'main/import': PageMainImport, 'main/invite': PageMainInvite, @@ -155,6 +155,8 @@ const Page = observer(class Page extends React.Component<I.PageComponent> { ret.params.action = 'object'; ret.params.id = data.objectId; ret.params.spaceId = data.spaceId; + ret.params.cid = data.cid; + ret.params.key = data.key; }; // Invite route diff --git a/src/ts/component/page/main/archive.tsx b/src/ts/component/page/main/archive.tsx index 996bd4e70b..447dd6bd17 100644 --- a/src/ts/component/page/main/archive.tsx +++ b/src/ts/component/page/main/archive.tsx @@ -1,101 +1,42 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; +import React, { forwardRef, useRef, useState, useImperativeHandle } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; -import { Title, Footer, Icon, ListObjectManager } from 'Component'; +import { Title, Footer, Icon, ListManager } from 'Component'; import { I, U, J, translate, Action, analytics } from 'Lib'; -const PageMainArchive = observer(class PageMainArchive extends React.Component<I.PageComponent> { +const PageMainArchive = observer(forwardRef<I.PageRef, I.PageComponent>((props, ref) => { - refManager: any = null; - rowLength = 0; + const { isPopup } = props; + const nodeRef = useRef(null); + const managerRef = useRef(null); + const [ rowLength, setRowLength ] = useState(0); - constructor (props: I.PageComponent) { - super(props); - - this.onRestore = this.onRestore.bind(this); - this.onRemove = this.onRemove.bind(this); - this.resize = this.resize.bind(this); - this.getRowLength = this.getRowLength.bind(this); - }; - - render () { - const filters: I.Filter[] = [ - { relationKey: 'isArchived', condition: I.FilterCondition.Equal, value: true }, - ]; - const sorts: I.Sort[] = [ - { relationKey: 'lastModifiedDate', type: I.SortType.Desc }, - ]; - - const buttons: I.ButtonComponent[] = [ - { icon: 'restore', text: translate('commonRestore'), onClick: this.onRestore }, - { icon: 'remove', text: translate('commonDeleteImmediately'), onClick: this.onRemove } - ]; - - return ( - <div className="wrapper"> - <div className="body"> - <div className="titleWrapper"> - <Icon className="archive" /> - <Title text={translate('commonBin')} /> - </div> - - <ListObjectManager - ref={ref => this.refManager = ref} - subId={J.Constant.subId.archive} - filters={filters} - sorts={sorts} - rowLength={this.getRowLength()} - ignoreArchived={false} - buttons={buttons} - iconSize={48} - resize={this.resize} - textEmpty={translate('pageMainArchiveEmpty')} - isReadonly={!U.Space.canMyParticipantWrite()} - /> - </div> - - <Footer component="mainObject" /> - </div> - ); + const onRestore = () => { + Action.restore(managerRef.current?.getSelected(), analytics.route.archive); + selectionClear(); }; - componentDidMount () { - analytics.event('ScreenBin'); - }; - - onRestore () { - if (!this.refManager) { - return; - }; - - Action.restore(this.refManager.selected || [], analytics.route.archive); - this.selectionClear(); + const onRemove = () => { + Action.delete(managerRef.current?.getSelected(), 'Bin', () => selectionClear()); }; - onRemove () { - Action.delete(this.refManager?.selected || [], 'Bin', () => this.selectionClear()); + const selectionClear = () => { + managerRef.current?.selectionClear(); }; - selectionClear () { - this.refManager?.selectionClear(); + const getRowLength = () => { + return U.Common.getWindowDimensions().ww <= 940 ? 2 : 3; }; - getRowLength () { - const { ww } = U.Common.getWindowDimensions(); - return ww <= 940 ? 2 : 3; - }; - - resize () { - const { isPopup } = this.props; + const resize = () => { const win = $(window); const container = U.Common.getPageContainer(isPopup); - const node = $(ReactDOM.findDOMNode(this)); + const node = $(nodeRef.current); const content = $('#popupPage .content'); const body = node.find('.body'); const hh = J.Size.header; const wh = isPopup ? container.height() : win.height(); - const rowLength = this.getRowLength(); + const rl = getRowLength(); node.css({ height: wh }); @@ -107,12 +48,54 @@ const PageMainArchive = observer(class PageMainArchive extends React.Component<I content.css({ minHeight: '', height: '' }); }; - if (this.rowLength != rowLength) { - this.rowLength = rowLength; - this.forceUpdate(); - }; + if (rowLength != rl) { + setRowLength(rl); + }; }; -}); + const filters: I.Filter[] = [ + { relationKey: 'isArchived', condition: I.FilterCondition.Equal, value: true }, + ]; + const sorts: I.Sort[] = [ + { relationKey: 'lastModifiedDate', type: I.SortType.Desc }, + ]; + + const buttons: I.ButtonComponent[] = [ + { icon: 'restore', text: translate('commonRestore'), onClick: onRestore }, + { icon: 'remove', text: translate('commonDeleteImmediately'), onClick: onRemove } + ]; + + useImperativeHandle(ref, () => ({ + resize, + })); + + return ( + <div ref={nodeRef} className="wrapper"> + <div className="body"> + <div className="titleWrapper"> + <Icon className="archive" /> + <Title text={translate('commonBin')} /> + </div> + + <ListManager + ref={managerRef} + subId={J.Constant.subId.archive} + filters={filters} + sorts={sorts} + rowLength={getRowLength()} + ignoreArchived={false} + buttons={buttons} + iconSize={48} + resize={resize} + textEmpty={translate('pageMainArchiveEmpty')} + isReadonly={!U.Space.canMyParticipantWrite()} + /> + </div> + + <Footer component="mainObject" /> + </div> + ); + +})); -export default PageMainArchive; +export default PageMainArchive; \ No newline at end of file diff --git a/src/ts/component/page/main/blank.tsx b/src/ts/component/page/main/blank.tsx index 18a564ef21..fe0f770474 100644 --- a/src/ts/component/page/main/blank.tsx +++ b/src/ts/component/page/main/blank.tsx @@ -1,12 +1,12 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { I } from 'Lib'; -class PageMainBlank extends React.Component<I.PageComponent> { +const PageMainBlank = forwardRef<{}, I.PageComponent>(() => { - render () { - return <div />; - }; + return ( + <div /> + ); -}; +}); export default PageMainBlank; \ No newline at end of file diff --git a/src/ts/component/page/main/chat.tsx b/src/ts/component/page/main/chat.tsx index e5d8dca597..176d10c884 100644 --- a/src/ts/component/page/main/chat.tsx +++ b/src/ts/component/page/main/chat.tsx @@ -1,269 +1,81 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; import { Header, Footer, Loader, Block, Deleted } from 'Component'; -import { I, M, C, S, U, J, Action, keyboard, analytics } from 'Lib'; +import { I, M, C, S, U, J, Action } from 'Lib'; -interface State { - isLoading: boolean; - isDeleted: boolean; -}; +const PageMainChat = observer(forwardRef<{}, I.PageComponent>((props, ref) => { -const PageMainChat = observer(class PageMainChat extends React.Component<I.PageComponent, State> { + const { isPopup, match } = props; + const nodeRef = useRef(null); + const headerRef = useRef(null); + const idRef = useRef(''); + const [ isLoading, setIsLoading ] = useState(false); + const [ isDeleted, setIsDeleted ] = useState(false); + const rootId = props.rootId ? props.rootId : match?.params?.id; + const object = S.Detail.get(rootId, rootId, [ 'chatId' ]); - _isMounted = false; - node: any = null; - id = ''; - refHeader: any = null; - refHead: any = null; - refControls: any = null; - loading = false; - timeout = 0; - - state = { - isLoading: false, - isDeleted: false, - }; - - constructor (props: I.PageComponent) { - super(props); - - this.resize = this.resize.bind(this); - }; - - render () { - const { isLoading, isDeleted } = this.state; - const rootId = this.getRootId(); - const readonly = this.isReadonly(); - const object = S.Detail.get(rootId, rootId, [ 'chatId' ]); - - if (isDeleted) { - return <Deleted {...this.props} />; - }; - - let content = null; - - if (isLoading) { - content = <Loader id="loader" />; - } else { - const chat = new M.Block({ id: J.Constant.blockId.chat, type: I.BlockType.Chat, childrenIds: [], fields: {}, content: {} }); - - content = ( - <div className="blocks"> - <Block - {...this.props} - key={chat.id} - rootId={rootId} - iconSize={20} - block={chat} - className="noPlus" - isSelectionDisabled={true} - isContextMenuDisabled={true} - readonly={readonly} - /> - </div> - ); - }; - - return ( - <div ref={node => this.node = node}> - <Header - {...this.props} - component="mainChat" - ref={ref => this.refHeader = ref} - rootId={object.chatId} - /> - - <div id="bodyWrapper" className="wrapper"> - <div className="editorWrapper isChat"> - {content} - </div> - </div> - - <Footer component="mainObject" {...this.props} /> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - this.open(); - this.rebind(); - }; - - componentDidUpdate () { - this.open(); - this.resize(); - this.checkDeleted(); + if (isDeleted || object.isDeleted) { + return <Deleted {...props} />; }; - componentWillUnmount () { - this._isMounted = false; - this.close(); - this.unbind(); - }; - - unbind () { - const namespace = this.getNamespace(); - const events = [ 'keydown', 'scroll' ]; - - $(window).off(events.map(it => `${it}.set${namespace}`).join(' ')); - }; - - rebind () { - const { isPopup } = this.props; - const win = $(window); - const namespace = this.getNamespace(); - const container = U.Common.getScrollContainer(isPopup); - - this.unbind(); - - win.on(`keydown.set${namespace}`, e => this.onKeyDown(e)); - container.on(`scroll.set${namespace}`, () => this.onScroll()); - }; - - checkDeleted () { - const { isDeleted } = this.state; - if (isDeleted) { + const open = () => { + if (idRef.current == rootId) { return; }; - const rootId = this.getRootId(); - const object = S.Detail.get(rootId, rootId, []); - - if (object.isDeleted) { - this.setState({ isDeleted: true }); - }; - }; - - getNamespace () { - return this.props.isPopup ? '-popup' : ''; - }; - - open () { - const rootId = this.getRootId(); + close(); + setIsLoading(true); + setIsDeleted(false); - if (this.id == rootId) { - return; - }; - - this.close(); - this.id = rootId; - this.setState({ isDeleted: false, isLoading: true }); + idRef.current = rootId; C.ObjectOpen(rootId, '', U.Router.getRouteSpaceId(), (message: any) => { + setIsLoading(false); + if (!U.Common.checkErrorOnOpen(rootId, message.error.code, this)) { return; }; const object = S.Detail.get(rootId, rootId, []); if (object.isDeleted) { - this.setState({ isDeleted: true, isLoading: false }); return; }; - this.refHeader?.forceUpdate(); - this.refHead?.forceUpdate(); - this.refControls?.forceUpdate(); - this.setState({ isLoading: false }); - this.resize(); + headerRef.current.forceUpdate(); + resize(); }); }; - close () { - if (!this.id) { - return; - }; - - const { isPopup, match } = this.props; - - let close = true; - if (isPopup && (match.params.id == this.id)) { - close = false; - }; - if (close) { - Action.pageClose(this.id, true); - }; - }; - - getRootId () { - const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; - }; + const close = () => { + const id = idRef.current; - onScroll () { - const { isPopup } = this.props; - - if (!isPopup && keyboard.isPopup()) { - return; - }; - - const selection = S.Common.getRef('selectionProvider'); - if (selection) { - selection.renderSelection(); - }; - }; - - onKeyDown (e: any): void { - const { isPopup } = this.props; - - if (!isPopup && keyboard.isPopup()) { + if (!id) { return; }; - const node = $(this.node); - const selection = S.Common.getRef('selectionProvider'); - const cmd = keyboard.cmdKey(); - const ids = selection ? selection.get(I.SelectType.Record) : []; - const count = ids.length; - const rootId = this.getRootId(); + const close = !(isPopup && (match?.params?.id == id)); - keyboard.shortcut(`${cmd}+f`, e, () => { - e.preventDefault(); - - node.find('#dataviewControls .filter .icon.search').trigger('click'); - }); - - if (!keyboard.isFocused) { - keyboard.shortcut(`${cmd}+a`, e, () => { - e.preventDefault(); - - const records = S.Record.getRecordIds(S.Record.getSubId(rootId, J.Constant.blockId.dataview), ''); - selection.set(I.SelectType.Record, records); - }); - - if (count && !S.Menu.isOpen()) { - keyboard.shortcut('backspace, delete', e, () => { - e.preventDefault(); - Action.archive(ids, analytics.route.chat); - selection.clear(); - }); - }; + if (close) { + Action.pageClose(id, true); }; }; - isReadonly () { - const rootId = this.getRootId(); + const isReadonly = () => { const root = S.Block.getLeaf(rootId, rootId); const object = S.Detail.get(rootId, rootId, []); return !U.Space.canMyParticipantWrite() || object.isArchived || root?.isLocked(); }; - resize () { - const { isLoading } = this.state; - const { isPopup } = this.props; - - if (!this._isMounted || isLoading) { + const resize = () => { + if (isLoading) { return; }; - const rootId = this.getRootId(); - const check = U.Data.checkDetails(rootId); - raf(() => { - const node = $(this.node); + const node = $(nodeRef.current); const cover = node.find('.block.blockCover'); const pageContainer = U.Common.getPageContainer(isPopup); const scrollContainer = U.Common.getScrollContainer(isPopup); @@ -281,12 +93,62 @@ const PageMainChat = observer(class PageMainChat extends React.Component<I.PageC const fh = Number(formWrapper.outerHeight(true)) || 0; const ch = Number(controls.outerHeight(true)) || 0; const hh = Number(head.outerHeight(true)) || 0; - const mh = scrollContainer.height() - headerHeight - fh - ch - hh - (check.withCover ? J.Size.coverPadding : 0); + const mh = scrollContainer.height() - headerHeight - fh - ch - hh; scrollWrapper.css({ minHeight: mh }); }); }; -}); + let content = null; + + if (isLoading) { + content = <Loader id="loader" />; + } else { + content = ( + <div className="blocks"> + <Block + {...props} + key={J.Constant.blockId.chat} + rootId={rootId} + iconSize={20} + block={new M.Block({ id: J.Constant.blockId.chat, type: I.BlockType.Chat, childrenIds: [], fields: {}, content: {} })} + className="noPlus" + isSelectionDisabled={true} + isContextMenuDisabled={true} + readonly={isReadonly()} + /> + </div> + ); + }; + + useEffect(() => { + return () => close(); + }, []); + + useEffect(() => { + open(); + resize(); + }); + + return ( + <div ref={nodeRef}> + <Header + {...props} + component="mainChat" + ref={headerRef} + rootId={object.chatId} + /> + + <div id="bodyWrapper" className="wrapper"> + <div className="editorWrapper isChat"> + {content} + </div> + </div> + + <Footer component="mainObject" {...props} /> + </div> + ); + +})); export default PageMainChat; \ No newline at end of file diff --git a/src/ts/component/page/main/create.tsx b/src/ts/component/page/main/create.tsx deleted file mode 100644 index b01770deff..0000000000 --- a/src/ts/component/page/main/create.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { Loader, Frame } from 'Component'; -import { I, U } from 'Lib'; - -class PageMainCreate extends React.Component<I.PageComponent> { - - render () { - return ( - <Frame> - <Loader id="loader" /> - </Frame> - ); - }; - - componentDidMount () { - const flags = [ I.ObjectFlag.DeleteEmpty, I.ObjectFlag.SelectType ]; - U.Object.create('', '', {}, I.BlockPosition.Bottom, '', flags, '', message => U.Object.openRoute(message.details)); - }; - -}; - -export default PageMainCreate; \ No newline at end of file diff --git a/src/ts/component/page/main/date.tsx b/src/ts/component/page/main/date.tsx index 786b689882..721d8136fd 100644 --- a/src/ts/component/page/main/date.tsx +++ b/src/ts/component/page/main/date.tsx @@ -1,17 +1,17 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Header, Footer, Deleted, ListObject, Button } from 'Component'; -import { I, C, S, U, Action, translate, analytics } from 'Lib'; -import HeadSimple from 'Component/page/elements/head/simple'; +import { Header, Footer, Deleted, ListObject, Button, Label, Loader, HeadSimple } from 'Component'; +import { I, C, S, U, J, Action, translate, analytics, keyboard } from 'Lib'; +import { eachDayOfInterval, isEqual, format, fromUnixTime } from 'date-fns'; interface State { isDeleted: boolean; + isLoading: boolean; relations: any[]; - selectedRelation: string; + relationKey: string; }; const SUB_ID = 'dateListObject'; -const RELATION_KEY_MENTION = 'mentions'; const PageMainDate = observer(class PageMainDate extends React.Component<I.PageComponent, State> { @@ -22,18 +22,18 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC refHead: any = null; refList: any = null; refCalIcon: any = null; - loading = false; timeout = 0; state = { isDeleted: false, + isLoading: false, relations: [], - selectedRelation: RELATION_KEY_MENTION, + relationKey: J.Relation.key.mention, }; render () { const { space } = S.Common; - const { isDeleted, relations, selectedRelation } = this.state; + const { isLoading, isDeleted, relations, relationKey } = this.state; const rootId = this.getRootId(); const object = S.Detail.get(rootId, rootId, []); @@ -41,77 +41,109 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC return <Deleted {...this.props} />; }; - const relation = S.Record.getRelationByKey(selectedRelation); - if (!relation) { - return null; - }; - - const columns: any[] = [ - { relationKey: 'type', name: translate('commonObjectType'), isObject: true }, - { relationKey: 'creator', name: translate('relationCreator'), isObject: true }, - ]; + const relation = S.Record.getRelationByKey(relationKey); + const dayString = U.Date.dayString(object.timestamp); + const dayName = [ U.Date.date('l', object.timestamp) ]; - const filters: I.Filter[] = []; + if (dayString) { + dayName.unshift(dayString); + }; - if (relation.format == I.RelationType.Object) { - filters.push({ relationKey: RELATION_KEY_MENTION, condition: I.FilterCondition.In, value: [ object.id ] }); + let content = null; + if (isLoading) { + content = <Loader id="loader" />; + } else + if (!relations.length || !relation) { + content = ( + <div className="emptyContainer"> + <Label text={translate('pageMainDateEmptyText')} /> + </div> + ); } else { - filters.push({ relationKey: selectedRelation, condition: I.FilterCondition.Equal, value: object.timestamp, format: I.RelationType.Date }); - }; + const columns: any[] = [ + { relationKey: 'type', name: translate('commonObjectType'), isObject: true }, + { relationKey: 'creator', name: translate('relationCreator'), isObject: true }, + ]; + + const keys = relations.map(it => it.relationKey); + const filters: I.Filter[] = []; + + if (relation.format == I.RelationType.Object) { + filters.push({ relationKey, condition: I.FilterCondition.In, value: [ object.id ] }); + } else { + filters.push({ relationKey, condition: I.FilterCondition.Equal, value: object.timestamp, format: I.RelationType.Date }); + }; - return ( - <div ref={node => this.node = node}> - <Header - {...this.props} - component="mainObject" - ref={ref => this.refHeader = ref} - rootId={rootId} - /> + if ([ 'createdDate' ].includes(relationKey)) { + filters.push({ relationKey: 'origin', condition: I.FilterCondition.NotEqual, value: I.ObjectOrigin.Builtin }); + keys.push('origin'); + }; - <div className="blocks wrapper"> - <HeadSimple - {...this.props} - noIcon={true} - ref={ref => this.refHead = ref} - rootId={rootId} - readonly={true} - /> + if ([ 'lastModifiedDate' ].includes(relationKey)) { + filters.push({ relationKey: 'createdDate', condition: I.FilterCondition.NotEqual, value: { type: 'valueFromRelation', relationKey: 'lastModifiedDate' } }); + }; + content = ( + <React.Fragment> <div className="categories"> - {relations.map((item) => { - const isMention = item.relationKey == RELATION_KEY_MENTION; + {relations.map(item => { + const isMention = item.relationKey == J.Relation.key.mention; const icon = isMention ? 'mention' : ''; - const separator = isMention ? <div className="separator" /> : ''; return ( - <React.Fragment key={item.relationKey}> - <Button - id={`category-${item.relationKey}`} - active={selectedRelation == item.relationKey} - color="blank" - className="c36" - onClick={() => this.onCategory(item.relationKey)} - icon={icon} - text={item.name} - /> - {relations.length > 1 ? separator : ''} - </React.Fragment> + <Button + id={`category-${item.relationKey}`} + key={item.relationKey} + active={relationKey == item.relationKey} + color="blank" + className="c36" + onClick={() => this.onCategoryClick(item.relationKey)} + icon={icon} + text={item.name} + /> ); })} </div> - <div className="dateList"> - <ListObject - ref={ref => this.refList = ref} - {...this.props} - spaceId={space} - subId={SUB_ID} - rootId={rootId} - columns={columns} - filters={filters} - route={analytics.route.screenDate} - /> + <ListObject + ref={ref => this.refList = ref} + {...this.props} + spaceId={space} + subId={SUB_ID} + rootId={rootId} + columns={columns} + filters={filters} + route={analytics.route.screenDate} + relationKeys={keys} + /> + </React.Fragment> + ); + }; + + return ( + <div ref={node => this.node = node}> + <Header + {...this.props} + component="mainChat" + ref={ref => this.refHeader = ref} + rootId={rootId} + /> + + <div className="blocks wrapper"> + <div className="dayName"> + {dayName.map((item, i) => <div key={i}>{item}</div>)} </div> + <HeadSimple + {...this.props} + noIcon={true} + ref={ref => this.refHead = ref} + rootId={rootId} + readonly={true} + relationKey={relationKey} + getDotMap={this.getDotMap} + /> + + {content} </div> <Footer component="mainObject" {...this.props} /> @@ -120,8 +152,16 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC }; componentDidMount () { + const match = keyboard.getMatch(); + const { relationKey } = match.params; + this._isMounted = true; - this.open(); + + if (relationKey) { + this.setState({ relationKey }, () => this.open()); + } else { + this.open(); + }; }; componentDidUpdate () { @@ -157,7 +197,7 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC this.close(); this.id = rootId; - this.setState({ isDeleted: false }); + this.setState({ isDeleted: false, isLoading: true }); C.ObjectOpen(rootId, '', U.Router.getRouteSpaceId(), (message: any) => { if (!U.Common.checkErrorOnOpen(rootId, message.error.code, this)) { @@ -183,11 +223,8 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC }; const { isPopup, match } = this.props; - - let close = true; - if (isPopup && (match.params.id == this.id)) { - close = false; - }; + const close = !(isPopup && (match?.params?.id == this.id)); + if (close) { Action.pageClose(this.id, true); }; @@ -195,11 +232,18 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC loadCategory () { const { space, config } = S.Common; + const { relationKey } = this.state; const rootId = this.getRootId(); + this.setState({ isLoading: true }); + C.RelationListWithValue(space, rootId, (message: any) => { const relations = (message.relations || []).map(it => S.Record.getRelationByKey(it.relationKey)).filter(it => { - if ([ RELATION_KEY_MENTION ].includes(it.relationKey)) { + if (!it) { + return false; + }; + + if ([ J.Relation.key.mention ].includes(it.relationKey)) { return true; }; @@ -211,32 +255,96 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC }); relations.sort((c1, c2) => { - const isMention1 = c1.relationKey == RELATION_KEY_MENTION; - const isMention2 = c2.relationKey == RELATION_KEY_MENTION; + const isMention1 = c1.relationKey == J.Relation.key.mention; + const isMention2 = c2.relationKey == J.Relation.key.mention; if (isMention1 && !isMention2) return -1; if (!isMention1 && isMention2) return 1; return 0; }); + this.setState({ relations, isLoading: false }); + if (relations.length) { - this.setState({ relations }); - this.onCategory(relations[0].relationKey); + if (!relationKey || !relations.find(it => it.relationKey == relationKey)) { + this.onCategory(relations[0].relationKey); + } else { + this.reload(); + }; + } else { + this.reload(); }; }); }; onCategory (relationKey: string) { - this.setState({ selectedRelation: relationKey }, () => { - this.refList?.getData(1); - }); + this.setState({ relationKey }, () => this.reload()); + }; + onCategoryClick (relationKey: string) { + this.onCategory(relationKey); analytics.event('SwitchRelationDate', { relationKey }); }; + reload () { + this.refList?.getData(1); + }; + getRootId () { const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + return rootId ? rootId : match?.params?.id; + }; + + getFilters = (start: number, end: number): I.Filter[] => { + const { relationKey } = this.state; + + if (!relationKey) { + return []; + }; + + return [ + + { + relationKey, + condition: I.FilterCondition.GreaterOrEqual, + value: start, + quickOption: I.FilterQuickOption.ExactDate, + format: I.RelationType.Date, + }, + { + relationKey, + condition: I.FilterCondition.LessOrEqual, + value: end, + quickOption: I.FilterQuickOption.ExactDate, + format: I.RelationType.Date, + } + ]; + }; + + getDotMap = (start: number, end: number, callBack: (res: Map<string, boolean>) => void): void => { + const { relationKey } = this.state; + const res = new Map(); + + if (!relationKey) { + callBack(res); + return; + }; + + U.Data.search({ + filters: this.getFilters(start, end), + keys: [ relationKey ], + }, (message: any) => { + eachDayOfInterval({ + start: fromUnixTime(start), + end: fromUnixTime(end) + }).forEach(date => { + if (message.records.find(rec => isEqual(date, fromUnixTime(rec[relationKey]).setHours(0, 0, 0, 0)))) { + res.set(format(date, 'dd-MM-yyyy'), true); + }; + }); + + callBack(res); + }); }; }); diff --git a/src/ts/component/page/main/edit.tsx b/src/ts/component/page/main/edit.tsx index a44aa8c4a6..780c041260 100644 --- a/src/ts/component/page/main/edit.tsx +++ b/src/ts/component/page/main/edit.tsx @@ -1,48 +1,19 @@ -import * as React from 'react'; +import React, { forwardRef, useRef } from 'react'; +import { observer } from 'mobx-react'; import { Header, Footer, EditorPage } from 'Component'; import { I, S, U, Onboarding, analytics } from 'Lib'; -class PageMainEdit extends React.Component<I.PageComponent> { - - refHeader: any = null; +const PageMainEdit = observer(forwardRef<I.PageRef, I.PageComponent>((props, ref) => { - constructor (props: I.PageComponent) { - super(props); - - this.onOpen = this.onOpen.bind(this); - }; - - render () { - const { isPopup } = this.props; - const rootId = this.getRootId(); - - return ( - <React.Fragment> - <Header - component="mainObject" - ref={ref => this.refHeader = ref} - {...this.props} - rootId={rootId} - /> - - <div id="bodyWrapper" className="wrapper"> - <EditorPage key="editorPage" {...this.props} isPopup={isPopup} rootId={rootId} onOpen={this.onOpen} /> - </div> - - <Footer component="mainObject" {...this.props} /> - </React.Fragment> - ); - }; + const { isPopup } = props; + const headerRef = useRef(null); - onOpen () { - const { isPopup } = this.props; - const rootId = this.getRootId(); + const onOpen = () => { + const rootId = getRootId(); const home = U.Space.getDashboard(); const object = S.Detail.get(rootId, rootId, [ 'type' ], true); - if (this.refHeader) { - this.refHeader.forceUpdate(); - }; + headerRef.current?.forceUpdate(); if (home && (rootId != home.id)) { let key = ''; @@ -60,11 +31,38 @@ class PageMainEdit extends React.Component<I.PageComponent> { analytics.event('ScreenObject', { objectType: object.type }); }; - getRootId () { - const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + const getRootId = () => { + const { rootId, match } = props; + return rootId ? rootId : match?.params?.id; }; - -}; -export default PageMainEdit; + const rootId = getRootId(); + + return ( + <> + <Header + component="mainObject" + ref={headerRef} + {...props} + rootId={rootId} + /> + + <div id="bodyWrapper" className="wrapper"> + <EditorPage + key="editorPage" {...props} + isPopup={isPopup} + rootId={rootId} + onOpen={onOpen} + /> + </div> + + <Footer + component="mainObject" + {...props} + /> + </> + ); + +})); + +export default PageMainEdit; \ No newline at end of file diff --git a/src/ts/component/page/main/empty.tsx b/src/ts/component/page/main/empty.tsx index cef68671f1..7ec832f3f8 100644 --- a/src/ts/component/page/main/empty.tsx +++ b/src/ts/component/page/main/empty.tsx @@ -1,66 +1,53 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; import { ObjectName, Label, IconObject, Header, Footer, Icon } from 'Component'; import { I, U, translate } from 'Lib'; -const PageMainEmpty = observer(class PageMainEmpty extends React.Component<I.PageComponent> { +const PageMainEmpty = observer(forwardRef<{}, I.PageComponent>((props, ref) => { - node = null; + const space = U.Space.getSpaceview(); + const home = U.Space.getDashboard(); - constructor (props: I.PageComponent) { - super(props); - - this.onDashboard = this.onDashboard.bind(this); + const onDashboard = () => { + U.Menu.dashboardSelect('.pageMainEmpty #empty-dashboard-select', true); }; - - render () { - const space = U.Space.getSpaceview(); - const home = U.Space.getDashboard(); - - return ( - <div - ref={node => this.node = node} - className="wrapper" - > - <Header - {...this.props} - component="mainEmpty" - text={translate('commonSearch')} - layout={I.ObjectLayout.SpaceView} - /> - <div className="wrapper"> - <IconObject object={space} size={96} /> - <ObjectName className="title" object={space} /> - <Label text={translate('pageMainEmptyDescription')} /> - - <div className="row"> - <div className="side left"> - <Label text={translate('commonHomepage')} /> - </div> + return ( + <> + <Header + {...props} + component="mainEmpty" + text={translate('commonSearch')} + layout={I.ObjectLayout.SpaceView} + /> + + <div className="wrapper"> + <IconObject object={space} size={96} /> + <ObjectName className="title" object={space} /> + <Label text={translate('pageMainEmptyDescription')} /> + + <div className="row"> + <div className="side left"> + <Label text={translate('commonHomepage')} /> + </div> - <div className="side right"> - <div id="empty-dashboard-select" className="select" onClick={this.onDashboard}> - <div className="item"> - <div className="name"> - {home ? home.name : translate('commonSelect')} - </div> + <div className="side right"> + <div id="empty-dashboard-select" className="select" onClick={onDashboard}> + <div className="item"> + <div className="name"> + {home ? home.name : translate('commonSelect')} </div> - <Icon className="arrow light" /> </div> + <Icon className="arrow light" /> </div> </div> </div> - - <Footer component="mainObject" /> </div> - ); - }; - - onDashboard () { - U.Menu.dashboardSelect('.pageMainEmpty #empty-dashboard-select', true); - }; -}); + <Footer component="mainObject" /> + </> + ); + +})); -export default PageMainEmpty; +export default PageMainEmpty; \ No newline at end of file diff --git a/src/ts/component/page/main/graph.tsx b/src/ts/component/page/main/graph.tsx index dceb84af2c..17fef7dd9b 100644 --- a/src/ts/component/page/main/graph.tsx +++ b/src/ts/component/page/main/graph.tsx @@ -1,136 +1,58 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect, useState } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { I, C, S, U, J, keyboard } from 'Lib'; import { Header, Footer, GraphProvider, Loader } from 'Component'; -const PageMainGraph = observer(class PageMainGraph extends React.Component<I.PageComponent> { +const PageMainGraph = observer(forwardRef<I.PageRef, I.PageComponent>((props, ref) => { - _isMounted = false; - node: any = null; - data: any = { - nodes: [], - edges: [], - }; - ids: string[] = []; - refHeader: any = null; - refGraph: any = null; - loading = false; - timeoutLoading = 0; - rootId = ''; - - constructor (props: I.PageComponent) { - super(props); - - this.onTab = this.onTab.bind(this); - }; + const { isPopup } = props; + const [ data, setData ] = useState({ edges: [], nodes: [] }); + const nodeRef = useRef(null); + const headerRef = useRef(null); + const graphRef = useRef(null); + const rootIdRef = useRef(''); - render () { - const rootId = this.getRootId(); - - return ( - <div - ref={node => this.node = node} - className="body" - > - <Header - {...this.props} - ref={ref => this.refHeader = ref} - component="mainGraph" - rootId={rootId} - tabs={U.Menu.getGraphTabs()} - tab="graph" - onTab={this.onTab} - layout={I.ObjectLayout.Graph} - /> - - <Loader id="loader" /> - - <div className="wrapper"> - <GraphProvider - key="graph" - {...this.props} - ref={ref => this.refGraph = ref} - id="global" - rootId={rootId} - data={this.data} - storageKey={J.Constant.graphId.global} - /> - </div> - - <Footer component="mainObject" /> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - - this.rebind(); - this.resize(); - this.load(); - this.initRootId(this.getRootId()); - }; - - componentDidUpdate () { - this.resize(); - - if (this.loading) { - window.clearTimeout(this.timeoutLoading); - this.timeoutLoading = window.setTimeout(() => this.setLoading(false), 100); - }; - }; - - componentWillUnmount () { - this._isMounted = false; - - this.unbind(); - window.clearTimeout(this.timeoutLoading); - }; - - unbind () { + const unbind = () => { $(window).off(`keydown.graphPage updateGraphRoot.graphPage removeGraphNode.graphPage sidebarResize.graphPage`); }; - rebind () { + const rebind = () => { const win = $(window); - this.unbind(); - win.on(`keydown.graphPage`, e => this.onKeyDown(e)); - win.on('updateGraphRoot.graphPage', (e: any, data: any) => this.initRootId(data.id)); - win.on('sidebarResize.graphPage', () => this.resize()); + unbind(); + win.on(`keydown.graphPage`, e => onKeyDown(e)); + win.on('updateGraphRoot.graphPage', (e: any, data: any) => initRootId(data.id)); + win.on('sidebarResize.graphPage', () => resize()); }; - onKeyDown (e: any) { + const onKeyDown = (e: any) => { const cmd = keyboard.cmdKey(); keyboard.shortcut(`${cmd}+f`, e, () => $('#button-header-search').trigger('click')); }; - load () { - this.setLoading(true); + const load = () => { + setLoading(true); C.ObjectGraph(S.Common.space, U.Data.graphFilters(), 0, [], J.Relation.graph, '', [], (message: any) => { - if (!this._isMounted || message.error.code) { + if (message.error.code) { return; }; - this.data.edges = message.edges; - this.data.nodes = message.nodes; - this.forceUpdate(); + setData({ + edges: message.edges, + nodes: message.nodes.map(it => S.Detail.mapper(it)) + }); - if (this.refGraph) { - this.refGraph.init(); - }; + graphRef.current?.init(); }); }; - setLoading (v: boolean) { - const node = $(this.node); + const setLoading = (v: boolean) => { + const node = $(nodeRef.current); const loader = node.find('#loader'); - this.loading = v; - if (v) { loader.show().css({ opacity: 1 }); } else { @@ -139,11 +61,10 @@ const PageMainGraph = observer(class PageMainGraph extends React.Component<I.Pag }; }; - resize () { - const { isPopup } = this.props; + const resize = () => { const win = $(window); const obj = U.Common.getPageContainer(isPopup); - const node = $(this.node); + const node = $(nodeRef.current); const wrapper = obj.find('.wrapper'); const oh = obj.height(); const header = node.find('#header'); @@ -159,29 +80,75 @@ const PageMainGraph = observer(class PageMainGraph extends React.Component<I.Pag }; }; - if (this.refGraph) { - this.refGraph.resize(); - }; + graphRef.current?.resize(); }; - initRootId (id: string) { - this.rootId = id; - this.refHeader.refChild.setRootId(id); + const initRootId = (id: string) => { + rootIdRef.current = id; }; - getRootId () { - const { rootId, match } = this.props; - return this.rootId || (rootId ? rootId : match.params.id); + const getRootId = () => { + const { rootId, match } = props; + return rootIdRef.current || (rootId ? rootId : match?.params?.id); }; - onTab (id: string) { + const rootId = getRootId(); + + const onTab = (id: string) => { const tab = U.Menu.getGraphTabs().find(it => it.id == id); if (tab) { - U.Object.openAuto({ id: this.getRootId(), layout: tab.layout }); + U.Object.openAuto({ id: getRootId(), layout: tab.layout }); }; }; -}); + useEffect(() => { + rebind(); + load(); + initRootId(getRootId()); + + return () => unbind(); + }, []); + + useEffect(() => { + resize(); + setLoading(false); + }, [ data ]); + + return ( + <div + ref={nodeRef} + className="body" + > + <Header + {...props} + ref={headerRef} + component="mainGraph" + rootId={rootId} + tabs={U.Menu.getGraphTabs()} + tab="graph" + onTab={onTab} + layout={I.ObjectLayout.Graph} + /> + + <Loader id="loader" /> + + <div className="wrapper"> + <GraphProvider + key="graph" + {...props} + ref={graphRef} + id="global" + rootId={rootId} + data={data} + storageKey={J.Constant.graphId.global} + /> + </div> + + <Footer component="mainObject" /> + </div> + ); + +})); -export default PageMainGraph; +export default PageMainGraph; \ No newline at end of file diff --git a/src/ts/component/page/main/history.tsx b/src/ts/component/page/main/history.tsx index 18d705cf5d..a50a39503f 100644 --- a/src/ts/component/page/main/history.tsx +++ b/src/ts/component/page/main/history.tsx @@ -448,7 +448,7 @@ const PageMainHistory = observer(class PageMainHistory extends React.Component<I getRootId () { const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + return rootId ? rootId : match?.params?.id; }; isSetOrCollection (): boolean { @@ -460,7 +460,7 @@ const PageMainHistory = observer(class PageMainHistory extends React.Component<I setVersion (version: I.HistoryVersion) { this.refSideLeft?.forceUpdate(); - this.refSideLeft?.refHeader?.refChild.setVersion(version); + this.refSideLeft?.refHeader?.setVersion(version); this.refSideLeft?.refHead?.forceUpdate(); $(window).trigger('updateDataviewData'); diff --git a/src/ts/component/page/main/history/left.tsx b/src/ts/component/page/main/history/left.tsx index cc047ca222..978dd23e44 100644 --- a/src/ts/component/page/main/history/left.tsx +++ b/src/ts/component/page/main/history/left.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Header, Block } from 'Component'; +import { Header, Block, HeadSimple } from 'Component'; import { I, M, S, U, translate } from 'Lib'; -import HeadSimple from 'Component/page/elements/head/simple'; interface Props extends I.PageComponent { rootId: string; @@ -34,8 +33,6 @@ const HistoryLeft = observer(class HistoryLeft extends React.Component<Props> { const cn = [ 'editorWrapper', check.className ]; const isSet = U.Object.isSetLayout(object.layout); const isCollection = U.Object.isCollectionLayout(object.layout); - const isHuman = U.Object.isHumanLayout(object.layout); - const isParticipant = U.Object.isParticipantLayout(object.layout); let head = null; let children = S.Block.getChildren(rootId, rootId); @@ -56,7 +53,7 @@ const HistoryLeft = observer(class HistoryLeft extends React.Component<Props> { children = children.filter(it => it.isDataview()); check.withIcon = false; } else - if (isHuman || isParticipant) { + if (U.Object.isInHumanLayouts(object.layout)) { icon.type = I.BlockType.IconUser; }; diff --git a/src/ts/component/page/main/import.tsx b/src/ts/component/page/main/import.tsx index 48c9479699..1845cf923e 100644 --- a/src/ts/component/page/main/import.tsx +++ b/src/ts/component/page/main/import.tsx @@ -1,51 +1,28 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useEffect, useRef } from 'react'; +import $ from 'jquery'; import { Loader, Title, Error, Frame, Button } from 'Component'; import { I, C, S, U, translate, analytics } from 'Lib'; -interface State { - error: string; -}; +const PageMainImport = forwardRef<{}, I.PageComponent>((props, ref) => { -class PageMainImport extends React.Component<I.PageComponent, State> { + const nodeRef = useRef(null); + const { isPopup } = props; + const [ error, setError ] = useState(''); - state = { - error: '', - }; - node = null; - - render () { - const { error } = this.state; - - return ( - <div - ref={ref => this.node = ref} - className="wrapper" - > - <Frame> - <Title text={error ? translate('commonError') : translate('pageMainImportTitle')} /> - <Error text={error} /> + const resize = () => { + const win = $(window); + const obj = U.Common.getPageContainer(isPopup); + const wh = isPopup ? obj.height() : win.height(); - {error ? ( - <div className="buttons"> - <Button - text={translate('commonBack')} - color="blank" - className="c36" - onClick={() => U.Space.openDashboard('route')} - /> - </div> - ) : <Loader />} - </Frame> - </div> - ); + $(nodeRef.current).css({ height: wh }); }; - componentDidMount (): void { - const search = this.getSearch(); + useEffect(() => { + const search = U.Common.searchParam(U.Router.history.location.search); C.GalleryDownloadManifest(search.source, (message: any) => { if (message.error.code) { - this.setState({ error: message.error.description }); + setError(message.error.description); } else { U.Space.openDashboard('route'); @@ -60,32 +37,30 @@ class PageMainImport extends React.Component<I.PageComponent, State> { }, S.Popup.getTimeout()); }; }); - - this.resize(); - }; - - componentDidUpdate (): void { - this.resize(); - }; - - getSearch () { - return U.Common.searchParam(U.Router.history.location.search); - }; - - resize () { - const { isPopup } = this.props; - const win = $(window); - const obj = U.Common.getPageContainer(isPopup); - const node = $(this.node); - const wrapper = obj.find('.wrapper'); - const oh = obj.height(); - const header = node.find('#header'); - const hh = header.height(); - const wh = isPopup ? oh - hh : win.height(); - - wrapper.css({ height: wh, paddingTop: isPopup ? 0 : hh }); - }; - -}; + }, []); + + useEffect(() => resize()); + + return ( + <div ref={nodeRef} className="wrapper" > + <Frame> + <Title text={error ? translate('commonError') : translate('pageMainImportTitle')} /> + <Error text={error} /> + + {error ? ( + <div className="buttons"> + <Button + text={translate('commonBack')} + color="blank" + className="c36" + onClick={() => U.Space.openDashboard('route')} + /> + </div> + ) : <Loader />} + </Frame> + </div> + ); + +}); export default PageMainImport; \ No newline at end of file diff --git a/src/ts/component/page/main/invite.tsx b/src/ts/component/page/main/invite.tsx index 44961596b9..f044b5080e 100644 --- a/src/ts/component/page/main/invite.tsx +++ b/src/ts/component/page/main/invite.tsx @@ -1,72 +1,32 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useImperativeHandle, useEffect } from 'react'; import { Loader, Title, Error, Frame, Button, Footer } from 'Component'; import { I, C, S, U, translate } from 'Lib'; -interface State { - error: string; +interface PageMainInviteRefProps { + resize: () => void; }; -class PageMainInvite extends React.Component<I.PageComponent, State> { +const PageMainInvite = forwardRef<PageMainInviteRefProps, I.PageComponent>((props, ref) => { - state = { - error: '', - }; - cid = ''; - key = ''; - node = null; - refFrame = null; - - render () { - const { error } = this.state; - - return ( - <div - ref={ref => this.node = ref} - className="wrapper" - > - <Frame ref={ref => this.refFrame = ref}> - <Title text={error ? translate('commonError') : translate('pageMainInviteTitle')} /> - <Error text={error} /> - - {error ? ( - <div className="buttons"> - <Button - text={translate('commonBack')} - color="blank" - className="c36" - onClick={() => U.Space.openDashboard('route')} - /> - </div> - ) : <Loader />} - </Frame> - - <Footer component="mainObject" {...this.props} /> - </div> - ); - }; - - componentDidMount (): void { - this.init(); - this.resize(); - }; + const { isPopup } = props; + const nodeRef = useRef(null); + const frameRef = useRef(null); + const cid = useRef(''); + const key = useRef(''); + const [ error, setError ] = useState(''); - componentDidUpdate (): void { - this.init(); - this.resize(); - }; - - init () { + const init = () => { const data = U.Common.searchParam(U.Router.history.location.search); - if ((this.cid == data.cid) && (this.key == data.key)) { + if ((cid.current == data.cid) && (key.current == data.key)) { return; }; - this.cid = data.cid; - this.key = data.key; + cid.current = data.cid; + key.current = data.key; if (!data.cid || !data.key) { - this.setState({ error: translate('pageMainInviteErrorData') }); + setError(translate('pageMainInviteErrorData')); } else { C.SpaceInviteView(data.cid, data.key, (message: any) => { U.Space.openDashboard('route', { replace: true }); @@ -112,20 +72,49 @@ class PageMainInvite extends React.Component<I.PageComponent, State> { }; }; - resize () { - const { isPopup } = this.props; + const resize = () => { const win = $(window); const obj = U.Common.getPageContainer(isPopup); - const node = $(this.node); + const node = $(nodeRef.current); const oh = obj.height(); - const header = node.find('#header'); - const hh = header.height(); - const wh = isPopup ? oh - hh : win.height(); + const wh = isPopup ? oh : win.height(); - node.css({ height: wh, paddingTop: isPopup ? 0 : hh }); - this.refFrame.resize(); + node.css({ height: wh }); + frameRef.current?.resize(); }; -}; + useEffect(() => { + init(); + resize(); + }); + + useImperativeHandle(ref, () => ({ resize })); + + return ( + <div + ref={nodeRef} + className="wrapper" + > + <Frame ref={frameRef}> + <Title text={error ? translate('commonError') : translate('pageMainInviteTitle')} /> + <Error text={error} /> + + {error ? ( + <div className="buttons"> + <Button + text={translate('commonBack')} + color="blank" + className="c36" + onClick={() => U.Space.openDashboard('route')} + /> + </div> + ) : <Loader />} + </Frame> + + <Footer component="mainObject" {...props} /> + </div> + ); + +}); export default PageMainInvite; \ No newline at end of file diff --git a/src/ts/component/page/main/media.tsx b/src/ts/component/page/main/media.tsx index cb5965197e..ef83e42db2 100644 --- a/src/ts/component/page/main/media.tsx +++ b/src/ts/component/page/main/media.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; -import { Header, Footer, Loader, Block, Button, Icon, IconObject, Deleted } from 'Component'; +import { Header, Footer, Loader, Block, Button, Icon, IconObject, Deleted, HeadSimple } from 'Component'; import { I, C, S, M, U, Action, translate, Relation, analytics } from 'Lib'; -import HeadSimple from 'Component/page/elements/head/simple'; interface State { isLoading: boolean; @@ -238,11 +237,7 @@ const PageMainMedia = observer(class PageMainMedia extends React.Component<I.Pag }; const { isPopup, match } = this.props; - - let close = true; - if (isPopup && (match.params.id == this.id)) { - close = false; - }; + const close = !(isPopup && (match?.params?.id == this.id)); if (close) { Action.pageClose(this.id, true); @@ -322,7 +317,7 @@ const PageMainMedia = observer(class PageMainMedia extends React.Component<I.Pag getRootId () { const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + return rootId ? rootId : match?.params?.id; }; resize () { diff --git a/src/ts/component/page/main/membership.tsx b/src/ts/component/page/main/membership.tsx index d881b5efea..554179b07c 100644 --- a/src/ts/component/page/main/membership.tsx +++ b/src/ts/component/page/main/membership.tsx @@ -1,57 +1,24 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useImperativeHandle, useEffect } from 'react'; +import { observer } from 'mobx-react'; import { Loader, Frame, Title, Error, Button } from 'Component'; import { I, S, U, J, translate, analytics } from 'Lib'; -interface State { - error: string; -}; +const PageMainMembership = observer(forwardRef<I.PageRef, I.PageComponent>((props, ref) => { -class PageMainMembership extends React.Component<I.PageComponent, State> { + const nodeRef = useRef(null); + const { isPopup } = props; + const [ error, setError ] = useState(''); + const { membership } = S.Auth; + const { status, tier } = membership; - state = { - error: '', - }; - node = null; - - render () { - const { error } = this.state; - - return ( - <div - ref={ref => this.node = ref} - className="wrapper" - > - <Frame> - <Title text={error ? translate('commonError') : translate('pageMainMembershipTitle')} /> - <Error text={error} /> - - {error ? ( - <div className="buttons"> - <Button - text={translate('commonBack')} - color="blank" - className="c36" - onClick={() => U.Space.openDashboard('route')} - /> - </div> - ) : <Loader />} - </Frame> - </div> - ); - }; - - componentDidMount (): void { - this.init(); - }; - - init () { + const init = () => { const data = U.Common.searchParam(U.Router.history.location.search); let { tier } = data; U.Data.getMembershipStatus((membership: I.Membership) => { if (!membership || membership.isNone) { - this.setState({ error: translate('pageMainMembershipError') }); + setError(translate('pageMainMembershipError')); return; }; @@ -66,19 +33,16 @@ class PageMainMembership extends React.Component<I.PageComponent, State> { S.Popup.open('membership', { data: { tier } }); } else { - this.finalise(); + finalise(); }; }, }); }); - this.resize(); + resize(); }; - finalise () { - const { membership } = S.Auth; - const { status, tier } = membership; - + const finalise = () => { S.Popup.closeAll(null, () => { if (status == I.MembershipStatus.Finalization) { S.Popup.open('membershipFinalization', { data: { tier } }); @@ -98,20 +62,43 @@ class PageMainMembership extends React.Component<I.PageComponent, State> { }); }; - resize () { - const { isPopup } = this.props; + const resize = () => { const win = $(window); + const node = $(nodeRef.current); const obj = U.Common.getPageContainer(isPopup); - const node = $(this.node); - const wrapper = obj.find('.wrapper'); - const oh = obj.height(); - const header = node.find('#header'); - const hh = header.height(); - const wh = isPopup ? oh - hh : win.height(); - - wrapper.css({ height: wh, paddingTop: isPopup ? 0 : hh }); - }; -}; + node.css({ height: (isPopup ? obj.height() : win.height()) }); + }; -export default PageMainMembership; + useEffect(() => init(), []); + + useImperativeHandle(ref, () => ({ + resize, + })); + + return ( + <div + ref={nodeRef} + className="wrapper" + > + <Frame> + <Title text={error ? translate('commonError') : translate('pageMainMembershipTitle')} /> + <Error text={error} /> + + {error ? ( + <div className="buttons"> + <Button + text={translate('commonBack')} + color="blank" + className="c36" + onClick={() => U.Space.openDashboard('route')} + /> + </div> + ) : <Loader />} + </Frame> + </div> + ); + +})); + +export default PageMainMembership; \ No newline at end of file diff --git a/src/ts/component/page/main/navigation.tsx b/src/ts/component/page/main/navigation.tsx index 13ca397990..7ebee57fb7 100644 --- a/src/ts/component/page/main/navigation.tsx +++ b/src/ts/component/page/main/navigation.tsx @@ -293,6 +293,10 @@ const PageMainNavigation = observer(class PageMainNavigation extends React.Compo }; onKeyDown (e: any) { + if (S.Popup.isOpen('search')) { + return; + }; + const items = this.getItems(); const l = items.length; @@ -399,8 +403,9 @@ const PageMainNavigation = observer(class PageMainNavigation extends React.Compo loadPage (id: string) { const { loading } = this.state; + const skipIds = U.Space.getSystemDashboardIds(); - if (!id || [ I.HomePredefinedId.Graph, I.HomePredefinedId.Last ].includes(id as any)) { + if (!id || skipIds.includes(id as any)) { return; }; @@ -466,7 +471,7 @@ const PageMainNavigation = observer(class PageMainNavigation extends React.Compo getRootId () { const { rootId, match } = this.props; - let root = rootId ? rootId : match.params.id; + let root = rootId ? rootId : match?.params?.id; if (root == I.HomePredefinedId.Graph) { root = U.Space.getLastOpened()?.id; }; diff --git a/src/ts/component/page/main/object.tsx b/src/ts/component/page/main/object.tsx index cfefda4725..20c4161a77 100644 --- a/src/ts/component/page/main/object.tsx +++ b/src/ts/component/page/main/object.tsx @@ -1,15 +1,19 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { I, C, U } from 'Lib'; -class PageMainObject extends React.Component<I.PageComponent> { +const PageMainObject = forwardRef<{}, I.PageComponent>((props, ref) => { - render () { - return <div />; - }; + const { match } = props; - componentDidMount (): void { - const { match } = this.props; - const { id, spaceId } = match.params || {}; + useEffect(() => { + const { id, spaceId, cid, key } = match.params || {}; + const space = U.Space.getSpaceviewBySpaceId(spaceId); + + // Redirect to invite page when invite parameters are present + if ((!space || !space.isAccountActive) && cid && key) { + U.Router.go(`/main/invite/?cid=${cid}&key=${key}`, { replace: true }); + return; + }; C.ObjectShow(id, '', spaceId, (message: any) => { if (message.error.code) { @@ -27,8 +31,11 @@ class PageMainObject extends React.Component<I.PageComponent> { U.Object.openRoute(item.details); }); - }; -}; + }, []); + + return <div />; + +}); export default PageMainObject; \ No newline at end of file diff --git a/src/ts/component/page/main/relation.tsx b/src/ts/component/page/main/relation.tsx index 459d471686..5f89f95266 100644 --- a/src/ts/component/page/main/relation.tsx +++ b/src/ts/component/page/main/relation.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Header, Footer, Loader, ListObject, Deleted } from 'Component'; +import { Header, Footer, Loader, ListObject, Deleted, HeadSimple } from 'Component'; import { I, C, S, U, Action, translate, analytics } from 'Lib'; -import HeadSimple from 'Component/page/elements/head/simple'; interface State { isDeleted: boolean; @@ -163,11 +162,8 @@ const PageMainRelation = observer(class PageMainRelation extends React.Component }; const { isPopup, match } = this.props; - - let close = true; - if (isPopup && (match.params.id == this.id)) { - close = false; - }; + const close = !(isPopup && (match?.params?.id == this.id)); + if (close) { Action.pageClose(this.id, true); }; @@ -175,7 +171,7 @@ const PageMainRelation = observer(class PageMainRelation extends React.Component getRootId () { const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + return rootId ? rootId : match?.params?.id; }; onCreate () { diff --git a/src/ts/component/page/main/set.tsx b/src/ts/component/page/main/set.tsx index 5f4e9e532d..ef02051b5c 100644 --- a/src/ts/component/page/main/set.tsx +++ b/src/ts/component/page/main/set.tsx @@ -2,10 +2,8 @@ import * as React from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; -import { Header, Footer, Loader, Block, Deleted } from 'Component'; +import { Header, Footer, Loader, Block, Deleted, HeadSimple, EditorControls } from 'Component'; import { I, M, C, S, U, J, Action, keyboard, translate, analytics } from 'Lib'; -import Controls from 'Component/page/elements/head/controls'; -import HeadSimple from 'Component/page/elements/head/simple'; interface State { isLoading: boolean; @@ -40,12 +38,10 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom const rootId = this.getRootId(); const check = U.Data.checkDetails(rootId); - if (isDeleted) { - return <Deleted {...this.props} />; - }; - let content = null; - + if (isDeleted) { + content = <Deleted {...this.props} />; + } else if (isLoading) { content = <Loader id="loader" />; } else { @@ -54,19 +50,28 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom const children = S.Block.getChildren(rootId, rootId, it => it.isDataview()); const cover = new M.Block({ id: rootId + '-cover', type: I.BlockType.Cover, childrenIds: [], fields: {}, content: {} }); const placeholder = isCollection ? translate('defaultNameCollection') : translate('defaultNameSet'); + const readonly = this.isReadonly(); content = ( <React.Fragment> - {check.withCover ? <Block {...this.props} key={cover.id} rootId={rootId} block={cover} /> : ''} + {check.withCover ? <Block {...this.props} key={cover.id} rootId={rootId} block={cover} readonly={readonly} /> : ''} <div className="blocks wrapper"> - <Controls ref={ref => this.refControls = ref} key="editorControls" {...this.props} rootId={rootId} resize={this.resize} /> + <EditorControls + ref={ref => this.refControls = ref} + key="editorControls" + {...this.props} + rootId={rootId} + resize={this.resize} + readonly={readonly} + /> + <HeadSimple {...this.props} ref={ref => this.refHead = ref} placeholder={placeholder} rootId={rootId} - readonly={this.isReadonly()} + readonly={readonly} /> {children.map((block: I.Block, i: number) => ( @@ -79,7 +84,7 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom block={block} className="noPlus" isSelectionDisabled={true} - readonly={this.isReadonly()} + readonly={readonly} /> ))} </div> @@ -195,11 +200,8 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom }; const { isPopup, match } = this.props; - - let close = true; - if (isPopup && (match.params.id == this.id)) { - close = false; - }; + const close = !(isPopup && (match?.params?.id == this.id)); + if (close) { Action.pageClose(this.id, true); }; @@ -207,7 +209,7 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom getRootId () { const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + return rootId ? rootId : match?.params?.id; }; onScroll () { @@ -275,6 +277,11 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom return true; }; + const object = S.Detail.get(rootId, rootId, [ 'isArchived' ], true); + if (object.isArchived) { + return true; + }; + return !U.Space.canMyParticipantWrite(); }; diff --git a/src/ts/component/page/main/type.tsx b/src/ts/component/page/main/type.tsx index 3bdcf6713d..c9ce590d89 100644 --- a/src/ts/component/page/main/type.tsx +++ b/src/ts/component/page/main/type.tsx @@ -1,10 +1,8 @@ import * as React from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; -import { Icon, Header, Footer, Loader, ListObjectPreview, ListObject, Select, Deleted } from 'Component'; +import { Icon, Header, Footer, Loader, ListPreviewObject, ListObject, Select, Deleted, HeadSimple, EditorControls } from 'Component'; import { I, C, S, U, J, focus, Action, analytics, Relation, translate } from 'Lib'; -import Controls from 'Component/page/elements/head/controls'; -import HeadSimple from 'Component/page/elements/head/simple'; interface State { isLoading: boolean; @@ -18,7 +16,6 @@ const PageMainType = observer(class PageMainType extends React.Component<I.PageC refHeader: any = null; refHead: any = null; refControls: any = null; - refListPreview: any = null; timeout = 0; page = 0; @@ -131,7 +128,7 @@ const PageMainType = observer(class PageMainType extends React.Component<I.PageC {isLoading ? <Loader id="loader" /> : ''} <div className={[ 'blocks', 'wrapper', check.className ].join(' ')}> - <Controls ref={ref => this.refControls = ref} key="editorControls" {...this.props} rootId={rootId} resize={() => {}} /> + <EditorControls ref={ref => this.refControls = ref} key="editorControls" {...this.props} rootId={rootId} resize={() => {}} /> <HeadSimple {...this.props} ref={ref => this.refHead = ref} @@ -154,9 +151,8 @@ const PageMainType = observer(class PageMainType extends React.Component<I.PageC {totalTemplate ? ( <div className="content"> - <ListObjectPreview + <ListPreviewObject key="listTemplate" - ref={ref => this.refListPreview = ref} getItems={() => S.Record.getRecords(subIdTemplate, [])} canAdd={allowedTemplate} onAdd={this.onTemplateAdd} @@ -298,11 +294,7 @@ const PageMainType = observer(class PageMainType extends React.Component<I.PageC }; const { isPopup, match } = this.props; - - let close = true; - if (isPopup && (match.params.id == this.id)) { - close = false; - }; + const close = !(isPopup && (match?.params?.id == this.id)); if (close) { Action.pageClose(this.id, true); @@ -557,7 +549,7 @@ const PageMainType = observer(class PageMainType extends React.Component<I.PageC getRootId () { const { rootId, match } = this.props; - return rootId ? rootId : match.params.id; + return rootId ? rootId : match?.params?.id; }; getSpaceId () { diff --git a/src/ts/component/page/main/void.tsx b/src/ts/component/page/main/void.tsx index 48ea019980..ab3cb7010a 100644 --- a/src/ts/component/page/main/void.tsx +++ b/src/ts/component/page/main/void.tsx @@ -1,34 +1,22 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; +import React, { forwardRef } from 'react'; import { Icon, Title, Label, Button } from 'Component'; import { I, translate, Action, analytics } from 'Lib'; -const PageMainVoid = observer(class PageMainVoid extends React.Component<I.PageComponent> { +const PageMainVoid = forwardRef<{}, I.PageComponent>(() => { - node = null; - - render () { - return ( - <div - ref={node => this.node = node} - className="wrapper" - > - <div className="container"> - <div className="iconWrapper"> - <Icon /> - </div> - <Title text={translate('pageMainVoidTitle')} /> - <Label text={translate('pageMainVoidText')} /> - <Button onClick={this.onClick} className="c36" text={translate('pageMainVoidCreateSpace')} /> + return ( + <div className="wrapper"> + <div className="container"> + <div className="iconWrapper"> + <Icon /> </div> + <Title text={translate('pageMainVoidTitle')} /> + <Label text={translate('pageMainVoidText')} /> + <Button onClick={() => Action.createSpace(analytics.route.void)} className="c36" text={translate('pageMainVoidCreateSpace')} /> </div> - ); - }; + </div> + ); - onClick () { - Action.createSpace(analytics.route.void); - }; - }); -export default PageMainVoid; +export default PageMainVoid; \ No newline at end of file diff --git a/src/ts/component/popup/about.tsx b/src/ts/component/popup/about.tsx index 0914dbf365..ed0bf019ae 100644 --- a/src/ts/component/popup/about.tsx +++ b/src/ts/component/popup/about.tsx @@ -1,40 +1,31 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { Title, Icon, Label, Button } from 'Component'; import { I, U, translate } from 'Lib'; -class PopupAbout extends React.Component<I.Popup> { - - constructor (props: I.Popup) { - super(props); - - this.onVersionCopy = this.onVersionCopy.bind(this); - }; - - render () { - return ( - <React.Fragment> - <div className="iconWrapper"> - <Icon /> - </div> - <Title text={translate('popupAboutTitle')} /> - <Label text={translate('popupAboutDescription')} /> - - <div className="version"> - {U.Common.sprintf(translate('popupAboutVersion'), this.getVersion())} - <Button onClick={this.onVersionCopy} text={translate('commonCopy')} className="c28" color="blank" /> - </div> - <div className="copyright">{translate('popupAboutCopyright')}</div> - </React.Fragment> - ); - }; - - getVersion () { - return U.Common.getElectron().version.app; - }; - - onVersionCopy () { - U.Common.copyToast(translate('commonVersion'), this.getVersion()); - }; +const PopupAbout: FC<I.Popup> = () => { + + const version = U.Common.getElectron().version.app; + + return ( + <> + <div className="iconWrapper"> + <Icon /> + </div> + <Title text={translate('popupAboutTitle')} /> + <Label text={translate('popupAboutDescription')} /> + + <div className="version"> + {U.Common.sprintf(translate('popupAboutVersion'), version)} + <Button + onClick={() => U.Common.copyToast(translate('commonVersion'), version)} + text={translate('commonCopy')} + className="c28" + color="blank" + /> + </div> + <div className="copyright">{translate('popupAboutCopyright')}</div> + </> + ); }; diff --git a/src/ts/component/popup/confirm.tsx b/src/ts/component/popup/confirm.tsx index ea08fa491e..848529045e 100644 --- a/src/ts/component/popup/confirm.tsx +++ b/src/ts/component/popup/confirm.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Title, Icon, Label, Button, Checkbox } from 'Component'; +import { Title, Icon, Label, Button, Checkbox, Error } from 'Component'; import { I, keyboard, translate, Storage } from 'Lib'; import { observer } from 'mobx-react'; @@ -22,7 +22,7 @@ const PopupConfirm = observer(class PopupConfirm extends React.Component<I.Popup render() { const { param } = this.props; const { data } = param; - const { title, text, icon, storageKey } = data; + const { title, text, icon, storageKey, error } = data; const canConfirm = undefined === data.canConfirm ? true : data.canConfirm; const canCancel = undefined === data.canCancel ? true : data.canCancel; @@ -53,6 +53,8 @@ const PopupConfirm = observer(class PopupConfirm extends React.Component<I.Popup {canConfirm ? <Button text={textConfirm} color={colorConfirm} className="c36" onClick={this.onConfirm} onMouseEnter={this.onMouseEnter} /> : ''} {canCancel ? <Button text={textCancel} color={colorCancel} className="c36" onClick={this.onCancel} onMouseEnter={this.onMouseEnter} /> : ''} </div> + + <Error text={error} /> </div> ); }; @@ -115,12 +117,14 @@ const PopupConfirm = observer(class PopupConfirm extends React.Component<I.Popup }; onConfirm (e: any) { - const { param } = this.props; + const { param, close } = this.props; const { data } = param; - const { onConfirm } = data; + const { onConfirm, noCloseOnConfirm } = data; e.preventDefault(); - this.props.close(); + if (!noCloseOnConfirm) { + close(); + }; if (onConfirm) { onConfirm(); diff --git a/src/ts/component/popup/help.tsx b/src/ts/component/popup/help.tsx index dbbc74a14b..21d2c7c70c 100644 --- a/src/ts/component/popup/help.tsx +++ b/src/ts/component/popup/help.tsx @@ -1,137 +1,42 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useRef, useEffect } from 'react'; import $ from 'jquery'; -import raf from 'raf'; import * as Docs from 'Docs'; import { Label, Icon, Cover, Button } from 'Component'; import { I, U, J, translate, Action } from 'Lib'; import Block from 'Component/block/help'; -interface State { - page: number; -}; - const LIMIT = 1; -class PopupHelp extends React.Component<I.Popup, State> { - - _isMounted = false; - node: any = null; - state = { - page: 0, +const PopupHelp = forwardRef<{}, I.Popup>((props, ref) => { + + const { getId, param, position } = props; + const { data } = param; + const [ page, setPage ] = useState(0); + const nodeRef = useRef(null); + const document = U.Common.toUpperCamelCase(data.document); + const blocks = Docs.Help[document] || []; + const title = blocks.find(it => it.style == I.TextStyle.Title); + const cover = blocks.find(it => it.type == I.BlockType.Cover); + const isWhatsNew = document == 'WhatsNew'; + const cn = [ 'editor', 'help' ]; + + if (cover) { + cn.push('withCover'); }; - - render () { - const { page } = this.state; - const document = this.getDocument(); - const blocks = this.getBlocks(); - const title = blocks.find(it => it.style == I.TextStyle.Title); - const cover = blocks.find(it => it.type == I.BlockType.Cover); - const isWhatsNew = document == 'WhatsNew'; - - const Section = (item: any) => ( - <div className="section"> - {item.children.map((child: any, i: number) => ( - <Block key={i} {...this.props} {...child} /> - ))} - </div> - ); - - let sections = this.getSections(); - - const length = sections.length; - - if (isWhatsNew) { - sections = sections.slice(page, page + LIMIT); - }; - return ( - <div - ref={node => this.node = node} - className="wrapper" - > - <div className="head"> - <div className="side left"> - {title ? <Label text={title.text} /> : ''} - </div> - <div className="side right"> - <Label text={translate('popupHelpLabel')} /> - <Icon onClick={() => Action.openUrl(J.Url.mail)} className="mail" /> - </div> - </div> - - <div className={[ 'editor', 'help', (cover ? 'withCover' : '') ].join(' ')}> - {cover ? <Cover {...cover.param} /> : ''} - - <div className="blocks"> - {sections.map((section: any, i: number) => ( - <Section key={i} {...this.props} {...section} /> - ))} - </div> - - {isWhatsNew ? ( - <div className="buttons"> - {page < length - 1 ? <Button className="c28" text={translate('popupHelpPrevious')} onClick={() => this.onArrow(1)} /> : ''} - {page > 0 ? <Button className="c28" text={translate('popupHelpNext')} onClick={() => this.onArrow(-1)} /> : ''} - </div> - ) : ''} - </div> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - this.rebind(); - this.resize(); - - U.Common.renderLinks($(this.node)); - }; - - componentDidUpdate () { - this.resize(); - - U.Common.renderLinks($(this.node)); - }; - - componentWillUnmount () { - this._isMounted = false; - this.unbind(); - }; - - rebind () { - this.unbind(); - $(window).off('resize.popupHelp').on('resize.popupHelp', () => this.resize()); - }; - - unbind () { - $(window).off('resize.help'); - }; - - getDocument () { - const { param } = this.props; - const { data } = param; - - return U.Common.toUpperCamelCase(data.document); - }; - - getBlocks () { - return Docs.Help[this.getDocument()] || []; - }; - - getSections (): any[] { - const document = this.getDocument(); - const blocks = this.getBlocks().filter(it => it.type != I.BlockType.Cover); + const getSections = (): any[] => { + const list = blocks.filter(it => it.type != I.BlockType.Cover); const sections: any[] = []; switch (document) { default: { - sections.push({ children: blocks }); + sections.push({ children: list }); break; }; case 'WhatsNew': { let section = { children: [], header: null }; - for (const block of blocks) { + for (const block of list) { if (!section.header && [ I.TextStyle.Title, I.TextStyle.Header1, I.TextStyle.Header2, I.TextStyle.Header3 ].includes(block.style)) { section.header = block; }; @@ -150,10 +55,15 @@ class PopupHelp extends React.Component<I.Popup, State> { return sections; }; - onArrow (dir: number) { - const { getId } = this.props; - const { page } = this.state; - const length = this.getSections().length; + let sections = getSections(); + + const length = sections.length; + + if (isWhatsNew) { + sections = sections.slice(page, page + LIMIT); + }; + + const onArrow = (dir: number) => { const obj = $(`#${getId()}-innerWrap`); if ((page + dir < 0) || (page + dir >= length)) { @@ -161,25 +71,47 @@ class PopupHelp extends React.Component<I.Popup, State> { }; obj.scrollTop(0); - this.setState({ page: page + dir }); + setPage(page + dir); }; - resize () { - if (!this._isMounted) { - return; - }; - - const { getId, position } = this.props; - const obj = $(`#${getId()}-innerWrap`); - const loader = obj.find('#loader'); - const hh = J.Size.header; + useEffect(() => U.Common.renderLinks($(nodeRef.current))); - loader.css({ width: obj.width(), height: obj.height() }); - position(); + return ( + <div + ref={nodeRef} + className="wrapper" + > + <div className="head"> + <div className="side left"> + {title ? <Label text={title.text} /> : ''} + </div> + <div className="side right"> + <Label text={translate('popupHelpLabel')} /> + <Icon onClick={() => Action.openUrl(J.Url.mail)} className="mail" /> + </div> + </div> + + <div className={cn.join(' ')}> + {cover ? <Cover {...cover.param} /> : ''} + + <div className="blocks"> + {sections.map((section: any, i: number) => ( + <div key={i} className="section"> + {section.children.map((child: any, i: number) => <Block key={i} {...props} {...child} />)} + </div> + ))} + </div> - raf(() => { obj.css({ top: hh + 20, marginTop: 0 }); }); - }; + {isWhatsNew ? ( + <div className="buttons"> + {page < length - 1 ? <Button className="c28" text={translate('popupHelpPrevious')} onClick={() => onArrow(1)} /> : ''} + {page > 0 ? <Button className="c28" text={translate('popupHelpNext')} onClick={() => onArrow(-1)} /> : ''} + </div> + ) : ''} + </div> + </div> + ); -}; +}); -export default PopupHelp; +export default PopupHelp; \ No newline at end of file diff --git a/src/ts/component/popup/index.tsx b/src/ts/component/popup/index.tsx index 9042f6ab17..609ad7cb72 100644 --- a/src/ts/component/popup/index.tsx +++ b/src/ts/component/popup/index.tsx @@ -33,6 +33,7 @@ class Popup extends React.Component<I.Popup> { _isMounted = false; node = null; + ref = null; isAnimating = false; constructor (props: I.Popup) { @@ -102,7 +103,8 @@ class Popup extends React.Component<I.Popup> { <div id={`${popupId}-innerWrap`} className="innerWrap"> <div className="content"> <Component - {...this.props} + {...this.props} + ref={ref => this.ref = ref} position={this.position} close={this.close} storageGet={this.storageGet} @@ -177,6 +179,10 @@ class Popup extends React.Component<I.Popup> { position () { const { id } = this.props; + if (this.ref && this.ref.beforePosition) { + this.ref.beforePosition(); + }; + raf(() => { if (!this._isMounted) { return; diff --git a/src/ts/component/popup/invite/confirm.tsx b/src/ts/component/popup/invite/confirm.tsx index ee0c1ff808..e08f418fe9 100644 --- a/src/ts/component/popup/invite/confirm.tsx +++ b/src/ts/component/popup/invite/confirm.tsx @@ -1,93 +1,20 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useRef, useEffect } from 'react'; import { observer } from 'mobx-react'; import { Title, Button, Error, IconObject, Loader } from 'Component'; import { I, C, S, U, translate, analytics } from 'Lib'; -interface State { - error: string; - isLoading: boolean; -}; +const PopupInviteConfirm = observer(forwardRef<{}, I.Popup>((props, ref) => { -const PopupInviteConfirm = observer(class PopupInviteConfirm extends React.Component<I.Popup, State> { + const [ error, setError ] = useState(''); + const [ isLoading, setIsLoading ] = useState(false); + const { param, close } = props; + const { data } = param; + const { icon, identity, route, spaceId } = data; + const { membership } = S.Auth; + const readerLimt = useRef(0); + const writerLimit = useRef(0); - state = { - error: '', - isLoading: false, - }; - - buttonRefs: Map<string, any> = new Map(); - participants = []; - - constructor (props: I.Popup) { - super(props); - - this.onConfirm = this.onConfirm.bind(this); - this.onReject = this.onReject.bind(this); - this.onMembership = this.onMembership.bind(this); - }; - - render() { - const { error, isLoading } = this.state; - const { param } = this.props; - const { data } = param; - const { icon } = data; - const { membership } = S.Auth; - const space = U.Space.getSpaceviewBySpaceId(this.getSpaceId()); - const name = U.Common.shorten(String(data.name || translate('defaultNamePage')), 32); - - if (!space) { - return null; - }; - - let buttons = []; - if (!this.getReaderLimit() && membership.isExplorer) { - buttons.push({ id: 'reader', text: translate('popupInviteConfirmButtonReaderLimit'), onClick: () => this.onMembership('members') }); - } else - if (!this.getWriterLimit()) { - buttons = buttons.concat([ - { id: 'reader', text: translate('popupInviteConfirmButtonReader'), onClick: () => this.onConfirm(I.ParticipantPermissions.Reader) }, - { id: 'writer', text: translate('popupInviteConfirmButtonWriterLimit'), onClick: () => this.onMembership('editors') }, - ]); - } else { - buttons = buttons.concat([ - { id: 'reader', text: translate('popupInviteConfirmButtonReader'), onClick: () => this.onConfirm(I.ParticipantPermissions.Reader) }, - { id: 'writer', text: translate('popupInviteConfirmButtonWriter'), onClick: () => this.onConfirm(I.ParticipantPermissions.Writer) }, - ]); - }; - - return ( - <React.Fragment> - {isLoading ? <Loader id="loader" /> : ''} - - <div className="iconWrapper"> - <IconObject object={{ name, iconImage: icon, layout: I.ObjectLayout.Participant }} size={48} /> - </div> - - <Title text={U.Common.sprintf(translate('popupInviteConfirmTitle'), name, U.Common.shorten(space.name, 32))} /> - - <div className="buttons"> - <div className="sides"> - {buttons.map((item: any, i: number) => <Button ref={ref => this.buttonRefs.set(item.id, ref)} key={i} {...item} className="c36" />)} - </div> - - <Button onClick={this.onReject} text={translate('popupInviteConfirmButtonReject')} className="c36" color="red" /> - </div> - - <Error text={error} /> - </React.Fragment> - ); - }; - - componentDidMount () { - const { param } = this.props; - const { data } = param; - const { route } = data; - - analytics.event('ScreenInviteConfirm', { route }); - this.load(); - }; - - onMembership (type: string) { + const onMembership = (type: string) => { S.Popup.closeAll(null, () => { S.Popup.open('settings', { data: { page: 'membership' } }); }); @@ -95,49 +22,43 @@ const PopupInviteConfirm = observer(class PopupInviteConfirm extends React.Compo analytics.event('ClickUpgradePlanTooltip', { type, route: analytics.route.inviteConfirm }); }; - onConfirm (permissions: I.ParticipantPermissions) { - this.setLoading(true); + const onConfirm = (permissions: I.ParticipantPermissions) => { + setIsLoading(true); - C.SpaceRequestApprove(this.getSpaceId(), this.getIdentity(), permissions, (message: any) => { + C.SpaceRequestApprove(spaceId, identity, permissions, (message: any) => { if (message.error.code) { - this.setError(message.error.description); + setError(message.error.description); return; }; analytics.event('ApproveInviteRequest', { type: permissions }); - this.setLoading(false); - this.props.close(); + setIsLoading(false); + close(); }); }; - onReject () { - this.setLoading(true); + const onReject = () => { + setIsLoading(true); - C.SpaceRequestDecline(this.getSpaceId(), this.getIdentity(), (message: any) => { + C.SpaceRequestDecline(spaceId, identity, (message: any) => { if (message.error.code) { - this.setError(message.error.description); + setError(message.error.description); return; }; analytics.event('RejectInviteRequest'); - this.setLoading(false); - this.props.close(); + setIsLoading(false); + close(); }); }; - setLoading (isLoading: boolean) { - this.setState({ isLoading }); - }; - - setError (error: string) { - this.setState({ error, isLoading: false }); - }; + const space = U.Space.getSpaceviewBySpaceId(spaceId) || {}; - load () { - this.setLoading(true); + const load = () => { + setIsLoading(true); U.Data.search({ - spaceId: this.getSpaceId(), + spaceId, keys: U.Data.participantRelationKeys(), filters: [ { relationKey: 'layout', condition: I.FilterCondition.Equal, value: I.ObjectLayout.Participant }, @@ -146,39 +67,60 @@ const PopupInviteConfirm = observer(class PopupInviteConfirm extends React.Compo ignoreDeleted: true, noDeps: true, }, (message: any) => { - this.participants = message.records || []; - this.setLoading(false); + const records = (message.records || []).filter(it => it.isActive); + + readerLimt.current = space.readersLimit - records.length; + writerLimit.current = space.writersLimit - records.filter(it => it.isWriter || it.isOwner).length; + + setIsLoading(false); }); }; - getSpaceId () { - return String(this.props.param.data?.spaceId || ''); + const name = U.Common.shorten(String(data.name || translate('defaultNamePage')), 32); + + let buttons = []; + if (!readerLimt.current && membership.isExplorer) { + buttons.push({ id: 'reader', text: translate('popupInviteConfirmButtonReaderLimit'), onClick: () => onMembership('members') }); + } else + if (!writerLimit.current) { + buttons = buttons.concat([ + { id: 'reader', text: translate('popupInviteConfirmButtonReader'), onClick: () => onConfirm(I.ParticipantPermissions.Reader) }, + { id: 'writer', text: translate('popupInviteConfirmButtonWriterLimit'), onClick: () => onMembership('editors') }, + ]); + } else { + buttons = buttons.concat([ + { id: 'reader', text: translate('popupInviteConfirmButtonReader'), onClick: () => onConfirm(I.ParticipantPermissions.Reader) }, + { id: 'writer', text: translate('popupInviteConfirmButtonWriter'), onClick: () => onConfirm(I.ParticipantPermissions.Writer) }, + ]); }; - getIdentity () { - return String(this.props.param.data?.identity || ''); - }; + useEffect(() => { + analytics.event('ScreenInviteConfirm', { route }); + load(); + }, []); - getReaderLimit () { - const space = U.Space.getSpaceviewBySpaceId(this.getSpaceId()); - if (!space) { - return 0; - }; + return ( + <> + {isLoading ? <Loader id="loader" /> : ''} - const participants = this.participants.filter(it => it.isActive); - return space.readersLimit - participants.length; - }; + <div className="iconWrapper"> + <IconObject object={{ name, iconImage: icon, layout: I.ObjectLayout.Participant }} size={48} /> + </div> - getWriterLimit () { - const space = U.Space.getSpaceviewBySpaceId(this.getSpaceId()); - if (!space) { - return 0; - }; + <Title text={U.Common.sprintf(translate('popupInviteConfirmTitle'), name, U.Common.shorten(space.name, 32))} /> - const participants = this.participants.filter(it => it.isActive && (it.isWriter || it.isOwner)); - return space.writersLimit - participants.length; - }; + <div className="buttons"> + <div className="sides"> + {buttons.map((item: any, i: number) => <Button key={i} {...item} className="c36" />)} + </div> + + <Button onClick={onReject} text={translate('popupInviteConfirmButtonReject')} className="c36" color="red" /> + </div> + + <Error text={error} /> + </> + ); -}); +})); -export default PopupInviteConfirm; +export default PopupInviteConfirm; \ No newline at end of file diff --git a/src/ts/component/popup/invite/qr.tsx b/src/ts/component/popup/invite/qr.tsx index b11f2ed695..63b0e3f467 100644 --- a/src/ts/component/popup/invite/qr.tsx +++ b/src/ts/component/popup/invite/qr.tsx @@ -1,49 +1,44 @@ -import * as React from 'react'; +import React, { forwardRef, useRef } from 'react'; import $ from 'jquery'; -import { Title, Button } from 'Component'; -import { I, S, J, translate, Renderer, analytics } from 'Lib'; -import QRCode from 'qrcode.react'; +import { Title, Button, QR } from 'Component'; +import { I, translate, Renderer, analytics } from 'Lib'; -class PopupInviteQr extends React.Component<I.Popup> { +const PopupInviteQr = forwardRef<{}, I.Popup>((props, ref) => { - node = null; + const nodeRef = useRef(null); + const { param } = props; + const { data } = param; + const { link } = data; - constructor (props: I.Popup) { - super(props); + const onDownload = () => { + const canvas = $(nodeRef.current).find('canvas').get(0); + if (!canvas) { + return; + }; - this.onDownload = this.onDownload.bind(this); - }; - - render () { - const { param } = this.props; - const { data } = param; - const { link } = data; - const theme = S.Common.getThemeClass(); - - return ( - <div ref={ref => this.node = ref}> - <Title text={translate('popupInviteQrTitle')} /> - - <div className="qrWrap"> - <QRCode value={link} fgColor={J.Theme[theme].qr.foreground} bgColor={J.Theme[theme].qr.bg} size={120} /> - </div> - - <div className="buttons"> - <Button text={translate('commonDownload')} className="c36" color="blank" onClick={this.onDownload} /> - </div> - </div> - ); - }; - - onDownload () { - const node = $(this.node); - const canvas = node.find('canvas').get(0); const image = canvas.toDataURL('image/png'); + if (!image) { + return; + }; Renderer.send('download', image, { saveAs: true }); analytics.event('ClickSettingsSpaceShare', { type: 'DownloadQr' }); }; -}; + return ( + <div ref={nodeRef}> + <Title text={translate('popupInviteQrTitle')} /> + + <div className="qrWrap"> + <QR value={link} /> + </div> + + <div className="buttons"> + <Button text={translate('commonDownload')} className="c36" color="blank" onClick={onDownload} /> + </div> + </div> + ); + +}); export default PopupInviteQr; diff --git a/src/ts/component/popup/migration.tsx b/src/ts/component/popup/migration.tsx index ed9d89f063..3393d3b2b7 100644 --- a/src/ts/component/popup/migration.tsx +++ b/src/ts/component/popup/migration.tsx @@ -1,84 +1,61 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import { observer } from 'mobx-react'; -import { Title, Label, Button } from 'Component'; -import { I, S, U, J, Onboarding, translate, analytics } from 'Lib'; -import QRCode from 'qrcode.react'; +import { Title, Label, Button, QR } from 'Component'; +import { I, U, J, Onboarding, translate, analytics } from 'Lib'; -interface State { - step: number; -}; +const PopupMigration = observer(forwardRef<{}, I.Popup>((props, ref) => { -const PopupMigration = observer(class PopupMigration extends React.Component<I.Popup, State> { + const { param, close } = props; + const { data } = param; + const { type } = data; - state = { - step: 0, - }; - node = null; + let content = null; - render () { - const { param, close } = this.props; - const { data } = param; - const { type } = data; - const theme = S.Common.getThemeClass(); + switch (type) { + case 'onboarding': { + content = ( + <React.Fragment> + <Title text={'⚡️ ' + translate('popupMigrationOnboardingTitle')} /> + <Label text={translate('popupMigrationOnboardingText1')} /> + <Label text={translate('popupMigrationOnboardingText2')} /> - let content = null; + <div className="qrWrap"> + <QR value={J.Url.download} /> + </div> - switch (type) { - case 'onboarding': { - content = ( - <React.Fragment> - <Title text={'⚡️ ' + translate('popupMigrationOnboardingTitle')} /> - <Label text={translate('popupMigrationOnboardingText1')} /> - <Label text={translate('popupMigrationOnboardingText2')} /> - - <div className="qrWrap"> - <QRCode value={J.Url.download} fgColor={J.Theme[theme].qr.foreground} bgColor={J.Theme[theme].qr.bg} size={100} /> - </div> - - <Label text={translate('popupMigrationOnboardingText3')} /> - - <div className="buttons"> - <Button text={translate('commonDone')} className="c36" onClick={() => close()} /> - </div> - </React.Fragment> - ); - break; - }; + <Label text={translate('popupMigrationOnboardingText3')} /> - case 'import': { - content = ( - <React.Fragment> - <Title text={translate('popupMigrationImportTitle')} /> - <Label text={translate('popupMigrationImportText1')} /> - <Label text={translate('popupMigrationImportText2')} /> - - <div className="qrWrap"> - <QRCode value={J.Url.download} fgColor={J.Theme[theme].qr.foreground} bgColor={J.Theme[theme].qr.bg} size={100} /> - </div> - - <Label text={U.Common.sprintf(translate('popupMigrationImportText3'), J.Url.community)} /> - - <div className="buttons"> - <Button text={translate('commonDone')} className="c36" onClick={() => close()} /> - </div> - </React.Fragment> - ); - break; - }; + <div className="buttons"> + <Button text={translate('commonDone')} className="c36" onClick={() => close()} /> + </div> + </React.Fragment> + ); + break; }; - return ( - <div ref={ref => this.node = ref}> - {content} - </div> - ); + case 'import': { + content = ( + <React.Fragment> + <Title text={translate('popupMigrationImportTitle')} /> + <Label text={translate('popupMigrationImportText1')} /> + <Label text={translate('popupMigrationImportText2')} /> + + <div className="qrWrap"> + <QR value={J.Url.download} /> + </div> + + <Label text={U.Common.sprintf(translate('popupMigrationImportText3'), J.Url.community)} /> + + <div className="buttons"> + <Button text={translate('commonDone')} className="c36" onClick={() => close()} /> + </div> + </React.Fragment> + ); + break; + }; }; - componentDidMount () { - const { param } = this.props; - const { data } = param; - const { type } = data; - + useEffect(() => { let event = ''; switch (type) { case 'onboarding': { @@ -95,29 +72,28 @@ const PopupMigration = observer(class PopupMigration extends React.Component<I.P if (event) { analytics.event(event); }; - } - componentWillUnmount(): void { - const { param } = this.props; - const { data } = param; - const { type } = data; - const eventData = { type: 'exit', route: '' }; - - switch (type) { - case 'onboarding': { - eventData.route = analytics.route.migrationOffer; - Onboarding.start('dashboard', false, true); - break; - }; - case 'import': { - eventData.route = analytics.route.migrationImport; - break; + return () => { + const eventData = { type: 'exit', route: '' }; + + switch (type) { + case 'onboarding': { + eventData.route = analytics.route.migrationOffer; + Onboarding.start('dashboard', false, true); + break; + }; + case 'import': { + eventData.route = analytics.route.migrationImport; + break; + }; }; + + analytics.event('ClickMigration', eventData); }; + }, []); - analytics.event('ClickMigration', eventData); - }; + return content; -}); +})); export default PopupMigration; diff --git a/src/ts/component/popup/objectManager.tsx b/src/ts/component/popup/objectManager.tsx index f868cd1cb9..d62d86edc4 100644 --- a/src/ts/component/popup/objectManager.tsx +++ b/src/ts/component/popup/objectManager.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { Title, Button, ListObjectManager } from 'Component'; +import { Title, Button, ListManager } from 'Component'; import { C, I, keyboard, translate } from 'Lib'; import { observer } from 'mobx-react'; @@ -35,7 +35,7 @@ const PopupObjectManager = observer(class PopupObjectManager extends React.Compo <React.Fragment> <Title text={title} /> - <ListObjectManager + <ListManager ref={ref => this.refManager = ref} subId={subId} rowLength={2} @@ -89,7 +89,7 @@ const PopupObjectManager = observer(class PopupObjectManager extends React.Compo switch (type) { case I.ObjectManagerPopup.Favorites: { - C.ObjectListSetIsFavorite(this.refManager.selected, true); + C.ObjectListSetIsFavorite(this.refManager.getSelected(), true); break; }; }; diff --git a/src/ts/component/popup/page.tsx b/src/ts/component/popup/page.tsx index 3ab81d5166..5c7d2bd521 100644 --- a/src/ts/component/popup/page.tsx +++ b/src/ts/component/popup/page.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { RouteComponentProps } from 'react-router'; @@ -8,89 +8,45 @@ import { Page } from 'Component'; interface Props extends I.Popup, RouteComponentProps<any> {}; -const PopupPage = observer(class PopupPage extends React.Component<Props> { +const PopupPage = observer(forwardRef<{}, Props>((props, ref) => { + + const { param, getId } = props; + const { data } = param; + const { matchPopup } = data; - _isMounted = false; - ref = null; - - render () { - const { param } = this.props; - const { data } = param; - const { matchPopup } = data; - - return ( - <div id="wrap"> - <Page - ref={ref => this.ref = ref} - {...this.props} - rootId={matchPopup.params.id} - isPopup={true} - matchPopup={matchPopup} - /> - </div> - ); - }; - - componentDidMount () { - const { param } = this.props; - const { data } = param; - const { matchPopup } = data; - - this._isMounted = true; - this.rebind(); - this.resize(); - - historyPopup.pushMatch(matchPopup); - }; - - componentWillUnmount () { - this._isMounted = false; - this.unbind(); - - historyPopup.clear(); - keyboard.setWindowTitle(); - }; - - rebind () { - if (!this._isMounted) { - return; - }; - - this.unbind(); + const rebind = () => { + unbind(); - const { getId } = this.props; - const win = $(window); - const obj = $(`#${getId()}`); - - win.on('resize.popupPage', () => this.resize()); - obj.find('.innerWrap').on('scroll.common', () => S.Menu.resizeAll()); + $(`#${getId()}`).find('.innerWrap').on('scroll.common', () => S.Menu.resizeAll()); }; - unbind () { - const { getId } = this.props; - const win = $(window); - const obj = $(`#${getId()}`); - - win.off('resize.popupPage'); - obj.find('.innerWrap').off('scroll.common'); + const unbind = () => { + $(`#${getId()}`).find('.innerWrap').off('scroll.common'); }; - resize () { - if (!this._isMounted) { - return; - }; + useEffect(() => { + rebind(); - const { getId, position } = this.props; - const obj = $(`#${getId()}-innerWrap`); - const loader = obj.find('#loader'); - const hh = J.Size.header; - - loader.css({ width: obj.width(), height: obj.height() }); - position(); - - raf(() => { obj.css({ top: hh + 20, marginTop: 0 }); }); - }; + historyPopup.pushMatch(matchPopup); -}); + return () => { + unbind(); + historyPopup.clear(); + keyboard.setWindowTitle(); + }; + }, []); + + return ( + <div id="wrap"> + <Page + {...props} + rootId={matchPopup.params.id} + isPopup={true} + matchPopup={matchPopup} + /> + </div> + ); + +})); export default PopupPage; \ No newline at end of file diff --git a/src/ts/component/popup/page/settings/import/csv.tsx b/src/ts/component/popup/page/settings/import/csv.tsx index 9fb0ae8ee2..2a409331ec 100644 --- a/src/ts/component/popup/page/settings/import/csv.tsx +++ b/src/ts/component/popup/page/settings/import/csv.tsx @@ -30,7 +30,7 @@ class PopupSettingsPageImportCsv extends React.Component<I.PopupSettings, State> const { error } = this.state; const { delimiter, delimiters } = this.delimiterOptions(); - let modeOptions: any[] = [ + const modeOptions: any[] = [ { id: I.CsvImportMode.Collection, name: translate('popupSettingsImportCsvCollection') }, ]; @@ -38,8 +38,6 @@ class PopupSettingsPageImportCsv extends React.Component<I.PopupSettings, State> modeOptions.unshift({ id: I.CsvImportMode.Table, name: translate('popupSettingsImportCsvTable') }); }; - modeOptions = modeOptions.map(it => ({ ...it, id: String(it.id) })); - return ( <div> <Head {...this.props} returnTo="importIndex" name={translate('popupSettingsImportTitle')} /> diff --git a/src/ts/component/popup/page/settings/personal.tsx b/src/ts/component/popup/page/settings/personal.tsx index 38c664a841..25ac20ec16 100644 --- a/src/ts/component/popup/page/settings/personal.tsx +++ b/src/ts/component/popup/page/settings/personal.tsx @@ -7,21 +7,16 @@ const PopupSettingsPagePersonal = observer(class PopupSettingsPagePersonal exten render () { const { getId } = this.props; - const { config, interfaceLang, navigationMenu, linkStyle, fullscreenObject, hideSidebar, showRelativeDates, showVault, dateFormat, timeFormat, } = S.Common; + const { config, interfaceLang, linkStyle, fullscreenObject, hideSidebar, showRelativeDates, showVault, dateFormat, timeFormat, } = S.Common; const { hideTray, hideMenuBar, languages } = config; const canHideMenu = U.Common.isPlatformWindows() || U.Common.isPlatformLinux(); const interfaceLanguages = U.Menu.getInterfaceLanguages(); const spellingLanguages = U.Menu.getSpellingLanguages(); - const navigationMenuModes: I.Option[] = [ - { id: I.NavigationMenuMode.Click, name: translate('popupSettingsPersonalNavigationMenuClick') }, - { id: I.NavigationMenuMode.Hover, name: translate('popupSettingsPersonalNavigationMenuHover') }, - { id: I.NavigationMenuMode.Context, name: translate('popupSettingsPersonalNavigationMenuContext') }, - ]; const linkStyles: I.Option[] = [ { id: I.LinkCardStyle.Card, name: translate('menuBlockLinkSettingsStyleCard') }, { id: I.LinkCardStyle.Text, name: translate('menuBlockLinkSettingsStyleText') }, - ].map(it => ({ ...it, id: String(it.id) })); + ]; const sidebarMode = showVault ? translate('sidebarMenuAll') : translate('sidebarMenuSidebar'); return ( @@ -64,22 +59,6 @@ const PopupSettingsPagePersonal = observer(class PopupSettingsPagePersonal exten <Label className="section" text={translate('popupSettingsPersonalSectionEditor')} /> <div className="actionItems"> - <div className="item"> - <Label text={translate('popupSettingsPersonalNavigationMenu')} /> - - <Select - id="navigationMenu" - value={navigationMenu} - options={navigationMenuModes} - onChange={v => { - S.Common.navigationMenuSet(v); - analytics.event('ChangeShowQuickCapture', { type: v }); - }} - arrowClassName="black" - menuParam={{ horizontal: I.MenuDirection.Right }} - /> - </div> - <div className="item"> <Label text={translate('popupSettingsPersonalLinkStyle')} /> @@ -167,8 +146,11 @@ const PopupSettingsPagePersonal = observer(class PopupSettingsPagePersonal exten <Select id="dateFormat" value={String(dateFormat)} - options={U.Menu.dateFormatOptions().map(it => ({ ...it, id: String(it.id) }))} - onChange={v => S.Common.dateFormatSet(v)} + options={U.Menu.dateFormatOptions()} + onChange={v => { + S.Common.dateFormatSet(v); + analytics.event('ChangeDateFormat', { type: v }); + }} arrowClassName="black" menuParam={{ horizontal: I.MenuDirection.Right }} /> @@ -179,8 +161,11 @@ const PopupSettingsPagePersonal = observer(class PopupSettingsPagePersonal exten <Select id="timeFormat" value={String(timeFormat)} - options={U.Menu.timeFormatOptions().map(it => ({ ...it, id: String(it.id) }))} - onChange={v => S.Common.timeFormatSet(v)} + options={U.Menu.timeFormatOptions()} + onChange={v => { + S.Common.timeFormatSet(v); + analytics.event('ChangeTimeFormat', { type: v }); + }} arrowClassName="black" menuParam={{ horizontal: I.MenuDirection.Right }} /> diff --git a/src/ts/component/popup/page/settings/phrase.tsx b/src/ts/component/popup/page/settings/phrase.tsx index 1ee5fb6900..3e43680131 100644 --- a/src/ts/component/popup/page/settings/phrase.tsx +++ b/src/ts/component/popup/page/settings/phrase.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import QRCode from 'qrcode.react'; -import { Title, Label, Phrase } from 'Component'; -import { I, C, S, U, J, translate, analytics, Storage, Renderer } from 'Lib'; +import { Title, Label, Phrase, QR } from 'Component'; +import { I, C, S, U, translate, analytics, Storage, Renderer } from 'Lib'; interface State { entropy: string; @@ -28,7 +27,6 @@ const PopupSettingsPagePhrase = observer(class PopupSettingsPagePhrase extends R render () { const { entropy, showCode } = this.state; - const theme = S.Common.getThemeClass(); return ( <div @@ -52,7 +50,7 @@ const PopupSettingsPagePhrase = observer(class PopupSettingsPagePhrase extends R <div className="qrWrap" onClick={this.onCode}> <div className={!showCode ? 'isBlurred' : ''}> - <QRCode value={showCode ? entropy : translate('popupSettingsCodeStub')} fgColor={J.Theme[theme].qr.foreground} bgColor={J.Theme[theme].qr.bg} size={116} /> + <QR value={showCode ? entropy : translate('popupSettingsCodeStub')} /> </div> </div> </div> @@ -101,4 +99,4 @@ const PopupSettingsPagePhrase = observer(class PopupSettingsPagePhrase extends R }); -export default PopupSettingsPagePhrase; \ No newline at end of file +export default PopupSettingsPagePhrase; diff --git a/src/ts/component/popup/page/settings/pin/confirm.tsx b/src/ts/component/popup/page/settings/pin/confirm.tsx index ea0b576752..ff58ac910c 100644 --- a/src/ts/component/popup/page/settings/pin/confirm.tsx +++ b/src/ts/component/popup/page/settings/pin/confirm.tsx @@ -18,14 +18,18 @@ const PopupSettingsPagePinConfirm = observer(class PopupSettingsPagePinConfirm e render () { const { onPage, prevPage } = this.props; const { error } = this.state; - const pin = Storage.getPin(); return ( <React.Fragment> <Head onPage={() => onPage(prevPage)} name={translate('commonBack')} /> <Title text={translate('popupSettingsPinTitle')} /> <Label className="description" text={translate('popupSettingsPinVerify')} /> - <Pin ref={ref => this.ref = ref} expectedPin={pin} onSuccess={this.onCheckPin} onError={this.onError} /> + <Pin + ref={ref => this.ref = ref} + expectedPin={Storage.getPin()} + onSuccess={this.onCheckPin} + onError={this.onError} + /> <Error text={error} /> </React.Fragment> ); diff --git a/src/ts/component/popup/page/settings/space/index.tsx b/src/ts/component/popup/page/settings/space/index.tsx index e011ad5a71..8a5fb876f2 100644 --- a/src/ts/component/popup/page/settings/space/index.tsx +++ b/src/ts/component/popup/page/settings/space/index.tsx @@ -58,7 +58,6 @@ const PopupSettingsSpaceIndex = observer(class PopupSettingsSpaceIndex extends R const hasLink = cid && key; const isOwner = U.Space.isMyOwner(); const canWrite = U.Space.canMyParticipantWrite(); - const canDelete = !space.isPersonal; const isShareActive = U.Space.isShareActive(); let bytesUsed = 0; @@ -139,7 +138,9 @@ const PopupSettingsSpaceIndex = observer(class PopupSettingsSpaceIndex extends R <div className="sections"> <div className="section sectionSpaceShare" - onMouseEnter={isShareActive ? () => {} : e => Preview.tooltipShow({ text: translate('popupSettingsSpaceShareGenerateInviteDisabled'), element: $(e.currentTarget) })} + onMouseEnter={isShareActive ? () => {} : e => { + Preview.tooltipShow({ text: translate('popupSettingsSpaceShareGenerateInviteDisabled'), element: $(e.currentTarget) }); + }} onMouseLeave={e => Preview.tooltipHide(false)} > <Title text={translate(`popupSettingsSpaceShareTitle`)} /> @@ -239,8 +240,8 @@ const PopupSettingsSpaceIndex = observer(class PopupSettingsSpaceIndex extends R <Title text={translate('commonOwner')} /> </div> <div className="side right"> - <IconObject object={creator} size={24} /> - <ObjectName object={creator} /> + <IconObject object={{ ...creator, layout: I.ObjectLayout.Participant }} size={24} /> + <ObjectName object={{ ...creator, layout: I.ObjectLayout.Participant }} /> {creator.identity == account.id ? <div className="caption">({translate('commonYou')})</div> : ''} </div> </div> @@ -416,11 +417,9 @@ const PopupSettingsSpaceIndex = observer(class PopupSettingsSpaceIndex extends R </div> </div> - {canDelete ? ( - <div className="buttons"> - <Button text={isOwner ? translate('commonDelete') : translate('commonLeaveSpace')} color="red" onClick={this.onDelete} /> - </div> - ) : ''} + <div className="buttons"> + <Button text={isOwner ? translate('commonDelete') : translate('commonLeaveSpace')} color="red" onClick={this.onDelete} /> + </div> <Error text={error} /> </div> diff --git a/src/ts/component/popup/page/settings/space/list.tsx b/src/ts/component/popup/page/settings/space/list.tsx index 66aff30e6d..bb6ef72e15 100644 --- a/src/ts/component/popup/page/settings/space/list.tsx +++ b/src/ts/component/popup/page/settings/space/list.tsx @@ -12,7 +12,6 @@ const PopupSettingsPageSpacesList = observer(class PopupSettingsPageSpacesList e const Row = (space: any) => { const participant = U.Space.getMyParticipant(space.targetSpaceId); const creator = U.Space.getCreator(space.targetSpaceId, space.creator); - const hasMenu = !space.isPersonal; let creatorElement = null; if (participant && !participant.isOwner && !creator._empty_) { @@ -36,7 +35,7 @@ const PopupSettingsPageSpacesList = observer(class PopupSettingsPageSpacesList e <div className="col">{participant ? translate(`participantPermissions${participant.permissions}`) : ''}</div> <div className="col">{translate(`spaceStatus${space.spaceAccountStatus}`)}</div> <div className="col colMore"> - {hasMenu ? <Icon id={`icon-more-${space.id}`} className="more withBackground" onClick={() => this.onMore(space)} /> : ''} + <Icon id={`icon-more-${space.id}`} className="more withBackground" onClick={() => this.onMore(space)} /> </div> </div> ); diff --git a/src/ts/component/popup/page/settings/space/share.tsx b/src/ts/component/popup/page/settings/space/share.tsx index 0dcab34b1e..771a13c99b 100644 --- a/src/ts/component/popup/page/settings/space/share.tsx +++ b/src/ts/component/popup/page/settings/space/share.tsx @@ -350,7 +350,7 @@ const PopupSettingsSpaceShare = observer(class PopupSettingsSpaceShare extends R C.SpaceInviteGenerate(S.Common.space, (message: any) => { btn.setLoading(false); - if (!this.setError(message.error)) { + if (this.setError(message.error)) { return; }; @@ -372,9 +372,7 @@ const PopupSettingsSpaceShare = observer(class PopupSettingsSpaceShare extends R textConfirm: translate('popupConfirmStopSharingSpaceConfirm'), colorConfirm: 'red', onConfirm: () => { - C.SpaceStopSharing(S.Common.space); - this.setInvite('', ''); - + C.SpaceStopSharing(S.Common.space, () => this.setInvite('', '')); analytics.event('StopSpaceShare'); }, }, diff --git a/src/ts/component/popup/page/settings/space/storage.tsx b/src/ts/component/popup/page/settings/space/storage.tsx index 03740c824e..7ce5d83003 100644 --- a/src/ts/component/popup/page/settings/space/storage.tsx +++ b/src/ts/component/popup/page/settings/space/storage.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; import { observer } from 'mobx-react'; -import { Title, ListObjectManager } from 'Component'; +import { Title, ListManager } from 'Component'; import { I, J, translate, Action, analytics } from 'Lib'; import Head from '../head'; const PopupSettingsPageStorageManager = observer(class PopupSettingsPageStorageManager extends React.Component<I.PopupSettings, {}> { - ref = null; + refManager = null; constructor (props: I.PopupSettings) { super(props); @@ -31,8 +31,8 @@ const PopupSettingsPageStorageManager = observer(class PopupSettingsPageStorageM <Head onPage={this.onBack} name={translate('commonBack')} /> <Title text={translate('popupSettingsSpaceStorageManagerTitle')} /> - <ListObjectManager - ref={ref => this.ref = ref} + <ListManager + ref={ref => this.refManager = ref} subId={J.Constant.subId.fileManager} rowLength={2} buttons={buttons} @@ -49,9 +49,7 @@ const PopupSettingsPageStorageManager = observer(class PopupSettingsPageStorageM }; onRemove () { - if (this.ref) { - Action.delete(this.ref.selected || [], analytics.route.settings, () => this.ref.selectionClear()); - }; + Action.delete(this.refManager.getSelected(), analytics.route.settings, () => this.refManager?.selectionClear()); }; onBack () { diff --git a/src/ts/component/popup/page/usecase/item.tsx b/src/ts/component/popup/page/usecase/item.tsx index 456670be6f..cc44532fb5 100644 --- a/src/ts/component/popup/page/usecase/item.tsx +++ b/src/ts/component/popup/page/usecase/item.tsx @@ -136,7 +136,6 @@ class PopupUsecasePageItem extends React.Component<I.PopupUsecase, State> { }; onMenu () { - const { config } = S.Common; const { getId, close } = this.props; const object = this.getObject(); const route = this.getRoute(); @@ -160,7 +159,7 @@ class PopupUsecasePageItem extends React.Component<I.PopupUsecase, State> { noVirtualisation: true, onSelect: (e: any, item: any) => { const isNew = item.id == 'add'; - const withChat = U.Common.isChatAllowed(); + const withChat = U.Object.isAllowedChat(); this.setState({ isLoading: true }); analytics.event('ClickGalleryInstallSpace', { type: isNew ? 'New' : 'Existing', route }); @@ -191,7 +190,7 @@ class PopupUsecasePageItem extends React.Component<I.PopupUsecase, State> { ]; if (U.Space.canCreateSpace()) { - list.push({ id: 'add', icon: 'add', name: translate('popupUsecaseSpaceCreate') }); + list.push({ id: 'add', icon: 'add', name: translate('popupUsecaseSpaceCreate'), isBig: true }); }; list = list.concat(U.Space.getList() diff --git a/src/ts/component/popup/phrase.tsx b/src/ts/component/popup/phrase.tsx index 02a182c533..8bf7d7b22e 100644 --- a/src/ts/component/popup/phrase.tsx +++ b/src/ts/component/popup/phrase.tsx @@ -1,44 +1,42 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { Title, Label, Button, IconObject } from 'Component'; import { I, translate } from 'Lib'; -class PopupPhrase extends React.Component<I.Popup> { +const PopupPhrase: FC<I.Popup> = (props) => { - render () { - return ( - <div> - <Title text={translate('popupPhraseTitle1')} /> - <div className="rows"> - <div className="row"> - <IconObject size={40} iconSize={40} object={{ iconEmoji: ':game_die:' }} /> - <Label text={translate('popupPhraseLabel1')} /> - </div> - <div className="row"> - <IconObject size={40} iconSize={40} object={{ iconEmoji: ':old_key:' }} /> - <Label text={translate('popupPhraseLabel2')} /> - </div> - <div className="row"> - <IconObject size={40} iconSize={40} object={{ iconEmoji: ':point_up:' }} /> - <Label text={translate('popupPhraseLabel3')} /> - </div> + return ( + <> + <Title text={translate('popupPhraseTitle1')} /> + <div className="rows"> + <div className="row"> + <IconObject size={40} iconSize={40} object={{ iconEmoji: ':game_die:' }} /> + <Label text={translate('popupPhraseLabel1')} /> </div> - - <Title className="c2" text={translate('popupPhraseTitle2')} /> - <div className="columns"> - <div className="column"> - <li>{translate('popupPhraseLabel4')}</li> - </div> - <div className="column"> - <li>{translate('popupPhraseLabel5')}</li> - </div> + <div className="row"> + <IconObject size={40} iconSize={40} object={{ iconEmoji: ':old_key:' }} /> + <Label text={translate('popupPhraseLabel2')} /> + </div> + <div className="row"> + <IconObject size={40} iconSize={40} object={{ iconEmoji: ':point_up:' }} /> + <Label text={translate('popupPhraseLabel3')} /> </div> + </div> - <div className="buttons"> - <Button text={translate('commonOkay')} onClick={() => this.props.close()} /> + <Title className="c2" text={translate('popupPhraseTitle2')} /> + <div className="columns"> + <div className="column"> + <li>{translate('popupPhraseLabel4')}</li> </div> + <div className="column"> + <li>{translate('popupPhraseLabel5')}</li> + </div> + </div> + + <div className="buttons"> + <Button text={translate('commonOkay')} onClick={() => props.close()} /> </div> - ); - }; + </> + ); }; diff --git a/src/ts/component/popup/pin.tsx b/src/ts/component/popup/pin.tsx index fb6de7aa47..4c56ebbafb 100644 --- a/src/ts/component/popup/pin.tsx +++ b/src/ts/component/popup/pin.tsx @@ -1,56 +1,15 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useRef, useEffect } from 'react'; import { Title, Pin, Error } from 'Component'; import { I, keyboard, translate, Storage } from 'Lib'; -import { observer } from 'mobx-react'; -interface State { - error: string; -}; +const PopupPin = forwardRef<{}, I.Popup>(({ param, close }, ref) => { -const PopupPin = observer(class PopupConfirm extends React.Component<I.Popup, State> { - - ref = null; - state = { - error: '' - }; - - constructor (props: I.Popup) { - super(props); - - this.onSuccess = this.onSuccess.bind(this); - this.onError = this.onError.bind(this); - }; - - render () { - const { error } = this.state; - - return ( - <React.Fragment> - <Title text={translate('authPinCheckTitle')} /> - <Pin - ref={ref => this.ref = ref} - expectedPin={Storage.getPin()} - onSuccess={this.onSuccess} - onError={this.onError} - /> - <Error text={error} /> - </React.Fragment> - ); - }; - - componentDidMount() { - keyboard.setFocus(true); - }; - - componentWillUnmount() { - keyboard.setFocus(false); - }; - - onSuccess () { - const { param, close } = this.props; - const { data } = param; - const { onSuccess } = data; + const { data } = param; + const { onError, onSuccess } = data; + const pinRef = useRef(null); + const [ error, setError ] = useState(''); + const onSuccessHandler = () => { if (onSuccess) { onSuccess(); }; @@ -58,19 +17,36 @@ const PopupPin = observer(class PopupConfirm extends React.Component<I.Popup, St close(); }; - onError () { - const { param } = this.props; - const { data } = param; - const { onError } = data; - - this.ref.reset(); - this.setState({ error: translate('authPinCheckError') }); + const onErrorHandler = () => { + pinRef.current.reset(); + setError(translate('authPinCheckError')); if (onError) { onError(); }; }; - + + useEffect(() => { + keyboard.setFocus(true); + + return () => { + keyboard.setFocus(false); + }; + }, []); + + return ( + <> + <Title text={translate('authPinCheckTitle')} /> + <Pin + ref={pinRef} + expectedPin={Storage.getPin()} + onSuccess={onSuccessHandler} + onError={onErrorHandler} + /> + <Error text={error} /> + </> + ); + }); export default PopupPin; \ No newline at end of file diff --git a/src/ts/component/popup/preview.tsx b/src/ts/component/popup/preview.tsx index e9ef39d625..f2e4edd77f 100644 --- a/src/ts/component/popup/preview.tsx +++ b/src/ts/component/popup/preview.tsx @@ -265,7 +265,7 @@ class PopupPreview extends React.Component<I.Popup> { const { getId } = this.props; const node = $(`#${getId()}-innerWrap`); const element = node.find(`#itemPreview-${idx}`); - const loader = element.find('.loader') + const loader = element.find('.loader'); const obj = this.galleryMap.get(idx); const { src, type, isLoaded, width, height } = obj; diff --git a/src/ts/component/popup/search.tsx b/src/ts/component/popup/search.tsx index 9fb5c5c440..8d260740fe 100644 --- a/src/ts/component/popup/search.tsx +++ b/src/ts/component/popup/search.tsx @@ -27,6 +27,7 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, refList: any = null; refRows: any[] = []; timeout = 0; + delay = 0; cache: any = {}; items: any[] = []; n = 0; @@ -53,7 +54,6 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, render () { const { isLoading } = this.state; - const filter = this.getFilter(); const items = this.getItems(); const Context = (meta: any): any => { @@ -247,17 +247,18 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, <div className="head"> <Filter icon="search" - value={filter} + value={this.filter} ref={ref => this.refFilter = ref} placeholder={translate('popupSearchPlaceholder')} onSelect={this.onFilterSelect} - onKeyUp={this.onFilterChange} + onChange={v => this.onFilterChange(v)} + onKeyUp={(e, v) => this.onFilterChange(v)} onClear={this.onFilterClear} /> </div> {!items.length && !isLoading ? ( - <EmptySearch filter={filter} /> + <EmptySearch filter={this.filter} /> ) : ''} {this.cache && items.length && !isLoading ? ( @@ -303,18 +304,15 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, const filter = String(storage.filter || ''); const setFilter = () => { - if (!filter || !this.refFilter) { + if (!this.refFilter) { return; }; - this.filter = filter; this.range = { from: 0, to: filter.length }; this.refFilter.setValue(filter); - this.reload(); + this.refFilter.setRange(this.range); }; - setFilter(); - this._isMounted = true; this.initCache(); this.rebind(); @@ -324,7 +322,7 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, if (backlink) { U.Object.getById(backlink, {}, item => this.setBacklink(item, 'Saved', () => setFilter())); } else { - this.reload(); + setFilter(); }; analytics.event('ScreenSearch', { route, type: (filter ? 'Saved' : 'Empty') }); @@ -332,22 +330,10 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, componentDidUpdate () { const items = this.getItems(); - const filter = this.getFilter(); - - if (filter != this.filter) { - this.filter = filter; - this.reload(); - return; - }; this.initCache(); this.setActive(items[this.n]); - if (this.refFilter) { - this.refFilter.setValue(this.filter); - this.refFilter.setRange(this.range); - }; - if (this.refList) { this.refList.recomputeRowHeights(0); this.refList.scrollToPosition(this.top); @@ -498,24 +484,32 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, node.find('.item.active').removeClass('active'); }; - onFilterChange (e: any, v: string) { + onFilterChange (v: string) { const { storageSet, param } = this.props; const { data } = param; const { route } = data; - if (this.filter == v) { + window.clearTimeout(this.timeout); + + if (v && (this.filter == v)) { return; }; - window.clearTimeout(this.timeout); this.timeout = window.setTimeout(() => { storageSet({ filter: v }); + if (this.filter != v) { + analytics.event('SearchInput', { route }); + }; + + this.filter = v; this.range = this.refFilter?.getRange(); - this.forceUpdate(); - - analytics.event('SearchInput', { route }); - }, J.Constant.delay.keyboard); + this.reload(); + + if (!this.delay) { + this.delay = J.Constant.delay.keyboard; + }; + }, this.delay); }; onFilterSelect (e: any) { @@ -523,13 +517,11 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, }; onFilterClear () { - const { param } = this.props; + const { param, storageSet } = this.props; const { data } = param; const { route } = data; - this.props.storageSet({ filter: '' }); - this.reload(); - + storageSet({ filter: '' }); analytics.event('SearchInput', { route }); }; @@ -589,7 +581,7 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, load (clear: boolean, callBack?: () => void) { const { space } = S.Common; const { backlink } = this.state; - const filter = this.getFilter(); + const filter = this.filter; const templateType = S.Record.getTemplateType(); const filters: any[] = [ { relationKey: 'layout', condition: I.FilterCondition.NotIn, value: U.Object.getSystemLayouts() }, @@ -634,11 +626,13 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup, this.items = this.items.concat(records); - U.Data.subscribeIds({ - subId: J.Constant.subId.search, - ids: this.items.map(it => it.id), - noDeps: true, - }); + if (this.items.length) { + U.Data.subscribeIds({ + subId: J.Constant.subId.search, + ids: this.items.map(it => it.id), + noDeps: true, + }); + }; if (clear) { this.setState({ isLoading: false }, callBack); diff --git a/src/ts/component/popup/settings/onboarding.tsx b/src/ts/component/popup/settings/onboarding.tsx index 85c985a344..5b39dddcb9 100644 --- a/src/ts/component/popup/settings/onboarding.tsx +++ b/src/ts/component/popup/settings/onboarding.tsx @@ -11,6 +11,16 @@ const PopupSettingsOnboarding = observer(class PopupSettingsOnboarding extends R constructor (props: I.Popup) { super(props); + const { networkConfig } = S.Auth; + const { mode, path } = networkConfig; + const userPath = U.Common.getElectron().userPath(); + + this.config = { + userPath, + mode, + path: path || '', + }; + this.onUpload = this.onUpload.bind(this); this.onSave = this.onSave.bind(this); this.onPathClick = this.onPathClick.bind(this); @@ -50,7 +60,6 @@ const PopupSettingsOnboarding = observer(class PopupSettingsOnboarding extends R <div className="actionItems"> <div className="item"> <Label text={translate('popupSettingsPersonalInterfaceLanguage')} /> - <Select id="interfaceLang" value={interfaceLang} @@ -89,7 +98,7 @@ const PopupSettingsOnboarding = observer(class PopupSettingsOnboarding extends R <Label text={translate('popupSettingsOnboardingNetworkTitle')} /> {path ? <Label className="small" text={U.Common.shorten(path, 32)} /> : ''} </div> - <Button className="c28" text={translate('commonUpload')} onClick={this.onUpload} /> + <Button className="c28" text={translate('commonLoad')} onClick={this.onUpload} /> </div> ) : ''} @@ -113,20 +122,6 @@ const PopupSettingsOnboarding = observer(class PopupSettingsOnboarding extends R ); }; - componentDidMount(): void { - const { networkConfig } = S.Auth; - const { mode, path } = networkConfig; - const userPath = U.Common.getElectron().userPath(); - - this.config = { - userPath, - mode, - path: path || '' - }; - this.refMode?.setValue(this.config.mode); - this.forceUpdate(); - }; - onChange (key: string, value: any) { this.config[key] = value; this.forceUpdate(); diff --git a/src/ts/component/popup/share.tsx b/src/ts/component/popup/share.tsx index dd1dcb4d63..89b26d8f49 100644 --- a/src/ts/component/popup/share.tsx +++ b/src/ts/component/popup/share.tsx @@ -1,35 +1,26 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { Title, Label, Button } from 'Component'; import { I, U, J, translate, analytics } from 'Lib'; -class PopupShare extends React.Component<I.Popup> { +const PopupShare: FC<I.Popup> = () => { - constructor (props: I.Popup) { - super(props); - - this.onClick = this.onClick.bind(this); + const onClick = () => { + U.Common.copyToast(translate('commonLink'), J.Url.share); + analytics.event('ClickShareAppCopyLink'); }; - render () { - return ( - <div> - <Title text={translate('popupShareTitle')} /> - <Label text={translate('popupShareLabel')} /> + return ( + <> + <Title text={translate('popupShareTitle')} /> + <Label text={translate('popupShareLabel')} /> - <div className="section"> - <Label text={U.Common.sprintf(translate('popupShareLinkText'), J.Url.share, J.Url.share)} /> - </div> - - <Button text={translate('commonCopyLink')} onClick={this.onClick} /> + <div className="section"> + <Label text={U.Common.sprintf(translate('popupShareLinkText'), J.Url.share, J.Url.share)} /> </div> - ); - }; - - onClick () { - U.Common.copyToast(translate('commonLink'), J.Url.share); - analytics.event('ClickShareAppCopyLink'); - }; + <Button text={translate('commonCopyLink')} onClick={onClick} /> + </> + ); }; -export default PopupShare; +export default PopupShare; \ No newline at end of file diff --git a/src/ts/component/popup/shortcut.tsx b/src/ts/component/popup/shortcut.tsx index 7cded06096..28a127c332 100644 --- a/src/ts/component/popup/shortcut.tsx +++ b/src/ts/component/popup/shortcut.tsx @@ -1,365 +1,69 @@ -import * as React from 'react'; -import $ from 'jquery'; -import raf from 'raf'; -import { I, U, J, keyboard, translate } from 'Lib'; +import React, { forwardRef, useState } from 'react'; +import { I, U, J } from 'Lib'; -interface State { - page: string; -}; +const PopupShortcut = forwardRef<{}, I.Popup>(() => { -interface Section { - id?: string; - name: string; - children: { - name?: string; - description?: string; - className?: string; - children: Item[]; - }[]; -}; + const [ page, setPage ] = useState('main'); + const isMac = U.Common.isPlatformMac(); + const sections = J.Shortcut(); + const section = sections.find(it => it.id == page); -interface Item { - com?: string; - mac?: string; - name: string; -}; + const Tab = (item: any) => ( + <div className={[ 'item', (item.id == page ? 'active' : '') ].join(' ')} onClick={() => setPage(item.id)}> + {item.name} + </div> + ); -class PopupShortcut extends React.Component<I.Popup, State> { + const Section = (item: any) => { + const cn = [ 'section' ]; - state = { - page: 'main', - }; - _isMounted = false; - - render () { - const { page } = this.state; - const isMac = U.Common.isPlatformMac(); - const sections = this.getSections(); - const section = sections.find(it => it.id == page); - - const Tab = (item: any) => ( - <div className={[ 'item', (item.id == page ? 'active' : '') ].join(' ')} onClick={() => this.onPage(item.id)}> - {item.name} - </div> - ); - - const Section = (item: any) => { - const cn = [ 'section' ]; - - if (item.className) { - cn.push(item.className); - }; - - return ( - <div className={cn.join(' ')}> - {item.name ? <div className="name">{item.name}</div> : ''} - {item.description ? <div className="descr">{item.description}</div> : ''} - - <div className="items"> - {item.children.map((item: any, i: number) => ( - <Item key={i} {...item} /> - ))} - </div> - </div> - ); - }; - - const Item = (item: any) => { - const caption = isMac && item.mac ? item.mac : item.com; - - return ( - <div className="item"> - <div className="key" dangerouslySetInnerHTML={{ __html: U.Common.sanitize(caption) }} /> - <div className="descr">{item.name}</div> - </div> - ); + if (item.className) { + cn.push(item.className); }; return ( - <div className="wrapper"> - <div className="head"> - <div className="tabs"> - {sections.map((item: any, i: number) => ( - <Tab key={i} {...item} /> - ))} - </div> - </div> + <div className={cn.join(' ')}> + {item.name ? <div className="name">{item.name}</div> : ''} + {item.description ? <div className="descr">{item.description}</div> : ''} - <div className="body scrollable"> - {(section.children || []).map((item: any, i: number) => ( - <Section key={i} {...item} /> + <div className="items"> + {item.children.map((item: any, i: number) => ( + <Item key={i} {...item} /> ))} </div> </div> ); }; - componentDidMount () { - this._isMounted = true; - this.rebind(); - this.resize(); - }; - - componentWillUnmount () { - this._isMounted = false; - this.unbind(); - }; - - rebind () { - this.unbind(); - $(window).on('resize.popupShortcut', () => this.resize()); - }; - - unbind () { - $(window).off('resize.popupShortcut'); - }; - - onPage (id: string) { - this.setState({ page: id }); - }; - - getSections (): Section[] { - const cmd = keyboard.cmdSymbol(); - const alt = keyboard.altSymbol(); + const Item = (item: any) => { + const caption = isMac && item.mac ? item.mac : item.com; - const sections = [ - { - id: 'main', - name: translate('popupShortcutMain'), - children: [ - { - name: translate('popupShortcutBasics'), children: [ - { com: `${cmd} + N`, name: translate('popupShortcutMainBasics1') }, - { com: `${cmd} + ${alt} + N`, name: translate('popupShortcutMainBasics19') }, - { com: `${cmd} + Shift + N`, name: translate('popupShortcutMainBasics2') }, - { com: `${cmd} + Enter`, name: translate('popupShortcutMainBasics4') }, - { mac: `${cmd} + Ctrl + F`, com: `${cmd} + ${alt} + F`, name: translate('popupShortcutMainBasics5') }, - { com: `${cmd} + Z`, name: translate('popupShortcutMainBasics6') }, - { com: `${cmd} + Shift + Z`, name: translate('popupShortcutMainBasics7') }, - { com: `${cmd} + P`, name: translate('popupShortcutMainBasics8') }, - { com: `${cmd} + F`, name: translate('popupShortcutMainBasics9') }, - { com: `${cmd} + Q`, name: translate('popupShortcutMainBasics10') }, - { mac: `${cmd} + Y`, com: 'Ctrl + H', name: translate('popupShortcutMainBasics11') }, - { com: 'Shift + Click', name: translate('popupShortcutMainBasics12') }, - { com: `${cmd} + Click`, name: translate('popupShortcutMainBasics13') }, - { com: 'Ctrl + Space', name: translate('popupShortcutMainBasics14') }, - { com: `${cmd} + \\, ${cmd} + .`, name: translate('popupShortcutMainBasics15') }, - { com: `${cmd} + =`, name: translate('popupShortcutMainBasics16') }, - { com: `${cmd} + Minus`, name: translate('popupShortcutMainBasics17') }, - { com: `${cmd} + 0`, name: translate('popupShortcutMainBasics18') }, - { com: `Ctrl + Tab, Ctrl + Shift + Tab`, name: translate('popupShortcutMainBasics20') }, - { com: `${cmd} + Shift + M`, name: translate('popupShortcutMainBasics21') }, - ] - }, - - { - name: translate('popupShortcutMainStructuring'), children: [ - { com: 'Enter', name: translate('popupShortcutMainStructuring1') }, - { com: 'Shift + Enter', name: translate('popupShortcutMainStructuring2') }, - { com: 'Delete', name: translate('popupShortcutMainStructuring3') }, - { com: 'Tab', name: translate('popupShortcutMainStructuring4') }, - { com: 'Shift + Tab', name: translate('popupShortcutMainStructuring5') }, - ] - }, - - { - name: translate('popupShortcutMainSelection'), children: [ - { com: 'Double Click', name: translate('popupShortcutMainSelection1') }, - { com: 'Triple Click', name: translate('popupShortcutMainSelection2') }, - { com: `${cmd} + A`, name: translate('popupShortcutMainSelection3') }, - { com: 'Shift + ↑ or ↓', name: translate('popupShortcutMainSelection4') }, - { com: `${cmd} + Click`, name: translate('popupShortcutMainSelection5') }, - { com: 'Shift + Click', name: translate('popupShortcutMainSelection6') }, - ] - }, - - { - name: translate('commonActions'), children: [ - { com: '/', name: translate('popupShortcutMainActions1') }, - { com: `${cmd} + /`, name: translate('popupShortcutMainActions2') }, - { mac: `${cmd} + Delete`, com: 'Ctrl + Backspace', name: translate('popupShortcutMainActions3') }, - { com: `${cmd} + C`, name: translate('popupShortcutMainActions4') }, - { com: `${cmd} + X`, name: translate('popupShortcutMainActions5') }, - { com: `${cmd} + V`, name: translate('popupShortcutMainActions6') }, - { com: `${cmd} + D`, name: translate('popupShortcutMainActions7') }, - { com: `${cmd} + E`, name: translate('popupShortcutMainActions8') + ' 🏄‍♂' }, - ] - }, - - { - name: translate('popupShortcutMainTextStyle'), children: [ - { com: `${cmd} + B`, name: translate('popupShortcutMainTextStyle1') }, - { com: `${cmd} + I`, name: translate('popupShortcutMainTextStyle2') }, - { com: `${cmd} + U`, name: translate('popupShortcutMainTextStyle3') }, - { com: `${cmd} + Shift +S`, name: translate('popupShortcutMainTextStyle4') }, - { com: `${cmd} + K`, name: translate('popupShortcutMainTextStyle5') }, - { com: `${cmd} + L`, name: translate('popupShortcutMainTextStyle6') }, - { com: `${cmd} + Shift + C`, name: translate('popupShortcutMainTextStyle7') }, - { com: `${cmd} + Shift + H`, name: translate('popupShortcutMainTextStyle8') }, - ] - }, - ], - }, - - { - id: 'navigation', - name: translate('popupShortcutNavigation'), - children: [ - { - name: translate('popupShortcutBasics'), children: [ - { com: `${cmd} + ,(comma)`, name: translate('popupShortcutNavigationBasics1') }, - { com: `${cmd} + O`, name: translate('popupShortcutNavigationBasics2') }, - { com: `${cmd} + ${alt} + O`, name: translate('popupShortcutNavigationBasics3') }, - { com: `${cmd} + S, ${cmd} + K`, name: translate('popupShortcutNavigationBasics4') }, - { com: `${alt} + H`, name: translate('popupShortcutNavigationBasics6') }, - { mac: `${cmd} + [, ${cmd} + ←`, com: 'Alt + ←', name: translate('popupShortcutNavigationBasics7') }, - { mac: `${cmd} + ], ${cmd} + →`, com: 'Alt + →', name: translate('popupShortcutNavigationBasics8') }, - ] - }, - - { - name: translate('popupShortcutNavigationMenu'), children: [ - { com: '↓ or Tab', name: translate('popupShortcutNavigationMenu1') }, - { com: '↑ or Shift + Tab', name: translate('popupShortcutNavigationMenu2') }, - { com: '←', name: translate('popupShortcutNavigationMenu3') }, - { com: '→', name: translate('popupShortcutNavigationMenu4') }, - { com: 'Enter', name: translate('popupShortcutNavigationMenu5') }, - ] - }, - - { - name: translate('popupShortcutNavigationPage'), children: [ - { com: `${cmd} + Shift + T`, name: translate('popupShortcutNavigationPage1') }, - { com: '↓', name: translate('popupShortcutNavigationPage2') }, - { com: '↑', name: translate('popupShortcutNavigationPage3') }, - { com: `${cmd} + ←`, name: translate('popupShortcutNavigationPage4') }, - { com: `${cmd} + →`, name: translate('popupShortcutNavigationPage5') }, - { com: `${cmd} + ↑`, name: translate('popupShortcutNavigationPage6') }, - { com: `${cmd} + ↓`, name: translate('popupShortcutNavigationPage7') }, - { com: `${cmd} + Shift + ↑↓`, name: translate('popupShortcutNavigationPage8') }, - { com: `${cmd} + Shift + R`, name: translate('popupShortcutNavigationPage9') }, - { com: `${cmd} + Enter`, name: translate('popupShortcutNavigationPage10') }, - ] - }, - ], - }, - - { - id: 'markdown', - name: translate('popupShortcutMarkdown'), - children: [ - { - name: translate('popupShortcutMarkdownWhileTyping'), - children: [ - { com: '`', name: translate('popupShortcutMarkdownWhileTyping1') }, - { com: '** or __', name: translate('popupShortcutMarkdownWhileTyping2') }, - { com: '* or _', name: translate('popupShortcutMarkdownWhileTyping3') }, - { com: '~~', name: translate('popupShortcutMarkdownWhileTyping4') }, - { com: '-->', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '⟶') }, - { com: '<--', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '⟵') }, - { com: '<-->', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '⟷') }, - { com: '->', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '→') }, - { com: '<-', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '←') }, - { com: '--', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '—') }, - { com: '(r)', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '®') }, - { com: '(tm)', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '™') }, - { com: '...', name: U.Common.sprintf(translate('popupShortcutMarkdownWhileTypingInserts'), '…') }, - ] - }, - { - name: translate('popupShortcutMarkdownBeginningOfLine'), - children: [ - { com: '# + Space', name: translate('popupShortcutMarkdownBeginningOfLine1') }, - { com: '# # + Space', name: translate('popupShortcutMarkdownBeginningOfLine2') }, - { com: '# # # + Space', name: translate('popupShortcutMarkdownBeginningOfLine3') }, - { com: '" + Space', name: translate('popupShortcutMarkdownBeginningOfLine4') }, - { com: '* or + or - and Space', name: translate('popupShortcutMarkdownBeginningOfLine5') }, - { com: '[] + Space', name: translate('popupShortcutMarkdownBeginningOfLine6') }, - { com: '1. + Space', name: translate('popupShortcutMarkdownBeginningOfLine7') }, - { com: '> + Space', name: translate('popupShortcutMarkdownBeginningOfLine8') }, - { com: '``` + Space', name: translate('popupShortcutMarkdownBeginningOfLine9') }, - { com: '--- + Space', name: translate('popupShortcutMarkdownBeginningOfLine10') }, - { com: '*** + Space', name: translate('popupShortcutMarkdownBeginningOfLine11') }, - ] - }, - ], - }, - - { - id: 'command', - name: translate('popupShortcutCommand'), - children: [ - { - name: translate('commonMenu'), children: [ - { com: '/', name: translate('popupShortcutCommandMenu1') }, - { com: '↓ & ↑', name: translate('popupShortcutCommandMenu2') }, - { com: '→ & ←', name: translate('popupShortcutCommandMenu3') }, - { com: 'Esc or Clear /', name: translate('popupShortcutCommandMenu4') }, - ] - }, - - { description: translate('popupShortcutCommandDescription'), children: [], className: 'separator' }, - { - name: translate('popupShortcutCommandText'), children: [ - { com: '/text', name: translate('popupShortcutCommandText1') }, - { com: '/h1', name: translate('popupShortcutCommandText2') }, - { com: '/h2', name: translate('popupShortcutCommandText3') }, - { com: '/h3', name: translate('popupShortcutCommandText4') }, - { com: '/high', name: translate('popupShortcutCommandText5') }, - ] - }, - - { - name: translate('popupShortcutCommandLists'), children: [ - { com: '/todo', name: translate('popupShortcutCommandLists1') }, - { com: '/bullet', name: translate('popupShortcutCommandLists2') }, - { com: '/num', name: translate('popupShortcutCommandLists3') }, - { com: '/toggle', name: translate('popupShortcutCommandLists4') }, - ] - }, - - { - name: translate('popupShortcutCommandObjects'), children: [ - { com: '@today, @tomorrow', name: translate('popupShortcutCommandObjects1') }, - { com: '/page', name: translate('popupShortcutCommandObjects2') }, - { com: '/file', name: translate('popupShortcutCommandObjects3') }, - { com: '/image', name: translate('popupShortcutCommandObjects4') }, - { com: '/video', name: translate('popupShortcutCommandObjects5') }, - { com: '/bookmark', name: translate('popupShortcutCommandObjects6') }, - { com: '/link', name: translate('popupShortcutCommandObjects7') }, - ] - }, - - { - name: translate('popupShortcutCommandOther'), children: [ - { com: '/line', name: translate('popupShortcutCommandOther1') }, - { com: '/dots', name: translate('popupShortcutCommandOther2') }, - { com: '/code', name: translate('popupShortcutCommandOther3') }, - ] - }, - ], - }, - ]; - - return sections; + return ( + <div className="item"> + <div className="key" dangerouslySetInnerHTML={{ __html: U.Common.sanitize(caption) }} /> + <div className="descr">{item.name}</div> + </div> + ); }; - resize () { - if (!this._isMounted) { - return; - }; - - const { getId, position } = this.props; - const obj = $(`#${getId()}-innerWrap`); - const loader = obj.find('#loader'); - const hh = J.Size.header; - - loader.css({ width: obj.width(), height: obj.height() }); - position(); + return ( + <div className="wrapper"> + <div className="head"> + <div className="tabs"> + {sections.map((item: any, i: number) => ( + <Tab key={i} {...item} /> + ))} + </div> + </div> - raf(() => { obj.css({ top: hh + 20, marginTop: 0 }); }); - }; + <div className="body scrollable"> + {(section.children || []).map((item: any, i: number) => ( + <Section key={i} {...item} /> + ))} + </div> + </div> + ); -}; +}); -export default PopupShortcut; +export default PopupShortcut; \ No newline at end of file diff --git a/src/ts/component/popup/spaceCreate.tsx b/src/ts/component/popup/spaceCreate.tsx index 961f3a2786..a2cc4734c9 100644 --- a/src/ts/component/popup/spaceCreate.tsx +++ b/src/ts/component/popup/spaceCreate.tsx @@ -1,103 +1,42 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, useEffect } from 'react'; import { observer } from 'mobx-react'; -import _ from 'lodash'; import { Label, Input, IconObject, Button, Loader, Error } from 'Component'; import { I, C, S, U, J, translate, keyboard, analytics, Storage } from 'Lib'; -interface State { - error: string; - isLoading: boolean; - iconOption: number; - usecase: I.Usecase; -}; +const PopupSpaceCreate = observer(forwardRef<{}, I.Popup>(({ param = {}, close }, ref) => { -const PopupSpaceCreate = observer(class PopupSpaceCreate extends React.Component<I.PopupSettings, State> { + const nameRef = useRef(null); + const iconRef = useRef(null); + const [ error, setError ] = useState(''); + const [ isLoading, setIsLoading ] = useState(false); + const [ iconOption, setIconOption ] = useState(U.Common.rand(1, J.Constant.count.icon)); - refIcon: any = null; - refName: any = null; + const onKeyDown = (e: any, v: string) => { + keyboard.shortcut('enter', e, () => { + e.preventDefault(); - state = { - error: '', - isLoading: false, - iconOption: U.Common.rand(1, J.Constant.count.icon), - usecase: I.Usecase.Empty, + onSubmit(false); + }); }; - constructor (props: any) { - super(props); - - this.onKeyDown = this.onKeyDown.bind(this); - this.onChange = this.onChange.bind(this); - this.onIcon = this.onIcon.bind(this); - }; + const onChange = (e: any, v: string) => { + const object = getObject(); - render () { - const { error, iconOption, isLoading } = this.state; - const space = { - name: this.refName?.getValue(), - layout: I.ObjectLayout.SpaceView, - iconOption, + if (iconRef.current) { + object.name = v; + iconRef.current?.setObject(object); }; - - return ( - <React.Fragment> - - {isLoading ? <Loader id="loader" /> : ''} - - <Label text={translate('popupSpaceCreateLabel')} /> - - <div className="iconWrapper"> - <IconObject - ref={ref => this.refIcon = ref} - size={96} - object={space} - canEdit={false} - menuParam={{ horizontal: I.MenuDirection.Center }} - onClick={this.onIcon} - /> - </div> - - <div className="nameWrapper"> - <Input - ref={ref => this.refName = ref} - value="" - onKeyDown={this.onKeyDown} - onChange={this.onChange} - placeholder={translate('defaultNamePage')} - /> - </div> - - <div className="buttons"> - <Button text={translate('popupSpaceCreateCreate')} onClick={() => this.onSubmit(false)} /> - <Button text={translate('popupSpaceCreateImport')} color="blank" onClick={() => this.onSubmit(true)} /> - </div> - - <Error text={error} /> - - </React.Fragment> - ); - }; - - componentDidMount (): void { - window.setTimeout(() => this.refName?.focus(), 15); }; - onKeyDown (e: any, v: string) { - keyboard.shortcut('enter', e, () => { - e.preventDefault(); - - this.onSubmit(false); - }); - }; - - onChange (e: any, v: string) { - if (this.refIcon) { - this.refIcon.props.object.name = v; - this.refIcon.forceUpdate(); + const getObject = () => { + return { + name: nameRef.current?.getValue(), + layout: I.ObjectLayout.SpaceView, + iconOption: iconOption, }; }; - checkName (v: string): string { + const checkName = (v: string): string => { if ([ translate('defaultNameSpace'), translate('defaultNamePage'), @@ -107,37 +46,37 @@ const PopupSpaceCreate = observer(class PopupSpaceCreate extends React.Component return v; }; - onSubmit (withImport: boolean) { - const { param } = this.props; - const { isLoading, iconOption } = this.state; + const onSubmit = (withImport: boolean) => { const { data } = param; const { onCreate, route } = data; - const name = this.checkName(this.refName.getValue()); + const name = checkName(nameRef.current.getValue()); if (isLoading) { return; }; - this.setLoading(true); + setIsLoading(true); - const withChat = U.Common.isChatAllowed(); + const withChat = U.Object.isAllowedChat(); const details = { name, iconOption, spaceDashboardId: I.HomePredefinedId.Last, }; - C.WorkspaceCreate(details, I.Usecase.GetStarted, withChat, (message: any) => { - this.setLoading(false); + analytics.event(withImport ? 'ClickCreateSpaceImport' : 'ClickCreateSpaceEmpty'); + + C.WorkspaceCreate(details, I.Usecase.Empty, withChat, (message: any) => { + setIsLoading(false); if (message.error.code) { - this.setState({ error: message.error.description }); + setError(message.error.description); return; }; C.WorkspaceSetInfo(message.objectId, details, () => { if (message.error.code) { - this.setState({ error: message.error.description }); + setError(message.error.description); return; }; @@ -150,7 +89,7 @@ const PopupSpaceCreate = observer(class PopupSpaceCreate extends React.Component U.Space.initSpaceState(); if (withImport) { - this.props.close(() => { + close(() => { S.Popup.open('settings', { data: { isSpace: true, page: 'importIndex' }, className: 'isSpace' }); }); }; @@ -161,28 +100,67 @@ const PopupSpaceCreate = observer(class PopupSpaceCreate extends React.Component } }); - analytics.event('CreateSpace', { usecase: I.Usecase.GetStarted, middleTime: message.middleTime, route }); - analytics.event('SelectUsecase', { type: I.Usecase.GetStarted }); + analytics.event('CreateSpace', { usecase: I.Usecase.Empty, middleTime: message.middleTime, route }); + analytics.event('SelectUsecase', { type: I.Usecase.Empty }); }); }); }; - setLoading (isLoading: boolean) { - this.state.isLoading = isLoading; - this.setState({ isLoading }); - }; - - onIcon () { - let { iconOption } = this.state; + const onIcon = () => { + let icon = iconOption; - iconOption++; - if (iconOption > J.Constant.count.icon) { - iconOption = 1; + icon++; + if (icon > J.Constant.count.icon) { + icon = 1; }; - this.setState({ iconOption }); + setIconOption(icon); }; -}); + const object = getObject(); + + useEffect(() => { + const object = getObject(); + iconRef.current?.setObject(object); + }, [ iconOption ]); + + return ( + <> + {isLoading ? <Loader id="loader" /> : ''} + <Label text={translate('popupSpaceCreateLabel')} /> + + <div className="iconWrapper"> + <IconObject + ref={iconRef} + size={96} + object={object} + canEdit={false} + menuParam={{ horizontal: I.MenuDirection.Center }} + onClick={onIcon} + /> + </div> + + <div className="nameWrapper"> + <Input + ref={nameRef} + value="" + focusOnMount={true} + onKeyDown={onKeyDown} + onChange={onChange} + placeholder={translate('defaultNamePage')} + /> + </div> + + <div className="buttons"> + <Button text={translate('popupSpaceCreateCreate')} onClick={() => onSubmit(false)} /> + <Button text={translate('popupSpaceCreateImport')} color="blank" onClick={() => onSubmit(true)} /> + </div> + + <Error text={error} /> + + </> + ); + +})); export default PopupSpaceCreate; \ No newline at end of file diff --git a/src/ts/component/preview/default.tsx b/src/ts/component/preview/default.tsx index 6157d90cd3..32de526185 100644 --- a/src/ts/component/preview/default.tsx +++ b/src/ts/component/preview/default.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect, useState, useRef } from 'react'; import { observer } from 'mobx-react'; import { ObjectName, ObjectDescription, ObjectType, IconObject, Loader } from 'Component'; import { S, U } from 'Lib'; @@ -11,99 +11,70 @@ interface Props { setObject?: (object: any) => void; }; -interface State { - object: any; - loading: boolean; -}; - -const PreviewDefault = observer(class PreviewDefault extends React.Component<Props, State> { - - public static defaultProps = { - className: '', - }; - - _isMounted = false; - state = { - object: null, - loading: false, - }; - id = ''; - - render () { - const { className } = this.props; - const { loading } = this.state; - const cn = [ 'previewDefault', className ]; - const object = this.props.object || this.state.object || {}; - const type = S.Record.getTypeById(object.type); - - if (U.Object.isParticipantLayout(object.layout)) { - object.name = object.globalName || object.name; - }; - - return ( - <div className={cn.join(' ')}> - {loading ? <Loader /> : ( - <React.Fragment> - <div className="previewHeader"> - <IconObject object={object} /> - <ObjectName object={object} /> - </div> - <ObjectDescription object={object} /> - <div className="featured"> - <ObjectType object={type} /> - </div> - </React.Fragment> - )} - </div> - ); - }; - - componentDidMount (): void { - this._isMounted = true; - this.init(); +const PreviewDefault = observer(forwardRef<{}, Props>((props, ref) => { + + const { + rootId = '', + className = '', + object: initialObject, + position, + setObject: setParentObject, + } = props; + const [ isLoading, setIsLoading ] = useState(false); + const [ object, setObject ] = useState(initialObject || {}); + const idRef = useRef(null); + const cn = [ 'previewDefault', className ]; + const type = S.Record.getTypeById(object.type); + + if (U.Object.isParticipantLayout(object.layout)) { + object.name = object.globalName || object.name; }; - componentDidUpdate (): void { - this.init(); - }; - - init () { - const { position } = this.props; - const object = this.props.object || this.state.object; - - object ? position && position() : this.load(); - }; - - componentWillUnmount(): void { - this._isMounted = false; - this.id = ''; - }; - - load () { - const { rootId, setObject } = this.props; - const { loading } = this.state; - - if (loading || (this.id == rootId)) { + const load = () => { + if (isLoading || (idRef.current == rootId)) { return; }; - this.id = rootId; - this.setState({ loading: true }); + idRef.current = rootId; + setIsLoading(true); U.Object.getById(rootId, {}, (object) => { - if (!this._isMounted) { - return; - }; + setObject(object); + setIsLoading(false); - this.state.object = object; - this.setState({ object, loading: false }); - - if (setObject) { - setObject(object); + if (setParentObject) { + setParentObject(object); }; }); }; -}); + useEffect(() => { + if (initialObject) { + setObject(initialObject); + }; + }); + + useEffect(() => { + initialObject ? position && position() : load(); + }); + + return ( + <div className={cn.join(' ')}> + {isLoading ? <Loader /> : ( + <> + <div className="previewHeader"> + <IconObject object={object} /> + <ObjectName object={object} /> + </div> + <ObjectDescription object={object} /> + <div className="featured"> + <ObjectType object={type} /> + </div> + </> + )} + </div> + ); + +})); export default PreviewDefault; \ No newline at end of file diff --git a/src/ts/component/preview/index.tsx b/src/ts/component/preview/index.tsx index 575a64bd2f..a983238d4d 100644 --- a/src/ts/component/preview/index.tsx +++ b/src/ts/component/preview/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useEffect, useRef, MouseEvent } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { PreviewLink, PreviewObject, PreviewDefault } from 'Component'; @@ -7,104 +7,21 @@ import { I, S, U, Preview, Mark, translate, Action } from 'Lib'; const OFFSET_Y = 8; const BORDER = 12; -interface State { - object: any; -}; - -const PreviewComponent = observer(class PreviewComponent extends React.Component<object, State> { - - ref = null; - state = { - object: null, - }; +const PreviewIndex = observer(forwardRef(() => { - constructor (props) { - super(props); - - this.onClick = this.onClick.bind(this); - this.onCopy = this.onCopy.bind(this); - this.onEdit = this.onEdit.bind(this); - this.onUnlink = this.onUnlink.bind(this); - this.position = this.position.bind(this); - this.setObject = this.setObject.bind(this); - }; - - render () { - const { preview } = S.Common; - const { type, target, object, noUnlink, noEdit } = preview; - const cn = [ 'previewWrapper' ]; - - let head = null; - let content = null; - - switch (type) { - case I.PreviewType.Link: { - head = ( - <div className="head"> - <div id="button-copy" className="item" onClick={this.onCopy}>{translate('commonCopyLink')}</div> - {!noEdit ? <div id="button-edit" className="item" onClick={this.onEdit}>{translate('previewEdit')}</div> : ''} - {!noUnlink ? <div id="button-unlink" className="item" onClick={this.onUnlink}>{translate('commonUnlink')}</div> : ''} - </div> - ); - - content = <PreviewLink ref={ref => this.ref = ref} url={target} position={this.position} />; - break; - }; - - case I.PreviewType.Object: { - if (!noUnlink) { - head = ( - <div className="head"> - <div id="button-unlink" className="item" onClick={this.onUnlink}>{translate('commonUnlink')}</div> - </div> - ); - }; - - content = <PreviewObject ref={ref => this.ref = ref} size={I.PreviewSize.Small} rootId={target} setObject={this.setObject} position={this.position} />; - break; - }; - - case I.PreviewType.Default: { - if (!noUnlink) { - head = ( - <div className="head"> - <div id="button-unlink" className="item" onClick={this.onUnlink}>{translate('commonUnlink')}</div> - </div> - ); - }; - - content = <PreviewDefault ref={ref => this.ref = ref} rootId={target} object={object} setObject={this.setObject} position={this.position} />; - break; - }; - }; - - if (head) { - cn.push('withHead'); - }; - - return ( - <div id="preview" className={cn.join(' ')}> - <div className="polygon" onClick={this.onClick} /> - <div className="content"> - {head} - - <div onClick={this.onClick}> - {content} - </div> - </div> - </div> - ); - }; - - onClick (e: React.MouseEvent) { + const nodeRef = useRef(null); + const polygonRef = useRef(null); + const { preview } = S.Common; + const { type, target, object: initialObject, marks, range, noUnlink, noEdit, x, y, width, height, onChange } = preview; + const [ object, setObject ] = useState(initialObject || {}); + const cn = [ 'previewWrapper' ]; + const win = $(window); + + const onClick = (e: MouseEvent) => { if (e.button) { return; }; - const { preview } = S.Common; - const { type, target } = preview; - const object = preview.object || this.state.object; - switch (type) { case I.PreviewType.Link: { Action.openUrl(target); @@ -119,22 +36,16 @@ const PreviewComponent = observer(class PreviewComponent extends React.Component }; }; - onCopy () { - const { preview } = S.Common; - const { target } = preview; - - U.Common.clipboardCopy({ text: target }); + const onCopy = () => { + U.Common.copyToast(translate('commonLink'), target); Preview.previewHide(true); }; - onEdit (e) { + const onEdit = (e) => { e.preventDefault(); e.stopPropagation(); - const { preview } = S.Common; - const { marks, range, onChange } = preview; const mark = Mark.getInRange(marks, I.MarkType.Link, range); - const win = $(window); const rect = U.Common.objectCopy($('#preview').get(0).getBoundingClientRect()); S.Menu.open('blockLink', { @@ -151,18 +62,12 @@ const PreviewComponent = observer(class PreviewComponent extends React.Component }); }; - onUnlink () { - const { preview } = S.Common; - const { range, onChange } = preview; - - onChange(Mark.toggleLink({ type: this.getMarkType(), param: '', range }, preview.marks)); + const onUnlink = () => { + onChange(Mark.toggleLink({ type: getMarkType(), param: '', range }, preview.marks)); Preview.previewHide(true); }; - getMarkType () { - const { preview } = S.Common; - const { type } = preview; - + const getMarkType = () => { switch (type) { case I.PreviewType.Link: { return I.MarkType.Link; @@ -175,20 +80,13 @@ const PreviewComponent = observer(class PreviewComponent extends React.Component }; }; - setObject (object) { - this.setState({ object }); - }; - - position () { - const { preview } = S.Common; - const { x, y, width, height } = preview; - const win = $(window); - const obj = $('#preview'); - const poly = obj.find('.polygon'); + const position = () => { + const node = $(nodeRef.current); + const poly = $(polygonRef.current); const { ww, wh } = U.Common.getWindowDimensions(); const st = win.scrollTop(); - const ow = obj.outerWidth(); - const oh = obj.outerHeight(); + const ow = node.outerWidth(); + const oh = node.outerHeight(); const css: any = { opacity: 0, left: 0, top: 0 }; const pcss: any = { top: 'auto', bottom: 'auto', width: '', left: '', height: height + OFFSET_Y, clipPath: '' }; @@ -232,16 +130,90 @@ const PreviewComponent = observer(class PreviewComponent extends React.Component css.left = Math.max(BORDER, css.left); css.left = Math.min(ww - ow - BORDER, css.left); - obj.show().css(css); + node.show().css(css); if (!preview.noAnimation) { - obj.addClass('anim'); + node.addClass('anim'); }; poly.css(pcss); - window.setTimeout(() => { obj.css({ opacity: 1, transform: 'translateY(0%)' }); }, 15); + window.setTimeout(() => { node.css({ opacity: 1, transform: 'translateY(0%)' }); }, 15); + }; + + let head = null; + let content = null; + + const unlink = <div id="button-unlink" className="item" onClick={onUnlink}>{translate('commonUnlink')}</div>; + const props = { + rootId: target, + url: target, + object, + size: I.PreviewSize.Small, + setObject, + position, }; -}); + switch (type) { + case I.PreviewType.Link: { + head = ( + <div className="head"> + <div id="button-copy" className="item" onClick={onCopy}>{translate('commonCopyLink')}</div> + {!noEdit ? <div id="button-edit" className="item" onClick={onEdit}>{translate('previewEdit')}</div> : ''} + {!noUnlink ? unlink : ''} + </div> + ); + + content = <PreviewLink {...props} />; + break; + }; + + case I.PreviewType.Object: { + if (!noUnlink) { + head = <div className="head">{unlink}</div>; + }; + + content = <PreviewObject {...props} />; + break; + }; + + case I.PreviewType.Default: { + if (!noUnlink) { + head = <div className="head">{unlink}</div>; + }; + + content = <PreviewDefault {...props} />; + break; + }; + }; + + if (head) { + cn.push('withHead'); + }; + + useEffect(() => { + if (initialObject) { + setObject(initialObject); + }; + + position(); + }, [ type ]); + + return content ? ( + <div + ref={nodeRef} + id="preview" + className={cn.join(' ')} + onMouseLeave={() => Preview.previewHide(true)} + > + <div ref={polygonRef} className="polygon" onClick={onClick} /> + <div className="content"> + {head} + + <div onClick={onClick}>{content}</div> + </div> + </div> + ) : null; + +})); -export default PreviewComponent; +export default PreviewIndex; \ No newline at end of file diff --git a/src/ts/component/preview/link.tsx b/src/ts/component/preview/link.tsx index 8e634d7cb0..79ca94fad1 100644 --- a/src/ts/component/preview/link.tsx +++ b/src/ts/component/preview/link.tsx @@ -1,113 +1,79 @@ -import * as React from 'react'; -import $ from 'jquery'; +import React, { forwardRef, useState, useEffect, useRef } from 'react'; import { Loader } from 'Component'; import { C, U } from 'Lib'; -import { observer } from 'mobx-react'; interface Props { url: string; position?: () => void; }; -interface State { - loading: boolean; +interface Info { title: string; description: string; - faviconUrl: string; imageUrl: string; }; const ALLOWED_SCHEME = [ 'http', 'https' ]; -const PreviewLink = observer(class PreviewLink extends React.Component<Props, State> { - - _isMounted = false; - node: any = null; - state = { - loading: false, - title: '', - description: '', - faviconUrl: '', - imageUrl: '', - }; - url: string; - - render () { - const { url } = this.props; - const { loading, title, description } = this.state; - - return ( - <div ref={node => this.node = node} className="previewLink"> - {loading ? <Loader /> : ( - <React.Fragment> - <div className="info"> - <div className="link">{U.Common.shortUrl(url)}</div> - {title ? <div className="name">{title}</div> : ''} - {description ? <div className="descr">{description}</div> : ''} - </div> - </React.Fragment> - )} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - this.load(); - }; +const PreviewLink = forwardRef<{}, Props>(({ url = '', position }, ref: any) => { - componentDidUpdate () { - const { position } = this.props; - const { imageUrl } = this.state; - - $(this.node).toggleClass('withImage', !!imageUrl); - - this.load(); - - if (position) { - position(); - }; - }; + const [ object, setObject ] = useState({} as Info); + const [ isLoading, setIsLoading ] = useState(false); + const { title, description, imageUrl } = object; + const cn = [ 'previewLink' ]; + const urlRef = useRef(''); - componentWillUnmount () { - this._isMounted = false; + if (imageUrl) { + cn.push('withImage'); }; - load () { - const { url } = this.props; - - if (this.url == url) { + const load = () => { + if (urlRef.current == url) { return; }; - this.url = url; - this.setState({ loading: true }); - const scheme = U.Common.getScheme(url); + if (scheme && !ALLOWED_SCHEME.includes(scheme)) { - this.setState({ loading: false }); return; }; - C.LinkPreview(url, (message: any) => { - if (!this._isMounted) { - return; - }; + urlRef.current = url; + setIsLoading(true); - let state: any = { loading: false }; + C.LinkPreview(url, (message: any) => { + setIsLoading(false); if (!message.error.code) { - state = Object.assign(state, message.previewLink); - }; - - this.setState(state); - - if (message.error.code) { - this.url = ''; + setObject(message.previewLink); + } else { + urlRef.current = ''; }; }); }; + useEffect(() => { + load(); + + if (position) { + position(); + }; + }); + + return ( + <div className={cn.join(' ')}> + {isLoading ? <Loader /> : ( + <> + <div className="info"> + <div className="link">{U.Common.shortUrl(url)}</div> + {title ? <div className="name">{title}</div> : ''} + {description ? <div className="descr">{description}</div> : ''} + </div> + </> + )} + </div> + ); + }); export default PreviewLink; \ No newline at end of file diff --git a/src/ts/component/preview/object.tsx b/src/ts/component/preview/object.tsx index 6c268efe91..dee18665ff 100644 --- a/src/ts/component/preview/object.tsx +++ b/src/ts/component/preview/object.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useRef, useEffect } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { Loader, IconObject, Cover, Icon } from 'Component'; @@ -16,441 +16,295 @@ interface Props { setObject?: (object: any) => void; }; -interface State { - loading: boolean; -}; - const Colors = [ 'yellow', 'red', 'ice', 'lime' ]; -const PreviewObject = observer(class PreviewObject extends React.Component<Props, State> { - - state = { - loading: false, - }; - - node: any = null; - isOpen = false; - _isMounted = false; - id = ''; - - public static defaultProps = { - className: '', - }; - - constructor (props: Props) { - super(props); - - this.onMouseEnter = this.onMouseEnter.bind(this); - this.onMouseLeave = this.onMouseLeave.bind(this); - this.setActive = this.setActive.bind(this); - }; - - render () { - const { loading } = this.state; - const { rootId, className, onClick, onMore } = this.props; - const previewSize = this.props.size; - const contextId = this.getRootId(); - const check = U.Data.checkDetails(contextId, rootId); - const object = S.Detail.get(contextId, rootId); - const { name, description, coverType, coverId, coverX, coverY, coverScale, iconImage } = object; - const childBlocks = S.Block.getChildren(contextId, rootId, it => !it.isLayoutHeader()).slice(0, 10); - const isTask = U.Object.isTaskLayout(object.layout); - const isBookmark = U.Object.isBookmarkLayout(object.layou); - const cn = [ 'previewObject' , check.className, className ]; - - let n = 0; - let c = 0; - let size = 48; - let cnPreviewSize; - - switch (previewSize) { - case I.PreviewSize.Large: { - size = 48; - cnPreviewSize = 'large'; - break; - }; - - case I.PreviewSize.Medium: { - size = 40; - cnPreviewSize = 'medium'; - break; - }; - - default: - case I.PreviewSize.Small: { - size = 32; - cnPreviewSize = 'small'; - break; - }; - }; - cn.push(cnPreviewSize); - - if (isTask || isBookmark) { - size = 16; +const PreviewObject = observer(forwardRef<{}, Props>(({ + rootId = '', + size = I.PreviewSize.Small, + className = '', + onClick, + onMore, + onMouseEnter, + onMouseLeave, + position, + setObject, +}, ref: any) => { + + const nodeRef = useRef(null); + const idRef = useRef(''); + const [ isLoading, setIsLoading ] = useState(false); + + let n = 0; + let c = 0; + let iconSize = 48; + let cnPreviewSize = ''; + + const Block = (item: any) => { + const { content, fields } = item; + const { text, style, checked, targetObjectId } = content; + const childBlocks = S.Block.getChildren(contextId, item.id); + const length = childBlocks.length; + const cn = [ 'element', U.Data.blockClass(item), item.className ]; + + let bullet = null; + let inner = null; + let isRow = false; + let line = <div className="line" />; + + switch (item.type) { + case I.BlockType.Text: { + if (!text) { + line = null; + }; - if (previewSize == I.PreviewSize.Small) { - size = 14; - }; - }; + if ([ I.TextStyle.Checkbox, I.TextStyle.Bulleted, I.TextStyle.Numbered, I.TextStyle.Quote ].indexOf(style) >= 0) { + cn.push('withBullet'); + }; - const Block = (item: any) => { - const { content, fields } = item; - const { text, style, checked, targetObjectId } = content; - const childBlocks = S.Block.getChildren(contextId, item.id); - const length = childBlocks.length; - const cn = [ 'element', U.Data.blockClass(item), item.className ]; - - let bullet = null; - let inner = null; - let isRow = false; - let line = <div className="line" />; - - switch (item.type) { - case I.BlockType.Text: { - if (!text) { - line = null; + switch (style) { + default: { + inner = line; + break; }; - if ([ I.TextStyle.Checkbox, I.TextStyle.Bulleted, I.TextStyle.Numbered, I.TextStyle.Quote ].indexOf(style) >= 0) { - cn.push('withBullet'); + case I.TextStyle.Header1: + case I.TextStyle.Header2: + case I.TextStyle.Header3: { + inner = text; + break; }; - switch (style) { - default: { - inner = line; - break; - }; - - case I.TextStyle.Header1: - case I.TextStyle.Header2: - case I.TextStyle.Header3: { - inner = text; - break; - }; - - case I.TextStyle.Checkbox: { - inner = ( - <React.Fragment> - <Icon className={[ 'check', (checked ? 'active' : '') ].join(' ')} /> - {line} - </React.Fragment> - ); - break; - }; + case I.TextStyle.Checkbox: { + inner = ( + <React.Fragment> + <Icon className={[ 'check', (checked ? 'active' : '') ].join(' ')} /> + {line} + </React.Fragment> + ); + break; + }; - case I.TextStyle.Quote: { - inner = ( - <React.Fragment> - <Icon className="hl" /> - {line} - </React.Fragment> - ); - break; - }; + case I.TextStyle.Quote: { + inner = ( + <React.Fragment> + <Icon className="hl" /> + {line} + </React.Fragment> + ); + break; + }; - case I.TextStyle.Bulleted: { - inner = ( - <React.Fragment> - <Icon className="bullet" /> - {line} - </React.Fragment> - ); - break; - }; + case I.TextStyle.Bulleted: { + inner = ( + <React.Fragment> + <Icon className="bullet" /> + {line} + </React.Fragment> + ); + break; + }; - case I.TextStyle.Toggle: { - inner = ( - <React.Fragment> - <Icon className="toggle" /> - {line} - </React.Fragment> - ); - break; - }; + case I.TextStyle.Toggle: { + inner = ( + <React.Fragment> + <Icon className="toggle" /> + {line} + </React.Fragment> + ); + break; + }; - case I.TextStyle.Numbered: { - inner = ( - <React.Fragment> - <div id={'marker-' + item.id} className="number" /> - {line} - </React.Fragment> - ); - break; - }; + case I.TextStyle.Numbered: { + inner = ( + <React.Fragment> + <div id={'marker-' + item.id} className="number" /> + {line} + </React.Fragment> + ); + break; }; - break; }; + break; + }; - case I.BlockType.Layout: { - if (style == I.LayoutStyle.Row) { - isRow = true; - }; - break; + case I.BlockType.Layout: { + if (style == I.LayoutStyle.Row) { + isRow = true; }; + break; + }; - case I.BlockType.Relation: { - inner = ( - <React.Fragment> - {line} - {line} - </React.Fragment> - ); + case I.BlockType.Relation: { + inner = ( + <React.Fragment> + {line} + {line} + </React.Fragment> + ); + break; + }; + + case I.BlockType.File: { + if ([ I.FileState.Empty, I.FileState.Error ].indexOf(content.state) >= 0) { break; }; - case I.BlockType.File: { - if ([ I.FileState.Empty, I.FileState.Error ].indexOf(content.state) >= 0) { + switch (content.type) { + default: + case I.FileType.File: { + bullet = <div className={[ 'bullet', 'bgColor', 'bgColor-' + Colors[c] ].join(' ')} />; + inner = ( + <React.Fragment> + <Icon className="color" inner={bullet} /> + {line} + </React.Fragment> + ); + + c++; + if (c > Colors.length - 1) { + c = 0; + }; break; }; - switch (content.type) { - default: - case I.FileType.File: { - bullet = <div className={[ 'bullet', 'bgColor', 'bgColor-' + Colors[c] ].join(' ')} />; - inner = ( - <React.Fragment> - <Icon className="color" inner={bullet} /> - {line} - </React.Fragment> - ); - - c++; - if (c > Colors.length - 1) { - c = 0; - }; - break; - }; - - case I.FileType.Image: { - const css: any = {}; + case I.FileType.Image: { + const css: any = {}; - if (fields.width) { - css.width = (fields.width * 100) + '%'; - }; - - inner = <img className="media" src={S.Common.imageUrl(targetObjectId, J.Size.image)} style={css} />; - break; - }; - - case I.FileType.Video: { - break; + if (fields.width) { + css.width = (fields.width * 100) + '%'; }; + inner = <img className="media" src={S.Common.imageUrl(targetObjectId, J.Size.image)} style={css} />; + break; }; - break; - }; - case I.BlockType.Link: { - bullet = <div className={[ 'bullet', 'bgColor', 'bgColor-' + Colors[c] ].join(' ')} />; - inner = ( - <React.Fragment> - <Icon className="color" inner={bullet} /> - {line} - </React.Fragment> - ); - - c++; - if (c > Colors.length - 1) { - c = 0; - }; - break; - }; - - case I.BlockType.Bookmark: { - if (!content.url) { + case I.FileType.Video: { break; }; - bullet = <div className={[ 'bullet', 'bgColor', 'bgColor-grey' ].join(' ')} />; - inner = ( - <div className="bookmark"> - <div className="side left"> - <div className="name"> - <div className="line odd" /> - </div> - - <div className="descr"> - <div className="line even" /> - <div className="line odd" /> - </div> - - <div className="url"> - <Icon className="color" inner={bullet} /> - <div className="line even" /> - </div> - </div> - <div className="side right" style={{ backgroundImage: `url("${S.Common.imageUrl(content.imageHash, 170)}")` }} /> - </div> - ); - break; }; + break; }; - return ( - <div className={cn.join(' ')} style={item.css}> - {inner ? ( - <div className="inner"> - {inner} - </div> - ) : ''} + case I.BlockType.Link: { + bullet = <div className={[ 'bullet', 'bgColor', 'bgColor-' + Colors[c] ].join(' ')} />; + inner = ( + <React.Fragment> + <Icon className="color" inner={bullet} /> + {line} + </React.Fragment> + ); - {length ? ( - <div className="children"> - {childBlocks.map((child: any, i: number) => { - const css: any = {}; - const cn = [ n % 2 == 0 ? 'even' : 'odd' ]; + c++; + if (c > Colors.length - 1) { + c = 0; + }; + break; + }; - if (i == 0) { - cn.push('first'); - }; + case I.BlockType.Bookmark: { + if (!content.url) { + break; + }; - if (i == childBlocks.length - 1) { - cn.push('last'); - }; + bullet = <div className={[ 'bullet', 'bgColor', 'bgColor-grey' ].join(' ')} />; + inner = ( + <div className="bookmark"> + <div className="side left"> + <div className="name"> + <div className="line odd" /> + </div> - if (isRow) { - css.width = (child.fields.width || 1 / length ) * 100 + '%'; - }; + <div className="descr"> + <div className="line even" /> + <div className="line odd" /> + </div> - n++; - n = this.checkNumber(child, n); - return <Block key={child.id} {...child} className={cn.join(' ')} css={css} />; - })} + <div className="url"> + <Icon className="color" inner={bullet} /> + <div className="line even" /> + </div> </div> - ) : ''} - </div> - ); + <div className="side right" style={{ backgroundImage: `url("${S.Common.imageUrl(content.imageHash, 170)}")` }} /> + </div> + ); + break; + }; }; return ( - <div - ref={node => this.node = node} - className={cn.join(' ')} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} - > - {loading ? <Loader /> : ( - <React.Fragment> - {onMore ? <div id={`item-more-${rootId}`} className="moreWrapper" onClick={onMore}><Icon className="more" /></div> : ''} - - <div onClick={onClick}> - <div className="scroller"> - {object.templateIsBundled ? <Icon className="logo" tooltip={translate('previewObjectTemplateIsBundled')} /> : ''} - - {(coverType != I.CoverType.None) && coverId ? <Cover type={coverType} id={coverId} image={coverId} className={coverId} x={coverX} y={coverY} scale={coverScale} withScale={true} /> : ''} - - <div className="heading"> - <IconObject size={size} object={object} /> - <div className="name">{name}</div> - <div className="featured" /> - </div> + <div className={cn.join(' ')} style={item.css}> + {inner ? ( + <div className="inner"> + {inner} + </div> + ) : ''} + + {length ? ( + <div className="children"> + {childBlocks.map((child: any, i: number) => { + const css: any = {}; + const cn = [ n % 2 == 0 ? 'even' : 'odd' ]; - <div className="blocks"> - {childBlocks.map((child: any, i: number) => { - const cn = [ n % 2 == 0 ? 'even' : 'odd' ]; + if (i == 0) { + cn.push('first'); + }; - if (i == 0) { - cn.push('first'); - }; + if (i == childBlocks.length - 1) { + cn.push('last'); + }; - if (i == childBlocks.length - 1) { - cn.push('last'); - }; + if (isRow) { + css.width = (child.fields.width || 1 / length ) * 100 + '%'; + }; - n++; - n = this.checkNumber(child, n); - return <Block key={child.id} className={cn.join(' ')} {...child} />; - })} - </div> - </div> - <div className="border" /> - </div> - </React.Fragment> - )} + n++; + n = checkNumber(child, n); + return <Block key={child.id} {...child} className={cn.join(' ')} css={css} />; + })} + </div> + ) : ''} </div> ); }; - componentDidMount () { - this._isMounted = true; - this.load(); - this.rebind(); - }; - - componentDidUpdate () { - const { rootId, position } = this.props; - const contextId = this.getRootId(); - const root = S.Block.wrapTree(contextId, rootId); - - this.load(); - - if (root) { - S.Block.updateNumbersTree([ root ]); - }; - - if (position) { - position(); - }; - }; - - componentWillUnmount () { - this._isMounted = false; - this.unbind(); - - Action.pageClose(this.getRootId(), false); - }; - - rebind () { - const { rootId } = this.props; - - this.unbind(); - $(window).on(`updatePreviewObject.${rootId}`, () => this.update()); + const rebind = () => { + unbind(); + $(window).on(`updatePreviewObject.${rootId}`, () => update()); }; - unbind () { - const { rootId } = this.props; - + const unbind = () => { $(window).off(`updatePreviewObject.${rootId}`); }; - onMouseEnter (e: any) { - const { onMouseEnter } = this.props; - + const onMouseEnterHandler = (e: any) => { if (onMouseEnter) { onMouseEnter(e); }; - $(this.node).addClass('hover'); + $(nodeRef.current).addClass('hover'); }; - onMouseLeave (e: any) { - const { onMouseLeave } = this.props; - + const onMouseLeaveHandler = (e: any) => { if (onMouseLeave) { onMouseLeave(e); }; - $(this.node).removeClass('hover'); + $(nodeRef.current).removeClass('hover'); }; - load () { - const { loading } = this.state; - const { rootId, setObject } = this.props; - const contextId = this.getRootId(); + const load = () => { + const contextId = getRootId(); - if (!this._isMounted || loading || (this.id == rootId)) { + if (isLoading || (idRef.current == rootId)) { return; }; - this.id = rootId; - this.setState({ loading: true }); + idRef.current = rootId; + setIsLoading(true); C.ObjectShow(rootId, 'preview', U.Router.getRouteSpaceId(), () => { - if (!this._isMounted) { - return; - }; - - this.setState({ loading: false }); + setIsLoading(false); if (setObject) { setObject(S.Detail.get(contextId, rootId, [])); @@ -458,7 +312,7 @@ const PreviewObject = observer(class PreviewObject extends React.Component<Props }); }; - checkNumber (block: I.Block, n: number) { + const checkNumber = (block: I.Block, n: number) => { const isText = block.type == I.BlockType.Text; if ([ I.BlockType.Layout ].includes(block.type)) { n = 0; @@ -469,20 +323,139 @@ const PreviewObject = observer(class PreviewObject extends React.Component<Props return n; }; - getRootId () { - const { rootId } = this.props; + const getRootId = () => { return [ rootId, 'preview' ].join('-'); }; - update () { - this.id = ''; - this.load(); + const update = () => { + idRef.current = ''; + load(); + }; + + const contextId = getRootId(); + const check = U.Data.checkDetails(contextId, rootId); + const object = S.Detail.get(contextId, rootId); + const { name, description, coverType, coverId, coverX, coverY, coverScale, iconImage } = object; + const childBlocks = S.Block.getChildren(contextId, rootId, it => !it.isLayoutHeader()).slice(0, 10); + const isTask = U.Object.isTaskLayout(object.layout); + const isBookmark = U.Object.isBookmarkLayout(object.layou); + const cn = [ 'previewObject' , check.className, className ]; + + switch (size) { + case I.PreviewSize.Large: { + iconSize = 48; + cnPreviewSize = 'large'; + break; + }; + + case I.PreviewSize.Medium: { + iconSize = 40; + cnPreviewSize = 'medium'; + break; + }; + + default: + case I.PreviewSize.Small: { + iconSize = 32; + cnPreviewSize = 'small'; + break; + }; }; - setActive (v: boolean) { - $(this.node).toggleClass('active', v); + cn.push(cnPreviewSize); + + if (isTask || isBookmark) { + iconSize = 16; + + if (size == I.PreviewSize.Small) { + iconSize = 14; + }; }; -}); + useEffect(() => { + load(); + rebind(); + + const contextId = getRootId(); + const root = S.Block.wrapTree(contextId, rootId); + + if (root) { + S.Block.updateNumbersTree([ root ]); + }; + + if (position) { + position(); + }; + + return () => { + unbind(); + + Action.pageClose(getRootId(), false); + }; + }); + + return ( + <div + ref={nodeRef} + className={cn.join(' ')} + onMouseEnter={onMouseEnterHandler} + onMouseLeave={onMouseLeaveHandler} + > + {isLoading ? <Loader /> : ( + <> + {onMore ? ( + <div id={`item-more-${rootId}`} className="moreWrapper" onClick={onMore}> + <Icon /> + </div> + ) : ''} + + <div onClick={onClick}> + <div className="scroller"> + {object.templateIsBundled ? <Icon className="logo" tooltip={translate('previewObjectTemplateIsBundled')} /> : ''} + + {(coverType != I.CoverType.None) && coverId ? ( + <Cover + type={coverType} + id={coverId} + image={coverId} + className={coverId} + x={coverX} + y={coverY} + scale={coverScale} + withScale={true} + /> + ) : ''} + + <div className="heading"> + <IconObject size={iconSize} object={object} /> + <div className="name">{name}</div> + <div className="featured" /> + </div> + + <div className="blocks"> + {childBlocks.map((child: any, i: number) => { + const cn = [ n % 2 == 0 ? 'even' : 'odd' ]; + + if (i == 0) { + cn.push('first'); + }; + + if (i == childBlocks.length - 1) { + cn.push('last'); + }; + + n++; + n = checkNumber(child, n); + return <Block key={child.id} className={cn.join(' ')} {...child} />; + })} + </div> + </div> + <div className="border" /> + </div> + </> + )} + </div> + ); +})); export default PreviewObject; \ No newline at end of file diff --git a/src/ts/component/selection/provider.tsx b/src/ts/component/selection/provider.tsx index 39a0ac7b56..77a28f95f0 100644 --- a/src/ts/component/selection/provider.tsx +++ b/src/ts/component/selection/provider.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect, useImperativeHandle } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; @@ -9,95 +9,64 @@ interface Props { children?: React.ReactNode; }; -const THRESHOLD = 10; - -const SelectionProvider = observer(class SelectionProvider extends React.Component<Props> { - - _isMounted = false; - x = 0; - y = 0; - dir = 0; - focused = ''; - range: any = null; - nodes: any[] = []; - top = 0; - startTop = 0; - containerOffset = null; - frame = 0; - hasMoved = false; - isSelecting = false; - isPopup = false; - rootId = ''; - rect: any = null; - - cacheNodeMap: Map<string, any> = new Map(); - cacheChildrenMap: Map<string, string[]> = new Map(); - - ids: Map<string, string[]> = new Map(); - idsOnStart: Map<string, string[]> = new Map(); - - constructor (props: Props) { - super(props); - - this.onMouseDown = this.onMouseDown.bind(this); - this.onMouseMove = this.onMouseMove.bind(this); - this.onMouseUp = this.onMouseUp.bind(this); - }; - - render () { - const { list } = S.Popup; - const { children } = this.props; - const length = list.length; - - return ( - <div - id="selection" - className="selection" - onMouseDown={this.onMouseDown} - > - <div id="selection-rect" /> - {children} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - this.rect = $('#selection-rect'); - this.rebind(); - }; - - componentDidUpdate (): void { - this.rebind(); - }; - - componentWillUnmount () { - this._isMounted = false; - this.unbind(); - }; +interface SelectionRefProps { + get(type: I.SelectType): string[]; + getForClick(id: string, withChildren: boolean, save: boolean): string[]; + set(type: I.SelectType, ids: string[]): void; + clear(): void; + scrollToElement(id: string, dir: number): void; + renderSelection(): void; + isSelecting(): boolean; + setIsSelecting(v: boolean): void; + hide(): void; +}; - rebind () { - this.unbind(); - U.Common.getScrollContainer(keyboard.isPopup()).on('scroll.selection', e => this.onScroll(e)); - }; +const THRESHOLD = 10; - unbind () { - this.unbindMouse(); - this.unbindKeyboard(); +const SelectionProvider = observer(forwardRef<SelectionRefProps, Props>((props, ref) => { + + const x = useRef(0); + const y = useRef(0); + const focusedId = useRef(''); + const range = useRef(null); + const nodes = useRef([]); + const top = useRef(0); + const startTop = useRef(0); + const containerOffset = useRef(null); + const frame = useRef(0); + const hasMoved = useRef(false); + const isSelecting = useRef(false); + const cacheNodeMap = useRef(new Map()); + const cacheChildrenMap = useRef(new Map()); + const ids = useRef(new Map()); + const idsOnStart = useRef(new Map()); + const { list } = S.Popup; + const { children } = props; + const length = list.length; + const rectRef = useRef(null); + + const rebind = () => { + unbind(); + U.Common.getScrollContainer(keyboard.isPopup()).on('scroll.selection', e => onScroll(e)); + }; + + const unbind = () => { + unbindMouse(); + unbindKeyboard(); }; - unbindMouse () { + const unbindMouse = () => { $(window).off('mousemove.selection mouseup.selection'); }; - unbindKeyboard () { + const unbindKeyboard = () => { const isPopup = keyboard.isPopup(); $(window).off('keydown.selection keyup.selection'); U.Common.getScrollContainer(isPopup).off('scroll.selection'); }; - scrollToElement (id: string, dir: number) { + const scrollToElement = (id: string, dir: number) => { const isPopup = keyboard.isPopup(); if (dir > 0) { @@ -121,12 +90,9 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone }; }; - onMouseDown (e: any) { - const isPopup = keyboard.isPopup(); - + const onMouseDown = (e: any) => { if ( e.button || - !this._isMounted || S.Menu.isOpen('', '', [ 'onboarding', 'searchText' ]) || S.Popup.isOpen('', [ 'page' ]) ) { @@ -134,59 +100,59 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone }; if (keyboard.isSelectionDisabled) { - this.hide(); + hide(); return; }; + const isPopup = keyboard.isPopup(); const { focused } = focus.state; const win = $(window); const container = U.Common.getScrollContainer(isPopup); - - this.rect.toggleClass('fromPopup', isPopup); - this.rootId = keyboard.getRootId(); - this.isPopup = isPopup; - this.x = e.pageX; - this.y = e.pageY; - this.hasMoved = false; - this.focused = focused; - this.top = this.startTop = container.scrollTop(); - this.idsOnStart = new Map(this.ids); - this.cacheChildrenMap.clear(); - this.cacheNodeMap.clear(); - this.setIsSelecting(true); + const rect = $(rectRef.current); + + rect.toggleClass('fromPopup', isPopup); + x.current = e.pageX; + y.current = e.pageY; + hasMoved.current = false; + focusedId.current = focused; + top.current = startTop.current = container.scrollTop(); + idsOnStart.current = new Map(ids.current); + cacheChildrenMap.current.clear(); + cacheNodeMap.current.clear(); + setIsSelecting(true); keyboard.disablePreview(true); if (isPopup && container.length) { - this.containerOffset = container.offset(); - this.x -= this.containerOffset.left; - this.y -= this.containerOffset.top - this.top; + containerOffset.current = container.offset(); + x.current -= containerOffset.current.left; + y.current -= containerOffset.current.top - top.current; }; - this.initNodes(); + initNodes(); if (e.shiftKey && focused) { const target = $(e.target).closest('.selectionTarget'); const type = target.attr('data-type') as I.SelectType; const id = target.attr('data-id'); - const ids = this.get(type); + const ids = get(type); if (!ids.length && (id != focused)) { - this.set(type, ids.concat([ focused ])); + set(type, ids.concat([ focused ])); }; }; scrollOnMove.onMouseDown(e, isPopup); - this.unbindMouse(); + unbindMouse(); - win.on(`mousemove.selection`, e => this.onMouseMove(e)); - win.on(`blur.selection mouseup.selection`, e => this.onMouseUp(e)); + win.on(`mousemove.selection`, e => onMouseMove(e)); + win.on(`blur.selection mouseup.selection`, e => onMouseUp(e)); }; - initNodes () { - const nodes = this.getPageContainer().find('.selectionTarget'); + const initNodes = () => { + const list = getPageContainer().find('.selectionTarget'); - nodes.each((i: number, item: any) => { + list.each((i: number, item: any) => { item = $(item); const id = item.attr('data-id'); @@ -194,83 +160,75 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone return; }; - const node = { - id, - type: item.attr('data-type'), - obj: item, - }; + const type = item.attr('data-type'); + const node = { id, type, obj: item }; + + nodes.current.push(node); - this.nodes.push(node); - this.cacheNode(node); - this.cacheChildrenIds(id); + cacheNode(node); + cacheChildrenIds(id); }); }; - onMouseMove (e: any) { - if (!this._isMounted) { - return; - }; - + const onMouseMove = (e: any) => { if (keyboard.isSelectionDisabled) { - this.hide(); + hide(); return; }; - const rect = this.getRect(this.x, this.y, e.pageX, e.pageY); + const isPopup = keyboard.isPopup(); + const rect = getRect(x.current, y.current, e.pageX, e.pageY); if ((rect.width < THRESHOLD) && (rect.height < THRESHOLD)) { return; }; - this.top = U.Common.getScrollContainer(this.isPopup).scrollTop(); - this.checkNodes(e); - this.drawRect(e.pageX, e.pageY); - this.hasMoved = true; + top.current = U.Common.getScrollContainer(isPopup).scrollTop(); + checkNodes(e); + drawRect(e.pageX, e.pageY); + hasMoved.current = true; scrollOnMove.onMouseMove(e.clientX, e.clientY); }; - onScroll (e: any) { - if (!this.isSelecting || !this.hasMoved) { + const onScroll = (e: any) => { + if (!isSelecting.current || !hasMoved.current) { return; }; - const container = U.Common.getScrollContainer(this.isPopup); - const top = container.scrollTop(); - const d = top > this.top ? 1 : -1; - const x = keyboard.mouse.page.x; - const y = keyboard.mouse.page.y + (!this.isPopup ? Math.abs(top - this.top) * d : 0); - const rect = this.getRect(this.x, this.y, x, y); + const isPopup = keyboard.isPopup(); + const container = U.Common.getScrollContainer(isPopup); + const st = container.scrollTop(); + const d = st > top.current ? 1 : -1; + const cx = keyboard.mouse.page.x; + const cy = keyboard.mouse.page.y + (!isPopup ? Math.abs(st - top.current) * d : 0); + const rect = getRect(x.current, y.current, cx, cy); const wh = container.height(); if ((rect.width < THRESHOLD) && (rect.height < THRESHOLD)) { return; }; - if (Math.abs(top - this.startTop) >= wh / 2) { - this.initNodes(); - this.startTop = top; + if (Math.abs(st - startTop.current) >= wh / 2) { + initNodes(); + startTop.current = st; } else { - this.nodes.forEach(it => this.cacheNode(it)); + nodes.current.forEach(it => cacheNode(it)); }; - this.checkNodes({ ...e, pageX: x, pageY: y }); - this.drawRect(x, y); + checkNodes({ ...e, pageX: cx, pageY: cy }); + drawRect(cx, cy); scrollOnMove.onMouseMove(keyboard.mouse.client.x, keyboard.mouse.client.y); - this.hasMoved = true; + hasMoved.current = true; }; - onMouseUp (e: any) { - if (!this._isMounted) { - return; - }; - - if (!this.hasMoved) { + const onMouseUp = (e: any) => { + if (!hasMoved.current) { if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { if (!keyboard.isSelectionClearDisabled) { - this.initIds(); - this.renderSelection(); + initIds(); + renderSelection(); $(window).trigger('selectionClear'); }; @@ -278,23 +236,25 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone let needCheck = false; if (e.ctrlKey || e.metaKey) { for (const i in I.SelectType) { - const idsOnStart = this.idsOnStart.get(I.SelectType[i]) || []; - needCheck = needCheck || Boolean(idsOnStart.length); + const list = idsOnStart.current.get(I.SelectType[i]) || []; + + needCheck = needCheck || Boolean(list.length); }; }; if (needCheck) { - this.checkNodes(e); + checkNodes(e); }; - const ids = this.get(I.SelectType.Block, true); + const rootId = keyboard.getRootId(); + const ids = get(I.SelectType.Block, true); const target = $(e.target).closest('.selectionTarget'); const id = target.attr('data-id'); const type = target.attr('data-type'); if (target.length && e.shiftKey && ids.length && (type == I.SelectType.Block)) { - const first = ids.length ? ids[0] : this.focused; - const tree = S.Block.getTree(this.rootId, S.Block.getBlocks(this.rootId)); + const first = ids.length ? ids[0] : focusedId.current; + const tree = S.Block.getTree(rootId, S.Block.getBlocks(rootId)); const list = S.Block.unwrapTree(tree); const idxStart = list.findIndex(it => it.id == first); const idxEnd = list.findIndex(it => it.id == id); @@ -305,7 +265,7 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone filter(it => it.isSelectable()). map(it => it.id); - this.set(type, ids.concat(slice)); + set(type, ids.concat(slice)); }; }; } else { @@ -314,39 +274,43 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone scrollOnMove.onMouseUp(e); - const ids = this.ids.get(I.SelectType.Block) || []; + const list = ids.current.get(I.SelectType.Block) || []; - if (ids.length) { + if (list.length) { focus.clear(true); S.Menu.close('blockContext'); }; - this.clearState(); + clearState(); }; - initIds () { + const initIds = () => { for (const i in I.SelectType) { - this.ids.set(I.SelectType[i], []); + ids.current.set(I.SelectType[i], []); }; }; - drawRect (x: number, y: number) { - if (!this.nodes.length) { + const drawRect = (dx: number, dy: number) => { + if (!nodes.current.length) { return; }; - if (U.Common.getSelectionRange()) { - this.rect.hide(); - } else { - const x1 = this.x + (this.containerOffset ? this.containerOffset.left : 0); - const y1 = this.y + (this.containerOffset ? this.containerOffset.top - this.top : 0); - const rect = this.getRect(x1, y1, x, y); + let ox = 0; + let oy = 0; - this.rect.show().css({ transform: `translate3d(${rect.x}px, ${rect.y}px, 0px)`, width: rect.width, height: rect.height }); + if (containerOffset.current) { + ox = containerOffset.current.left; + oy = containerOffset.current.top - top.current; }; + + const x1 = x.current + ox; + const y1 = y.current + oy; + const rect = getRect(x1, y1, dx, dy); + + $(rectRef.current).show().css({ transform: `translate3d(${rect.x}px, ${rect.y}px, 0px)`, width: rect.width, height: rect.height }); }; - getRect (x1: number, y1: number, x2: number, y2: number) { + const getRect = (x1: number, y1: number, x2: number, y2: number) => { return { x: Math.min(x1, x2), y: Math.min(y1, y2), @@ -355,204 +319,201 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone }; }; - cacheNode (node: any): { x: number; y: number; width: number; height: number; } { + const cacheNode = (node: any): { x: number; y: number; width: number; height: number; } => { if (!node.id) { return { x: 0, y: 0, width: 0, height: 0 }; }; - let cache = this.cacheNodeMap.get(node.id); + let cache = cacheNodeMap.current.get(node.id); if (cache) { return cache; }; const offset = node.obj.offset(); const rect = node.obj.get(0).getBoundingClientRect() as DOMRect; - const { x, y } = this.recalcCoords(offset.left, offset.top); + const { x, y } = recalcCoords(offset.left, offset.top); cache = { x, y, width: rect.width, height: rect.height }; - this.cacheNodeMap.set(node.id, cache); + cacheNodeMap.current.set(node.id, cache); return cache; }; - checkEachNode (e: any, type: I.SelectType, rect: any, node: any, ids: string[]): string[] { - const cache = this.cacheNode(node); + const checkEachNode = (e: any, type: I.SelectType, rect: any, node: any, list: string[]): string[] => { + const cache = cacheNode(node); + if (!cache || !U.Common.rectsCollide(rect, cache)) { - return ids; + return list; }; if (e.ctrlKey || e.metaKey) { - ids = (this.idsOnStart.get(type) || []).includes(node.id) ? ids.filter(it => it != node.id) : ids.concat(node.id); + list = (idsOnStart.current.get(type) || []).includes(node.id) ? list.filter(it => it != node.id) : list.concat(node.id); } else if (e.altKey) { - ids = ids.filter(it => it != node.id); + list = list.filter(it => it != node.id); } else { - ids.push(node.id); + list.push(node.id); }; - return ids; + + return list; }; - checkNodes (e: any) { - if (!this._isMounted) { - return; - }; - - const { focused, range } = focus.state; - const { x, y } = this.recalcCoords(e.pageX, e.pageY); - const rect = U.Common.objectCopy(this.getRect(this.x, this.y, x, y)); + const checkNodes = (e: any) => { + const recalc = recalcCoords(e.pageX, e.pageY); + const rect = U.Common.objectCopy(getRect(x.current, y.current, recalc.x, recalc.y)); if (!e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { - this.initIds(); + initIds(); }; - const ids = {}; + const list = {}; + for (const i in I.SelectType) { const type = I.SelectType[i]; - ids[type] = this.get(type, false); + list[type] = get(type, false); - this.nodes.filter(it => it.type == type).forEach(item => { - ids[type] = this.checkEachNode(e, type, rect, item, ids[type]); + nodes.current.filter(it => it.type == type).forEach(item => { + list[type] = checkEachNode(e, type, rect, item, list[type]); }); - this.ids.set(type, ids[type]); + ids.current.set(type, list[type]); }; - - const length = (ids[I.SelectType.Block] || []).length; - if (length > 0) { - if ((length == 1) && !(e.ctrlKey || e.metaKey)) { - const selected = $(`#block-${ids[I.SelectType.Block][0]}`); - const value = selected.find('#value'); + const length = (list[I.SelectType.Block] || []).length; - if (!value.length) { - return; - }; + if (!length) { + renderSelection(); + return; + }; - const el = value.get(0) as Element; - const range = getRange(el); - - if (!this.range) { - this.focused = selected.attr('data-id'); - this.range = range; - }; + if ((length == 1) && !(e.ctrlKey || e.metaKey)) { + const selected = $(`#block-${list[I.SelectType.Block][0]}`); + const value = selected.find('#value'); - if (this.range) { - if (this.range.end) { - this.initIds(); - }; - - if (!range) { - focus.set(this.focused, { from: this.range.start, to: this.range.end }); - focus.apply(); - }; - }; - } else { - if (focused && range.to) { - focus.clear(false); + if (!value.length) { + return; + }; + + const el = value.get(0) as Element; + const rc = getRange(el); + + if (!range.current) { + focusedId.current = selected.attr('data-id'); + range.current = rc; + }; + + if (range.current) { + if (range.current.end) { + initIds(); }; - keyboard.setFocus(false); - window.getSelection().empty(); - window.focus(); + if (!rc) { + focus.set(focusedId.current, { from: range.current.start, to: range.current.end }); + focus.apply(); + }; + }; + } else { + const { focused, range } = focus.state; + + if (focused && range.to) { + focus.clear(false); }; + + keyboard.setFocus(false); + window.getSelection().empty(); + window.focus(); }; - this.renderSelection(); + renderSelection(); }; - hide () { - this.rect.hide(); - this.unbindMouse(); + const hide = () => { + $(rectRef.current).hide(); + unbindMouse(); }; - clear () { - if (!this._isMounted) { - return; - }; - - this.initIds(); - this.renderSelection(); - this.clearState(); + const clear = () => { + initIds(); + renderSelection(); + clearState(); $(window).trigger('selectionClear'); }; - clearState () { + const clearState = () => { keyboard.disablePreview(false); - this.hide(); - this.setIsSelecting(false); - this.cacheNodeMap.clear(); - this.focused = ''; - this.range = null; - this.containerOffset = null; - this.isPopup = false; - this.rootId = ''; - this.nodes = []; + hide(); + setIsSelecting(false); + cacheNodeMap.current.clear(); + focusedId.current = ''; + nodes.current = []; + range.current = null; + containerOffset.current = null; }; - set (type: I.SelectType, ids: string[]) { - this.ids.set(type, U.Common.arrayUnique(ids || [])); - this.renderSelection(); + const set = (type: I.SelectType, list: string[]) => { + ids.current.set(type, U.Common.arrayUnique(list || [])); + renderSelection(); }; - get (type: I.SelectType, withChildren?: boolean): string[] { - let ids = [ ...new Set(this.ids.get(type) || []) ]; + const get = (type: I.SelectType, withChildren?: boolean): string[] => { + let list: string[] = [ ...new Set(ids.current.get(type) || []) ] as string[]; - if (!ids.length) { + if (!list.length) { return []; }; if (type != I.SelectType.Block) { - return ids; + return list; }; let ret = []; if (withChildren) { - ids.forEach(id => { + list.forEach(id => { ret.push(id); - ret = ret.concat(this.getChildrenIds(id)); + ret = ret.concat(getChildrenIds(id)); }); } else { let childrenIds = []; - ids.forEach(id => { - childrenIds = childrenIds.concat(this.getChildrenIds(id)); + list.forEach(id => { + childrenIds = childrenIds.concat(getChildrenIds(id)); }); if (childrenIds.length) { - ids = ids.filter(it => !childrenIds.includes(it)); + list = list.filter(it => !childrenIds.includes(it)); }; - ret = ids; + ret = list; }; return ret; }; - /* - Used to click and set selection automatically in block menu for example - */ - getForClick (id: string, withChildren: boolean, save: boolean): string[] { - let ids: string[] = this.get(I.SelectType.Block, withChildren); + // Used to click and set selection automatically in block menu for example + const getForClick = (id: string, withChildren: boolean, save: boolean): string[] => { + let ids: string[] = get(I.SelectType.Block, withChildren); if (id && !ids.includes(id)) { - this.clear(); - this.set(I.SelectType.Block, [ id ]); + clear(); + set(I.SelectType.Block, [ id ]); - ids = this.get(I.SelectType.Block, withChildren); + ids = get(I.SelectType.Block, withChildren); if (!save) { - this.clear(); + clear(); }; }; return ids; }; - cacheChildrenIds (id: string): string[] { - const block = S.Block.getLeaf(this.rootId, id); + const cacheChildrenIds = (id: string): string[] => { + const rootId = keyboard.getRootId(); + const block = S.Block.getLeaf(rootId, id); + if (!block) { return []; }; @@ -560,51 +521,51 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone let ids = []; if (!block.isTable()) { - const childrenIds = S.Block.getChildrenIds(this.rootId, id); + const childrenIds = S.Block.getChildrenIds(rootId, id); for (const childId of childrenIds) { ids.push(childId); - ids = ids.concat(this.cacheChildrenIds(childId)); + ids = ids.concat(cacheChildrenIds(childId)); }; }; - this.cacheChildrenMap.set(id, [ ...ids ]); + cacheChildrenMap.current.set(id, [ ...ids ]); return ids; }; - getChildrenIds (id: string) { - return this.cacheChildrenMap.get(id) || []; + const getChildrenIds = (id: string) => { + return cacheChildrenMap.current.get(id) || []; }; - getPageContainer () { + const getPageContainer = () => { return $(U.Common.getCellContainer(keyboard.isPopup() ? 'popup' : 'page')); }; - renderSelection () { - if (!this._isMounted) { - return; - }; + const renderSelection = () => { + const container = getPageContainer(); - if (this.frame) { - raf.cancel(this.frame); + if (frame.current) { + raf.cancel(frame.current); }; - raf(() => { + frame.current = raf(() => { $('.isSelectionSelected').removeClass('isSelectionSelected'); for (const i in I.SelectType) { const type = I.SelectType[i]; - const ids = this.get(type, true); + const list = get(type, true); - for (const id of ids) { - $(`#selectionTarget-${id}`).addClass('isSelectionSelected'); + for (const id of list) { + container.find(`#selectionTarget-${id}`).addClass('isSelectionSelected'); if (type == I.SelectType.Block) { - $(`#block-${id}`).addClass('isSelectionSelected'); + container.find(`#block-${id}`).addClass('isSelectionSelected'); - const childrenIds = this.getChildrenIds(id); + const childrenIds = getChildrenIds(id); if (childrenIds.length) { - childrenIds.forEach(childId => $(`#block-${childId}`).addClass('isSelectionSelected')); + childrenIds.forEach(childId => { + container.find(`#block-${childId}`).addClass('isSelectionSelected'); + }); }; }; }; @@ -612,22 +573,54 @@ const SelectionProvider = observer(class SelectionProvider extends React.Compone }); }; - recalcCoords (x: number, y: number): { x: number, y: number } { - if (this.containerOffset) { - const top = U.Common.getScrollContainer(this.isPopup).scrollTop(); - - x -= this.containerOffset.left; - y -= this.containerOffset.top - top; + const recalcCoords = (x: number, y: number): { x: number, y: number } => { + if (!containerOffset.current) { + return { x, y }; }; + const isPopup = keyboard.isPopup(); + const st = U.Common.getScrollContainer(isPopup).scrollTop(); + const { left, top } = containerOffset.current; + + x -= left; + y -= top - st; + return { x, y }; }; - setIsSelecting (v: boolean) { - this.isSelecting = v; + const setIsSelecting = (v: boolean) => { + isSelecting.current = v; $('html').toggleClass('isSelecting', v); }; - -}); + + useEffect(() => { + rebind(); + return () => unbind(); + }); + + useImperativeHandle(ref, () => ({ + get, + getForClick, + set, + clear, + scrollToElement, + renderSelection, + isSelecting: () => isSelecting.current, + setIsSelecting, + hide, + })); + + return ( + <div + id="selection" + className="selection" + onMouseDown={onMouseDown} + > + <div ref={rectRef} id="selection-rect" /> + {children} + </div> + ); + +})); export default SelectionProvider; \ No newline at end of file diff --git a/src/ts/component/selection/target.tsx b/src/ts/component/selection/target.tsx index 8b288fe919..d74cab5576 100644 --- a/src/ts/component/selection/target.tsx +++ b/src/ts/component/selection/target.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { I, U } from 'Lib'; interface Props { @@ -10,31 +10,28 @@ interface Props { onContextMenu?(e: any): void; }; -class SelectionTarget extends React.Component<Props> { - - public static defaultProps: Props = { - id: '', - type: I.SelectType.None, - style: {}, - className: '', - }; +const SelectionTarget: FC<Props> = ({ + id = '', + className = '', + type = I.SelectType.None, + children, + style = {}, + onContextMenu, +}) => { - render () { - const { id, type, children, style, className, onContextMenu } = this.props; + const cn = [ 'selectionTarget', className ]; - return ( - <div - id={`selectionTarget-${id}`} - className={`selectionTarget ${className}`} - style={style} - onContextMenu={onContextMenu} - {...U.Common.dataProps({ id, type })} - > - {children} - </div> - ); - }; - + return ( + <div + id={`selectionTarget-${id}`} + className={cn.join(' ')} + style={style} + onContextMenu={onContextMenu} + {...U.Common.dataProps({ id, type })} + > + {children} + </div> + ); }; export default SelectionTarget; \ No newline at end of file diff --git a/src/ts/component/sidebar/index.tsx b/src/ts/component/sidebar/index.tsx index dfcf171199..5f1d2cee68 100644 --- a/src/ts/component/sidebar/index.tsx +++ b/src/ts/component/sidebar/index.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; -import { Icon } from 'Component'; +import { Icon, Sync } from 'Component'; import { I, U, J, S, keyboard, Preview, sidebar } from 'Lib'; import SidebarWidget from './widget'; @@ -34,7 +34,6 @@ const Sidebar = observer(class Sidebar extends React.Component { render() { const { showVault, showObject } = S.Common; - const cn = [ 'sidebar' ]; const cmd = keyboard.cmdSymbol(); return ( @@ -48,10 +47,12 @@ const Sidebar = observer(class Sidebar extends React.Component { onContextMenu={this.onToggleContext} /> + <Sync id="sidebarSync" onClick={this.onSync} /> + <div ref={node => this.node = node} id="sidebar" - className={cn.join(' ')} + className="sidebar" > {showObject ? <SidebarObject ref={ref => this.refObject = ref} {...this.props} /> : <SidebarWidget {...this.props} ref={ref => this.refWidget = ref} />} <div className="resize-h" draggable={true} onDragStart={this.onResizeStart}> @@ -201,6 +202,17 @@ const Sidebar = observer(class Sidebar extends React.Component { U.Menu.sidebarContext('#sidebarToggle'); }; + onSync = () => { + S.Menu.closeAllForced(null, () => S.Menu.open('syncStatus', { + element: '#sidebarSync', + className: 'fixed', + classNameWrap: 'fromSidebar', + data: { + rootId: keyboard.getRootId(), + }, + })); + }; + }); export default Sidebar; \ No newline at end of file diff --git a/src/ts/component/sidebar/object.tsx b/src/ts/component/sidebar/object.tsx index 6a1cd66d0d..d125e1adc9 100644 --- a/src/ts/component/sidebar/object.tsx +++ b/src/ts/component/sidebar/object.tsx @@ -277,7 +277,7 @@ const SidebarObject = observer(class SidebarObject extends React.Component<{}, S $(window).on('keydown.sidebarObject', e => this.onKeyDown(e)); $(this.node).on('click', e => { - if (!this.refFilter || this.refFilter.isFocused) { + if (!this.refFilter || this.refFilter.isFocused()) { return; }; @@ -317,6 +317,7 @@ const SidebarObject = observer(class SidebarObject extends React.Component<{}, S const template = S.Record.getTemplateType(); const limit = this.offset + J.Constant.limit.menuRecords; const fileLayouts = [ I.ObjectLayout.File, I.ObjectLayout.Pdf ]; + const options = U.Menu.getObjectContainerSortOptions(this.type, this.sortId, this.sortType, this.orphan, this.compact); let sorts: I.Sort[] = []; let filters: I.Filter[] = [ @@ -378,7 +379,7 @@ const SidebarObject = observer(class SidebarObject extends React.Component<{}, S }; }; - if (this.orphan) { + if (this.orphan && options.find(it => it.id == I.SortId.Orphan)) { filters = filters.concat([ { relationKey: 'links', condition: I.FilterCondition.Empty, value: null }, { relationKey: 'backlinks', condition: I.FilterCondition.Empty, value: null }, @@ -713,7 +714,7 @@ const SidebarObject = observer(class SidebarObject extends React.Component<{}, S }; onKeyDown (e: any) { - if (!this.refFilter.isFocused) { + if (!this.refFilter.isFocused()) { return; }; diff --git a/src/ts/component/sidebar/widget.tsx b/src/ts/component/sidebar/widget.tsx index 6c301c30a8..9ef7130e91 100644 --- a/src/ts/component/sidebar/widget.tsx +++ b/src/ts/component/sidebar/widget.tsx @@ -9,8 +9,6 @@ type State = { previewId: string; }; -const SUB_ID = 'widgetArchive'; - const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, State> { state: State = { @@ -47,7 +45,7 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S const bodyCn = [ 'body' ]; const space = U.Space.getSpaceview(); const canWrite = U.Space.canMyParticipantWrite(); - const archived = S.Record.getRecordIds(SUB_ID, ''); + const hasShareBanner = U.Space.hasShareBanner(); let content = null; @@ -120,15 +118,11 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S ]); }; - if (U.Space.isShareBanner()) { - bodyCn.push('withShareBanner'); - }; - content = ( <React.Fragment> {space && !space._empty_ ? ( <React.Fragment> - <ShareBanner onClose={() => this.forceUpdate()} /> + {hasShareBanner ? <ShareBanner onClose={() => this.forceUpdate()} /> : ''} <DropTarget {...this.props} @@ -148,14 +142,6 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S isEditing={isEditing} /> </DropTarget> - - <Widget - block={new M.Block({ id: 'buttons', type: I.BlockType.Widget, content: { layout: I.WidgetLayout.Buttons } })} - disableContextMenu={true} - onDragStart={this.onDragStart} - onDragOver={this.onDragOver} - isEditing={isEditing} - /> </React.Fragment> ) : ''} @@ -173,26 +159,24 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S /> ))} - {archived.length ? ( - <DropTarget - {...this.props} - isTargetBottom={true} - rootId={S.Block.widgets} - id={last?.id} - dropType={I.DropType.Widget} - canDropMiddle={false} - className="lastTarget" - cacheKey="lastTarget" - > - <Button - text={translate('commonBin')} - color="" - className="widget" - icon="bin" - onClick={this.onArchive} - /> - </DropTarget> - ) : ''} + <DropTarget + {...this.props} + isTargetBottom={true} + rootId={S.Block.widgets} + id={last?.id} + dropType={I.DropType.Widget} + canDropMiddle={false} + className="lastTarget" + cacheKey="lastTarget" + > + <Button + text={translate('commonBin')} + color="" + className="widget" + icon="bin" + onClick={this.onArchive} + /> + </DropTarget> <div className="buttons"> {buttons.map(button => ( @@ -203,6 +187,10 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S ); }; + if (hasShareBanner) { + bodyCn.push('withShareBanner'); + }; + return ( <div id="containerWidget" @@ -231,14 +219,6 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S ); }; - componentDidMount (): void { - this.subscribeArchive(); - }; - - componentDidUpdate (): void { - this.subscribeArchive(); - }; - onEdit (e: any): void { e.stopPropagation(); @@ -258,7 +238,7 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S offsetY: -4, vertical: I.MenuDirection.Top, data: { - route: analytics.route.widget, + route: analytics.route.addWidget, filters: [ { relationKey: 'layout', condition: I.FilterCondition.NotIn, value: U.Object.getSystemLayouts() }, { relationKey: 'type', condition: I.FilterCondition.NotEqual, value: S.Record.getTemplateType()?.id }, @@ -395,11 +375,7 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S const body = node.find('#body'); const top = body.scrollTop(); - head.removeClass('show'); - - if (showVault && (top > 32)) { - head.addClass('show') - }; + head.toggleClass('show', showVault && (top > 32)); }; onContextMenu () { @@ -534,26 +510,6 @@ const SidebarWidget = observer(class SidebarWidget extends React.Component<{}, S }, S.Menu.getTimeout()); }; - subscribeArchive () { - const { space } = S.Common - - if (this.isSubcribed == space) { - return; - }; - - U.Data.searchSubscribe({ - subId: SUB_ID, - spaceId: space, - ignoreArchived: false, - filters: [ - { relationKey: 'isArchived', condition: I.FilterCondition.Equal, value: true }, - ], - limit: 1, - }, () => { - this.isSubcribed = space; - }); - }; - }); export default SidebarWidget; diff --git a/src/ts/component/util/cover.tsx b/src/ts/component/util/cover.tsx index dc74a6033b..7addd0eea5 100644 --- a/src/ts/component/util/cover.tsx +++ b/src/ts/component/util/cover.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC, MouseEvent } from 'react'; import { I, S, J } from 'Lib'; interface Props { @@ -11,50 +11,51 @@ interface Props { y?: number; scale?: number; withScale?: boolean; - preview?: boolean; children?: React.ReactNode; - onClick?(e: any): void; - onMouseDown?(e: any): void; + onClick?(e: MouseEvent): void; + onMouseDown?(e: MouseEvent): void; }; -class Cover extends React.Component<Props> { +const Cover: FC<Props> = ({ + id = '', + image = '', + src = '', + type = 0, + x = 0.5, + y = 0.5, + scale = 0, + withScale = false, + className = '', + onClick, + onMouseDown, + children, +}) => { - private static defaultProps = { - type: 0, - x: 0.5, - y: 0.5, - scale: 0, + const cn = [ 'cover', `type${type}`, id, className ]; + const style: any = {}; + + if ([ I.CoverType.Upload, I.CoverType.Source ].includes(type) && image) { + style.backgroundImage = `url("${S.Common.imageUrl(image, J.Size.cover)}")`; }; - render () { - const { id, image, src, type, x, y, scale, withScale, className, preview, onClick, onMouseDown, children } = this.props; - const cn = [ 'cover', 'type' + type, id ]; - const style: any = {}; - - if (className) { - cn.push(className); - }; - - if ([ I.CoverType.Upload, I.CoverType.Source ].includes(type) && image) { - style.backgroundImage = `url("${S.Common.imageUrl(image, J.Size.cover)}")`; - }; - - if (src) { - style.backgroundImage = `url("${src}")`; - }; - - if (withScale) { - style.backgroundPosition = `${Math.abs(x * 100)}% ${Math.abs(y * 100)}%`; - style.backgroundSize = ((scale + 1) * 100) + '%'; - }; - - return ( - <div className={cn.join(' ')} onClick={onClick} onMouseDown={onMouseDown} style={style}> - {children} - </div> - ); + if (src) { + style.backgroundImage = `url("${src}")`; + }; + + if (withScale) { + style.backgroundPosition = `${Math.abs(x * 100)}% ${Math.abs(y * 100)}%`; + style.backgroundSize = ((scale + 1) * 100) + '%'; }; + return ( + <div + className={cn.join(' ')} + onClick={onClick} + onMouseDown={onMouseDown} + style={style}> + {children} + </div> + ); }; export default Cover; \ No newline at end of file diff --git a/src/ts/component/util/emailCollectionForm.tsx b/src/ts/component/util/emailCollectionForm.tsx deleted file mode 100644 index b45d2138fa..0000000000 --- a/src/ts/component/util/emailCollectionForm.tsx +++ /dev/null @@ -1,292 +0,0 @@ -import * as React from 'react'; -import { Label, Checkbox, Input, Button, Icon, Pin } from 'Component'; -import { analytics, C, I, J, S, translate, U } from 'Lib'; - -interface Props { - onStepChange: () => void; - onComplete: () => void; -}; - -interface State { - countdown: number; - status: string; - statusText: string; - email: string, - subscribeNews: boolean, - subscribeTips: boolean, - pinDisabled: boolean, - showCodeSent: boolean, -}; - -class EmailCollectionForm extends React.Component<Props, State> { - - state = { - status: '', - statusText: '', - countdown: 60, - email: '', - subscribeNews: false, - subscribeTips: false, - pinDisabled: false, - showCodeSent: false, - }; - - step = 0; - node: any = null; - refCheckboxTips: any = null; - refCheckboxNews: any = null; - refEmail: any = null; - refButton: any = null; - refCode: any = null; - - interval = null; - timeout = null; - - constructor (props: Props) { - super(props); - - this.onCheck = this.onCheck.bind(this); - this.onSubmitEmail = this.onSubmitEmail.bind(this); - this.verifyEmail = this.verifyEmail.bind(this); - this.onConfirmEmailCode = this.onConfirmEmailCode.bind(this); - this.onResend = this.onResend.bind(this); - this.validateEmail = this.validateEmail.bind(this); - }; - - render () { - const { status, statusText, countdown, subscribeNews, subscribeTips, pinDisabled, showCodeSent } = this.state; - - let content = null; - let descriptionSuffix = 'Description'; - - switch (this.step) { - case 0: { - content = ( - <div className="step step0"> - <form onSubmit={this.onSubmitEmail}> - <div className="check" onClick={() => this.onCheck(this.refCheckboxTips, 'Tips')}> - <Checkbox ref={ref => this.refCheckboxTips = ref} value={false} /> {translate('emailCollectionCheckboxTipsLabel')} - </div> - <div className="check" onClick={() => this.onCheck(this.refCheckboxNews, 'Updates')}> - <Checkbox ref={ref => this.refCheckboxNews = ref} value={false} /> {translate('emailCollectionCheckboxNewsLabel')} - </div> - - <div className="inputWrapper"> - <Input ref={ref => this.refEmail = ref} onKeyUp={this.validateEmail} placeholder={translate(`emailCollectionEnterEmail`)} /> - </div> - - {status ? <div className={[ 'statusBar', status ].join(' ')}>{statusText}</div> : ''} - - <div className="buttonWrapper"> - <Button ref={ref => this.refButton = ref} onClick={this.onSubmitEmail} className="c36" text={translate('commonSignUp')} /> - </div> - </form> - </div> - ); - break; - }; - - case 1: { - content = ( - <div className="step step1"> - <Pin - ref={ref => this.refCode = ref} - pinLength={4} - isVisible={true} - onSuccess={this.onConfirmEmailCode} - readonly={pinDisabled} - /> - - {status ? <div className={[ 'statusBar', status ].join(' ')}>{statusText}</div> : ''} - - <div onClick={this.onResend} className={[ 'resend', (countdown ? 'countdown' : '') ].join(' ')}> - {showCodeSent ? translate('emailCollectionCodeSent') : translate('popupMembershipResend')} - {countdown && !showCodeSent ? U.Common.sprintf(translate('popupMembershipCountdown'), countdown) : ''} - </div> - </div> - ); - break; - }; - - case 2: { - descriptionSuffix = 'News'; - if (subscribeTips) { - descriptionSuffix = 'Tips'; - }; - if (subscribeTips && subscribeNews) { - descriptionSuffix = 'NewsAndTips'; - }; - - content = ( - <div className="step step2"> - <Icon /> - - <div className="buttonWrapper"> - <Button onClick={this.props.onComplete} className="c36" text={translate('emailCollectionGreat')} /> - </div> - </div> - ); - break; - }; - }; - - return ( - <div className="emailCollectionForm"> - <Label className="category" text={translate(`emailCollectionStep${this.step}Title`)} /> - <Label className="descr" text={translate(`emailCollectionStep${this.step}${descriptionSuffix}`)} /> - - {content} - </div> - ); - }; - - componentDidMount () { - this.refButton?.setDisabled(true); - - analytics.event('EmailCollection', { route: 'OnboardingTooltip', step: 1 }); - }; - - onCheck (ref, type) { - const val = ref.getValue(); - ref.toggle(); - - if (!val) { - analytics.event('ClickEmailCollection', { route: 'OnboardingTooltip', step: 1, type }); - }; - }; - - setStep (step: number) { - if (this.step == step) { - return; - }; - - this.step = step; - this.props.onStepChange(); - this.forceUpdate(); - - analytics.event('EmailCollection', { route: 'OnboardingTooltip', step: step + 1 }); - }; - - setStatus (status: string, statusText: string) { - this.setState({ status, statusText }); - - window.clearTimeout(this.timeout); - this.timeout = window.setTimeout(() => this.clearStatus(), 4000); - }; - - clearStatus () { - this.setState({ status: '', statusText: '' }); - }; - - validateEmail () { - this.clearStatus(); - - window.clearTimeout(this.timeout); - this.timeout = window.setTimeout(() => { - const value = this.refEmail?.getValue(); - const isValid = U.Common.checkEmail(value); - - if (value && !isValid) { - this.setStatus('error', translate('errorIncorrectEmail')); - }; - - this.refButton?.setDisabled(!isValid); - }, J.Constant.delay.keyboard); - }; - - onSubmitEmail (e: any) { - if (!this.refButton || !this.refEmail) { - return; - }; - - if (this.refButton.isDisabled()) { - return; - }; - - analytics.event('ClickEmailCollection', { route: 'OnboardingTooltip', step: 1, type: 'SignUp' }); - - this.setState({ - email: this.refEmail.getValue(), - subscribeNews: this.refCheckboxNews?.getValue(), - subscribeTips: this.refCheckboxTips?.getValue(), - }, () => { - this.refButton.setLoading(true); - this.verifyEmail(e) - }); - }; - - verifyEmail (e: any) { - e.preventDefault(); - - const { email, subscribeNews, subscribeTips } = this.state; - - C.MembershipGetVerificationEmail(email, subscribeNews, subscribeTips, true, (message) => { - this.refButton?.setLoading(false); - - if (message.error.code) { - this.setStatus('error', message.error.description); - return; - }; - - this.setStep(1); - this.startCountdown(60); - }); - }; - - onConfirmEmailCode () { - const code = this.refCode.getValue(); - - this.setState({ pinDisabled: true }); - - C.MembershipVerifyEmailCode(code, (message) => { - if (message.error.code) { - this.setStatus('error', message.error.description); - this.refCode.reset(); - this.setState({ pinDisabled: false }); - return; - }; - - this.setStep(2); - }); - }; - - onResend (e: any) { - if (this.state.countdown) { - return; - }; - - this.verifyEmail(e); - - analytics.event('ClickEmailCollection', { route: 'OnboardingTooltip', step: 2, type: 'Resend' }); - }; - - startCountdown (seconds) { - const { emailConfirmationTime } = S.Common; - - if (!emailConfirmationTime) { - S.Common.emailConfirmationTimeSet(U.Date.now()); - }; - - this.setState({ countdown: seconds, showCodeSent: true }); - - window.setTimeout(() => { - this.setState({ showCodeSent: false }); - }, 2000); - - this.interval = window.setInterval(() => { - let { countdown } = this.state; - - countdown--; - this.setState({ countdown }); - - if (!countdown) { - S.Common.emailConfirmationTimeSet(0); - window.clearInterval(this.interval); - this.interval = null; - }; - }, 1000); - }; - -}; - -export default EmailCollectionForm; diff --git a/src/ts/component/util/error.tsx b/src/ts/component/util/error.tsx index 03b2aedaeb..87fef01269 100644 --- a/src/ts/component/util/error.tsx +++ b/src/ts/component/util/error.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef } from 'react'; +import React, { FC } from 'react'; import { I, U } from 'Lib'; interface Props { @@ -8,19 +8,19 @@ interface Props { dataset?: any; }; -const Error = forwardRef<{}, Props>(({ +const Error: FC<Props> = ({ id = '', text = '', className = '', dataset = {}, -}, ref) => { - - const cn = [ 'error', className ]; +}) => { if (!text && !id) { return null; }; + const cn = [ 'error', className ]; + return ( <div id={id} @@ -30,6 +30,6 @@ const Error = forwardRef<{}, Props>(({ /> ); -}); +}; export default Error; \ No newline at end of file diff --git a/src/ts/component/util/floater.tsx b/src/ts/component/util/floater.tsx index 876b911093..65cf0d1fa5 100644 --- a/src/ts/component/util/floater.tsx +++ b/src/ts/component/util/floater.tsx @@ -1,41 +1,46 @@ import { H } from 'Lib'; -import React, { FC, useState, useEffect, ReactNode, useRef } from 'react'; +import $ from 'jquery'; +import React, { forwardRef, useState, useEffect, ReactNode, useRef, useImperativeHandle } from 'react'; import ReactDOM from 'react-dom'; interface Props { children: ReactNode; anchorEl: HTMLElement | null; - gap?: number; + offset?: number; isShown?: boolean; }; -export const Floater: FC<Props> = ({ +interface FloaterRefProps { + show(): void; + hide(): void; +}; + +const Floater = forwardRef<FloaterRefProps, Props>(({ children, anchorEl, - gap: offset = 0, - isShown = true, -}) => { - const ref = useRef<HTMLDivElement>(null); + offset = 0, +}, ref) => { + + const nodeRef = useRef<HTMLDivElement>(null); const [ position, setPosition ] = useState({ top: 0, left: 0 }); const cn = [ 'floater' ]; - if (isShown) { - cn.push('show'); - }; + useImperativeHandle(ref, () => ({ + show: () => $(nodeRef.current).addClass('show'), + hide: () => $(nodeRef.current).removeClass('show'), + })); const onMove = () => { - if (!anchorEl || !ref.current) { + if (!anchorEl || !nodeRef.current) { return; }; const anchorElRect = anchorEl.getBoundingClientRect(); - const elRect = ref.current.getBoundingClientRect(); + const elRect = nodeRef.current.getBoundingClientRect(); const { top: at, left: al, width: aw, height: ah } = anchorElRect; + const { height: eh, width: ew } = elRect; const sy = window.scrollY; - - const eh = elRect.height; - const ew = elRect.width; const nl = al + aw / 2 - ew / 2; let nt = at - eh - offset + sy; @@ -49,13 +54,14 @@ export const Floater: FC<Props> = ({ }; H.useElementMovement(anchorEl, onMove); - useEffect(() => onMove(), [ anchorEl, ref.current, isShown ]); + useEffect(() => onMove(), []); + useEffect(() => onMove(), [ anchorEl, nodeRef.current ]); return ReactDOM.createPortal( ( <div className={cn.join(' ')} - ref={ref} + ref={nodeRef} style={{ transform: `translate3d(${position.left}px, ${position.top}px, 0px)`}} > {children} @@ -63,4 +69,6 @@ export const Floater: FC<Props> = ({ ), document.getElementById('floaterContainer') ); -}; \ No newline at end of file +}); + +export default Floater; \ No newline at end of file diff --git a/src/ts/component/util/icon.tsx b/src/ts/component/util/icon.tsx index 62e2efddd7..61a6de2ed0 100644 --- a/src/ts/component/util/icon.tsx +++ b/src/ts/component/util/icon.tsx @@ -94,7 +94,7 @@ const Icon = forwardRef<HTMLDivElement, Props>(({ return ( <div - ref={nodeRef} + ref={ref || nodeRef} id={id} draggable={draggable} className={[ 'icon', className ].join(' ')} diff --git a/src/ts/component/util/iconObject.tsx b/src/ts/component/util/iconObject.tsx index c0be083c17..1f0f35f14d 100644 --- a/src/ts/component/util/iconObject.tsx +++ b/src/ts/component/util/iconObject.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { useRef, useState, useEffect, forwardRef, useImperativeHandle } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { IconEmoji } from 'Component'; @@ -35,6 +35,10 @@ interface Props { onMouseLeave?(e: any): void; }; +interface IconObjectRefProps { + setObject(object: any): void; +}; + const LAYOUTS_WITH_EMOJI_GALLERY = [ I.ObjectLayout.Page, I.ObjectLayout.Type, @@ -89,6 +93,8 @@ const FontSize = { 64: 40, 80: 64, 96: 64, + 108: 64, + 128: 64, }; const DefaultIcons = [ 'page', 'task', 'set', 'chat', 'bookmark', 'type', 'date' ]; @@ -107,245 +113,65 @@ const CheckboxTask = { }, }; -const IconObject = observer(class IconObject extends React.Component<Props> { - - node: any = null; - public static defaultProps = { - size: 20, - offsetX: 0, - offsetY: 0, - tooltipY: I.MenuDirection.Bottom, - color: 'grey', - menuParam: {}, - style: {}, - }; +const IconObject = observer(forwardRef<IconObjectRefProps, Props>((props, ref) => { + const { + className = '', + canEdit = false, + size = 20, + offsetX = 0, + offsetY = 0, + tooltip = '', + tooltipY = I.MenuDirection.Bottom, + noRemove = false, + noClick = false, + menuParam = {}, + style = {}, + getObject, + onSelect, + onUpload, + onClick, + onCheckbox, + onMouseEnter, + onMouseLeave, + } = props; + + const theme = S.Common.getThemeClass(); + const nodeRef = useRef(null); + const checkboxRef = useRef(null); + + let object: any = getObject ? getObject() : props.object || {}; - constructor (props: Props) { - super(props); + const [ stateObject, setStateObject ] = useState(null); - this.onClick = this.onClick.bind(this); - this.onMouseDown = this.onMouseDown.bind(this); - this.onMouseEnter = this.onMouseEnter.bind(this); - this.onMouseLeave = this.onMouseLeave.bind(this); + if (stateObject) { + object = Object.assign(object, stateObject || {}); }; - render () { - const { className, size, canEdit, style } = this.props; - const { theme } = S.Common; - const object = this.getObject(); - const layout = Number(object.layout) || I.ObjectLayout.Page; - const { id, name, iconEmoji, iconImage, iconOption, done, relationFormat, isDeleted } = object || {}; - const cn = [ 'iconObject', 'c' + size, U.Data.layoutClass(object.id, layout) ]; - const iconSize = this.iconSize(); - const tc = S.Common.getThemeClass(); - - if (className) { - cn.push(className); - }; - if (canEdit) { - cn.push('canEdit'); - }; - - let icon = null; - let icn = []; - - const defaultIcon = (type: string) => { - if (!DefaultIcons.includes(type)) { - return; - }; - - cn.push('withDefault'); - icn = icn.concat([ 'iconCommon', 'c' + iconSize ]); - icon = <img src={this.defaultIcon(type)} className={icn.join(' ')} />; - }; - - switch (layout) { - default: - case I.ObjectLayout.Chat: - case I.ObjectLayout.Page: { - if (iconImage) { - cn.push('withImage'); - }; - - let di = 'page'; - switch (layout) { - case I.ObjectLayout.Chat: di = 'chat'; break; - case I.ObjectLayout.Collection: - case I.ObjectLayout.Set: di = 'set'; break; - }; - - if (iconEmoji || iconImage) { - icon = <IconEmoji {...this.props} className={icn.join(' ')} size={iconSize} icon={iconEmoji} objectId={iconImage} />; - } else { - defaultIcon(di); - }; - break; - }; - - case I.ObjectLayout.Date: - defaultIcon('date'); - break; - - case I.ObjectLayout.Collection: - case I.ObjectLayout.Set: { - if (iconImage) { - cn.push('withImage'); - }; - - if (iconEmoji || iconImage) { - icon = <IconEmoji {...this.props} className={icn.join(' ')} size={iconSize} icon={iconEmoji} objectId={iconImage} />; - } else { - defaultIcon('set'); - }; - break; - }; - - case I.ObjectLayout.Human: - case I.ObjectLayout.Participant: { - icn = icn.concat([ 'iconImage', 'c' + iconSize ]); - - if (iconImage) { - cn.push('withImage'); - icon = <img src={S.Common.imageUrl(iconImage, iconSize * 2)} className={icn.join(' ')} />; - } else { - icon = <img src={this.userSvg()} className={icn.join(' ')} />; - }; - break; - }; - - case I.ObjectLayout.Task: { - icn = icn.concat([ 'iconCheckbox', 'c' + iconSize ]); - icon = <img id="checkbox" src={done ? CheckboxTask[tc][2] : CheckboxTask[tc][0]} className={icn.join(' ')} />; - break; - }; - - case I.ObjectLayout.Dashboard: { - break; - }; - - case I.ObjectLayout.Note: { - defaultIcon('page'); - break; - }; - - case I.ObjectLayout.Type: { - if (iconEmoji) { - icon = <IconEmoji {...this.props} className={icn.join(' ')} size={iconSize} icon={iconEmoji} objectId={iconImage} />; - } else { - defaultIcon('type'); - }; - break; - }; - - case I.ObjectLayout.Relation: { - if ([ I.RelationType.Icon, I.RelationType.Relations ].includes(relationFormat)) { - break; - }; - - icn = icn.concat([ 'iconCommon', 'c' + iconSize ]); - icon = <img src={`./img/icon/relation/${Relation.typeName(relationFormat)}.svg`} className={icn.join(' ')} />; - break; - }; - - case I.ObjectLayout.Bookmark: { - if (iconImage) { - icn = icn.concat([ 'iconImage', 'c' + iconSize ]); - icon = <img src={S.Common.imageUrl(iconImage, iconSize * 2)} className={icn.join(' ')} />; - } else { - defaultIcon('bookmark'); - }; - break; - }; - - case I.ObjectLayout.Image: { - if (id) { - cn.push('withImage'); - icn = icn.concat([ 'iconImage', 'c' + iconSize ]); - icon = <img src={S.Common.imageUrl(id, iconSize * 2)} className={icn.join(' ')} />; - } else { - icn = icn.concat([ 'iconFile', 'c' + iconSize ]); - icon = <img src={U.File.iconPath(object)} className={icn.join(' ')} />; - }; - break; - }; - - case I.ObjectLayout.Video: - case I.ObjectLayout.Audio: - case I.ObjectLayout.Pdf: - case I.ObjectLayout.File: { - icn = icn.concat([ 'iconFile', 'c' + iconSize ]); - icon = <img src={U.File.iconPath(object)} className={icn.join(' ')} />; - break; - }; - - case I.ObjectLayout.SpaceView: { - icn = icn.concat([ 'iconImage', 'c' + iconSize ]); - cn.push('withImage'); - - if (iconImage) { - icon = <img src={S.Common.imageUrl(iconImage, iconSize * 2)} className={icn.join(' ')} />; - } else { - cn.push('withOption'); - icon = <img src={this.spaceSvg(iconOption)} className={icn.join(' ')} />; - }; - break; - }; - - }; - - if (isDeleted) { - icn = [ 'iconCommon', 'c' + iconSize ]; - icon = <img src={Ghost} className={icn.join(' ')} />; - }; - - if (!icon) { - return null; - }; + const layout = Number(object.layout) || I.ObjectLayout.Page; + const { id, name, iconEmoji, iconImage, iconOption, done, relationFormat, isDeleted } = object || {}; + const cn = [ 'iconObject', `c${size}`, className, U.Data.layoutClass(object.id, layout) ]; + const iconSize = props.iconSize || IconSize[size]; - return ( - <div - ref={node => this.node = node} - id={this.props.id} - className={cn.join(' ')} - onClick={this.onClick} - onMouseDown={this.onMouseDown} - onMouseEnter={this.onMouseEnter} - onMouseLeave={this.onMouseLeave} - draggable={true} - style={style} - onDragStart={(e: any) => { - e.preventDefault(); - e.stopPropagation(); - }} - > - {icon} - </div> - ); + if (canEdit) { + cn.push('canEdit'); }; - getObject () { - const { getObject } = this.props; - return (getObject ? getObject() : this.props.object) || {}; - }; + let icon = null; + let icn = []; - onClick (e: any) { - if (this.props.noClick) { + const onClickHandler = (e: any) => { + if (noClick) { e.stopPropagation(); }; }; - onMouseEnter (e: any) { - const { canEdit, tooltip, tooltipY, onMouseEnter } = this.props; - const tc = S.Common.getThemeClass(); - const node = $(this.node); - const object = this.getObject(); - + const onMouseEnterHandler = (e: any) => { if (tooltip) { - Preview.tooltipShow({ text: tooltip, element: node, typeY: tooltipY }); + Preview.tooltipShow({ text: tooltip, element: $(nodeRef.current), typeY: tooltipY }); }; if (canEdit && U.Object.isTaskLayout(object.layout)) { - node.find('#checkbox').attr({ src: object.done ? CheckboxTask[tc][2] : CheckboxTask[tc][1] }); + $(checkboxRef.current).attr({ src: object.done ? CheckboxTask[theme][2] : CheckboxTask[theme][1] }); }; if (onMouseEnter) { @@ -353,16 +179,11 @@ const IconObject = observer(class IconObject extends React.Component<Props> { }; }; - onMouseLeave (e: any) { - const { canEdit, onMouseLeave } = this.props; - const tc = S.Common.getThemeClass(); - const node = $(this.node); - const object = this.getObject(); - + const onMouseLeaveHandler = (e: any) => { Preview.tooltipHide(false); if (canEdit && U.Object.isTaskLayout(object.layout)) { - node.find('#checkbox').attr({ src: object.done ? CheckboxTask[tc][2] : CheckboxTask[tc][0] }); + $(checkboxRef.current).attr({ src: object.done ? CheckboxTask[theme][2] : CheckboxTask[theme][0] }); }; if (onMouseLeave) { @@ -370,9 +191,7 @@ const IconObject = observer(class IconObject extends React.Component<Props> { }; }; - onMouseDown (e: any) { - const { canEdit, onClick, onCheckbox } = this.props; - const object = this.getObject(); + const onMouseDown = (e: any) => { const isTask = U.Object.isTaskLayout(object.layout); const isEmoji = LAYOUTS_WITH_EMOJI_GALLERY.includes(object.layout); @@ -397,22 +216,20 @@ const IconObject = observer(class IconObject extends React.Component<Props> { }; } else if (isEmoji) { - this.onEmoji(e); + onEmoji(e); }; - this.onMouseLeave(e); + onMouseLeaveHandler(e); }; - onEmoji (e: any) { + const onEmoji = (e: any) => { e.stopPropagation(); - const { id, offsetX, offsetY, onSelect, onUpload, noRemove, menuParam } = this.props; - const object = this.getObject(); - const noGallery = this.props.noGallery || [ I.ObjectLayout.SpaceView, I.ObjectLayout.Human ].includes(object.layout); - const noUpload = this.props.noUpload || [ I.ObjectLayout.Type ].includes(object.layout); + const noGallery = props.noGallery || [ I.ObjectLayout.SpaceView, I.ObjectLayout.Human ].includes(object.layout); + const noUpload = props.noUpload || [ I.ObjectLayout.Type ].includes(object.layout); S.Menu.open('smile', { - element: `#${id}`, + element: `#${props.id}`, offsetX, offsetY, data: { @@ -440,31 +257,18 @@ const IconObject = observer(class IconObject extends React.Component<Props> { }); }; - iconSize () { - const { size, iconSize } = this.props; - return iconSize || IconSize[size]; + const fontSize = (size: number): number => { + return Math.min(72, FontSize[size]); }; - fontSize (size: number) { - let s = FontSize[size]; - - if (size > 96) { - s = 72; - }; - - return s; - }; - - fontWeight (size: number) { + const fontWeight = (size: number): number => { return size > 18 ? 600 : 500; }; - userSvg (): string { - const { size } = this.props; - const color = J.Theme[S.Common.getThemeClass()]?.iconUser; - + const userSvg = (): string => { + const color = J.Theme[theme]?.iconUser; const circle = `<circle cx="50%" cy="50%" r="50%" fill="${color.bg}" />`; - const text = `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" fill="${color.text}" font-family="Inter, Helvetica" font-weight="${this.fontWeight(size)}" font-size="${this.fontSize(size)}px">${this.iconName()}</text>`; + const text = `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" fill="${color.text}" font-family="Inter, Helvetica" font-weight="${fontWeight(size)}" font-size="${fontSize(size)}px">${iconName()}</text>`; const svg = ` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 ${size} ${size}" xml:space="preserve" height="${size}px" width="${size}px"> ${circle} @@ -475,12 +279,11 @@ const IconObject = observer(class IconObject extends React.Component<Props> { return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); }; - spaceSvg (option: number): string { + const spaceSvg = (option: number): string => { const { bg, list } = J.Theme.iconSpace; - const { size } = this.props; const bgColor = bg[list[option - 1]]; - const text = `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" fill="#fff" font-family="Inter, Helvetica" font-weight="${this.fontWeight(size)}" font-size="${this.fontSize(size)}px">${this.iconName()}</text>`; + const text = `<text x="50%" y="50%" text-anchor="middle" dominant-baseline="central" fill="#fff" font-family="Inter, Helvetica" font-weight="${fontWeight(size)}" font-size="${fontSize(size)}px">${iconName()}</text>`; const svg = ` <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 ${size} ${size}" xml:space="preserve" height="${size}px" width="${size}px"> <rect width="${size}" height="${size}" fill="${bgColor}"/> @@ -491,21 +294,192 @@ const IconObject = observer(class IconObject extends React.Component<Props> { return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); }; - iconName () { - const object = this.getObject(); + const iconName = (): string => { + let ret = String(name || translate('defaultNamePage')); + ret = U.Smile.strip(ret); + ret = ret.trim().substring(0, 1).toUpperCase(); + ret = U.Common.htmlSpecialChars(ret); + return ret; + }; - let name = String(object.name || translate('defaultNamePage')); - name = U.Smile.strip(name); - name = name.trim().substring(0, 1).toUpperCase(); - name = U.Common.htmlSpecialChars(name); + const defaultIcon = (type: string) => { + if (!DefaultIcons.includes(type)) { + return; + }; + + const src = require(`img/icon/default/${type}.svg`); - return name; + cn.push('withDefault'); + icn = icn.concat([ 'iconCommon', 'c' + iconSize ]); + icon = <img src={src} className={icn.join(' ')} />; }; - defaultIcon (type: string) { - return require(`img/icon/default/${type}.svg`); + switch (layout) { + default: + case I.ObjectLayout.Chat: + case I.ObjectLayout.Page: { + if (iconImage) { + cn.push('withImage'); + }; + + let di = 'page'; + switch (layout) { + case I.ObjectLayout.Chat: di = 'chat'; break; + case I.ObjectLayout.Collection: + case I.ObjectLayout.Set: di = 'set'; break; + }; + + if (iconEmoji || iconImage) { + icon = <IconEmoji {...props} className={icn.join(' ')} size={iconSize} icon={iconEmoji} objectId={iconImage} />; + } else { + defaultIcon(di); + }; + break; + }; + + case I.ObjectLayout.Date: + defaultIcon('date'); + break; + + case I.ObjectLayout.Collection: + case I.ObjectLayout.Set: { + if (iconImage) { + cn.push('withImage'); + }; + + if (iconEmoji || iconImage) { + icon = <IconEmoji {...props} className={icn.join(' ')} size={iconSize} icon={iconEmoji} objectId={iconImage} />; + } else { + defaultIcon('set'); + }; + break; + }; + + case I.ObjectLayout.Human: + case I.ObjectLayout.Participant: { + icn = icn.concat([ 'iconImage', 'c' + size ]); + + if (iconImage) { + cn.push('withImage'); + icon = <img src={S.Common.imageUrl(iconImage, size * 2)} className={icn.join(' ')} />; + } else { + icon = <img src={userSvg()} className={icn.join(' ')} />; + }; + break; + }; + + case I.ObjectLayout.Task: { + icn = icn.concat([ 'iconCheckbox', 'c' + iconSize ]); + icon = <img ref={checkboxRef} src={done ? CheckboxTask[theme][2] : CheckboxTask[theme][0]} className={icn.join(' ')} />; + break; + }; + + case I.ObjectLayout.Dashboard: { + break; + }; + + case I.ObjectLayout.Note: { + defaultIcon('page'); + break; + }; + + case I.ObjectLayout.Type: { + if (iconEmoji) { + icon = <IconEmoji {...props} className={icn.join(' ')} size={iconSize} icon={iconEmoji} objectId={iconImage} />; + } else { + defaultIcon('type'); + }; + break; + }; + + case I.ObjectLayout.Relation: { + if ([ I.RelationType.Icon, I.RelationType.Relations ].includes(relationFormat)) { + break; + }; + + icn = icn.concat([ 'iconCommon', 'c' + iconSize ]); + icon = <img src={`./img/icon/relation/${Relation.typeName(relationFormat)}.svg`} className={icn.join(' ')} />; + break; + }; + + case I.ObjectLayout.Bookmark: { + if (iconImage) { + icn = icn.concat([ 'iconImage', 'c' + iconSize ]); + icon = <img src={S.Common.imageUrl(iconImage, iconSize * 2)} className={icn.join(' ')} />; + } else { + defaultIcon('bookmark'); + }; + break; + }; + + case I.ObjectLayout.Image: { + if (id) { + cn.push('withImage'); + icn = icn.concat([ 'iconImage', 'c' + iconSize ]); + icon = <img src={S.Common.imageUrl(id, iconSize * 2)} className={icn.join(' ')} />; + } else { + icn = icn.concat([ 'iconFile', 'c' + iconSize ]); + icon = <img src={U.File.iconPath(object)} className={icn.join(' ')} />; + }; + break; + }; + + case I.ObjectLayout.Video: + case I.ObjectLayout.Audio: + case I.ObjectLayout.Pdf: + case I.ObjectLayout.File: { + icn = icn.concat([ 'iconFile', 'c' + iconSize ]); + icon = <img src={U.File.iconPath(object)} className={icn.join(' ')} />; + break; + }; + + case I.ObjectLayout.SpaceView: { + icn = icn.concat([ 'iconImage', 'c' + iconSize ]); + cn.push('withImage'); + + if (iconImage) { + icon = <img src={S.Common.imageUrl(iconImage, iconSize * 2)} className={icn.join(' ')} />; + } else { + cn.push('withOption'); + icon = <img src={spaceSvg(iconOption)} className={icn.join(' ')} />; + }; + break; + }; + + }; + + if (isDeleted) { + icon = <img src={Ghost} className={[ 'iconCommon', `c${iconSize}` ].join(' ')} />; + }; + + useImperativeHandle(ref, () => ({ + setObject: object => setStateObject(object), + })); + + if (!icon) { + return null; }; -}); + return ( + <div + ref={nodeRef} + id={props.id} + className={cn.join(' ')} + onClick={onClickHandler} + onMouseDown={onMouseDown} + onMouseEnter={onMouseEnterHandler} + onMouseLeave={onMouseLeaveHandler} + draggable={true} + style={style} + onDragStart={(e: any) => { + e.preventDefault(); + e.stopPropagation(); + }} + > + {icon} + </div> + ); + +})); export default IconObject; \ No newline at end of file diff --git a/src/ts/component/util/marker.tsx b/src/ts/component/util/marker.tsx index 294cc81176..cc6eca3d06 100644 --- a/src/ts/component/util/marker.tsx +++ b/src/ts/component/util/marker.tsx @@ -1,7 +1,7 @@ import React, { forwardRef, useRef } from 'react'; import $ from 'jquery'; import { I, S, J } from 'Lib'; -import { useLocalObservable } from 'mobx-react'; +import { observer } from 'mobx-react'; interface Props { id: string; @@ -40,7 +40,7 @@ Theme.dark[I.MarkerType.Task] = { 2: require('img/icon/object/checkbox2.svg'), }; -const Marker = forwardRef<HTMLDivElement, Props>(({ +const Marker = observer(forwardRef<HTMLDivElement, Props>(({ id = '', type = I.MarkerType.Bulleted, color = 'default', @@ -54,10 +54,7 @@ const Marker = forwardRef<HTMLDivElement, Props>(({ const refNode = useRef<HTMLDivElement>(null); const cn = [ 'marker', className ]; const ci = [ 'markerInner', `c${type}`, `textColor textColor-${colorValue}` ]; - const { theme, themeClass } = useLocalObservable(() => ({ - theme: S.Common.theme, - themeClass: S.Common.getThemeClass(), - })); + const themeClass = S.Common.getThemeClass(); if (active) { cn.push('active'); @@ -139,6 +136,6 @@ const Marker = forwardRef<HTMLDivElement, Props>(({ {inner} </div> ); -}); +})); export default Marker; \ No newline at end of file diff --git a/src/ts/component/util/media/audio.tsx b/src/ts/component/util/media/audio.tsx index bd53259dc8..da32e42786 100644 --- a/src/ts/component/util/media/audio.tsx +++ b/src/ts/component/util/media/audio.tsx @@ -1,9 +1,7 @@ -import * as React from 'react'; +import React, { useState, useRef, useEffect, useImperativeHandle, forwardRef, MouseEvent } from 'react'; import $ from 'jquery'; -import { Icon, DragHorizontal, DragVertical } from 'Component'; +import { Icon, DragHorizontal, DragVertical, Floater } from 'Component'; import { U } from 'Lib'; -import { Floater } from '../floater'; -import _ from 'lodash'; interface PlaylistItem { name: string; @@ -16,269 +14,147 @@ interface Props { onPause?(): void; }; -interface State { - volume: number; - muted: boolean; - showVolumeSlider: boolean; - timeMetric: string; - current: PlaylistItem; +interface MediaAudioRefProps { + updatePlaylist(playlist: PlaylistItem[]): void; }; -class MediaAudio extends React.PureComponent<Props, State> { - - node: HTMLDivElement = null; - timeDragRef: DragHorizontal = null; - audioNode: HTMLAudioElement = null; - volumeIcon = null; - - playOnSeek = false; - current: PlaylistItem = { name: '', src: '' }; - resizeObserver: ResizeObserver; - fadeOutVolumeSlider = _.debounce(() => this.setState({ showVolumeSlider: false }), 250); - - startedPlaying = false; - - constructor (props: Props) { - super(props); - - this.state = { - volume: 1, - muted: false, - showVolumeSlider: false, - timeMetric: '', - current: null, - }; - - this.onPlayClick = this.onPlayClick.bind(this); - this.onMute = this.onMute.bind(this); - this.onResize = this.onResize.bind(this); - this.resizeObserver = new ResizeObserver(this.onResize); - }; - - render () { - const { volume, muted, current } = this.state; - const { src, name } = current || {}; - const iconClasses = [ 'volume']; - - if (!volume || muted) { - iconClasses.push('muted'); - }; - - return ( - <div - ref={node => this.node = node} - className="wrap resizable audio mediaAudio" - > - <audio id="audio" preload="auto" src={src} /> - - <div className="controlsWrapper"> - <div className="name"> - <span>{name}</span> - </div> - - <div className="controls"> - <Icon className="play" onClick={this.onPlayClick} /> - - <div className="timeDragWrapper"> - <DragHorizontal - id="time" - ref={ref => this.timeDragRef = ref} - value={0} - onStart={(e: any, v: number) => this.onTime(v)} - onMove={(e: any, v: number) => this.onTime(v)} - onEnd={(e: any, v: number) => this.onTimeEnd(v)} - /> - </div> - - <div className="time"> - <span id="timeMetric" className="metric">{this.state.timeMetric}</span> - </div> - <div onMouseLeave={this.fadeOutVolumeSlider}> - <Icon - onMouseEnter={() => { - this.fadeOutVolumeSlider.cancel(); - return this.setState({ showVolumeSlider: true }); - }} - ref={el => this.volumeIcon = el} - className={iconClasses.join(' ')} - onClick={this.onMute} - /> - - <Floater - anchorEl={this.volumeIcon?.node} - isShown={this.state.showVolumeSlider} - gap={8} - > - <DragVertical - id="volume" - className="volume" - value={volume * (muted ? 0 : 1)} - onChange={(e: any, v: number) => this.onVolume(v)} - onMouseEnter={() => { - this.fadeOutVolumeSlider.cancel(); - return this.setState({ showVolumeSlider: true }); - }} - - /> - </Floater> - </div> - </div> - </div> - </div> - ); - }; - - componentDidMount () { - const playlist = this.getPlaylist(); - - this.setState({ current: playlist[0] }); - this.resizeObserver.observe(this.node); - }; - - componentDidUpdate () { - this.resize(); - this.rebind(); - }; - - componentWillUnmount () { - this.unbind(); - this.resizeObserver.unobserve(this.node); - }; - - rebind () { - this.unbind(); - - const node = $(this.node); - const el = node.find('#audio'); - - this.audioNode = el.get(0) as HTMLAudioElement; - - if (el.length) { - el.on('canplay timeupdate', () => this.onTimeUpdate()); - el.on('play', () => this.onPlay()); - el.on('ended pause', () => this.onPause()); - }; - }; - - unbind () { - const node = $(this.node); - const el = node.find('#audio'); - - if (el.length) { - el.off('canplay timeupdate play ended pause'); - }; +const MediaAudio = forwardRef<MediaAudioRefProps, Props>(({ + playlist = [], + onPlay, + onPause, +}, ref) => { + + const nodeRef = useRef<HTMLDivElement>(null); + const audioRef = useRef<HTMLAudioElement>(null); + const timeRef = useRef(null); + const timeTextRef = useRef(null); + const volumeIconRef = useRef(null); + const playIconRef = useRef(null); + const floaterRef = useRef(null); + const resizeObserver = new ResizeObserver(() => resize()); + const [ current, setCurrent ] = useState<PlaylistItem>(null); + const { src, name } = current || {}; + const ci = [ 'volume' ]; + + const isPlaying = useRef(false); + const volume = useRef(1); + const isMuted = useRef(false); + const playOnSeek = useRef(false); + + const rebind = () => { + unbind(); + + const node = $(audioRef.current); + + node.on('canplay timeupdate', () => onTimeUpdate()); + node.on('play', () => onPlayHandler()); + node.on('ended pause', () => onPauseHandler()); }; - getPlaylist () { - return this.props.playlist || []; + const unbind = () => { + $(audioRef.current).off('canplay timeupdate play ended pause'); }; - updatePlaylist (playlist: PlaylistItem[]) { - playlist = playlist || []; - - this.setState({ current: playlist[0] }); - }; - - resize () { - if (this.timeDragRef) { - this.timeDragRef.resize(); - }; - }; - - onResize () { - this.resize(); - this.rebind(); + const resize = () => { + timeRef.current?.resize(); }; - onPlayClick (e: React.MouseEvent) { + const onPlayClick = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - const el = this.audioNode; - const paused = el.paused; - U.Common.pauseMedia(); - paused ? this.play() : this.pause(); + isPlaying.current ? pause() : play(); }; - onPlay () { - this.startedPlaying = true; - - const { onPlay } = this.props; - const node = $(this.node); - const icon = node.find('.icon.play'); - - icon.addClass('active'); + const onPlayHandler = () => { + isPlaying.current = true; + $(playIconRef.current).addClass('active'); if (onPlay) { onPlay(); }; }; - onPause () { - const { onPause } = this.props; - const node = $(this.node); - const icon = node.find('.icon.play'); - - icon.removeClass('active'); + const onPauseHandler = () => { + isPlaying.current = false; + $(playIconRef.current).removeClass('active'); if (onPause) { onPause(); }; }; - play () { - this.audioNode.play(); + const play = () => { + audioRef.current.play(); }; - pause () { - this.audioNode.pause(); + const pause = () => { + audioRef.current.pause(); }; - onMute (e: React.MouseEvent) { + const onMute = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - - const muted = !this.state.muted; - this.setState({ muted }); - this.audioNode.volume = this.state.volume * (muted ? 0 : 1); + + isMuted.current = !isMuted.current; + onVolume(volume.current); }; - onVolume (volume: number) { - this.setState({ volume }); - this.audioNode.volume = volume * (this.state.muted ? 0 : 1); + const onVolume = (v: number) => { + volume.current = v; + audioRef.current.volume = v * (isMuted.current ? 0 : 1); + + checkVolumeClass(); + }; + + const checkVolumeClass = () => { + $(volumeIconRef.current).toggleClass('isMuted', !(!isMuted.current && volume.current)); }; - onTime (v: number) { - const paused = this.audioNode.paused; + const onTime = (v: number) => { + const ref = audioRef.current; + if (!ref) { + return; + }; - if (!paused) { - this.pause(); - this.playOnSeek = true; + if (!ref.paused) { + pause(); + playOnSeek.current = true; }; - this.audioNode.currentTime = Number(v * this.audioNode.duration) || 0; + ref.currentTime = Number(v * ref.duration) || 0; }; - onTimeEnd (v: number) { - if (this.playOnSeek) { - this.play(); + const onTimeEnd = (v: number) => { + if (playOnSeek.current) { + play(); }; }; - onTimeUpdate () { - const el = this.audioNode; - if (!el) { + const onVolumeEnter = () => { + floaterRef.current.show(); + }; + + const onVolumeLeave = () => { + floaterRef.current.hide(); + }; + + const onTimeUpdate = () => { + const audio = audioRef.current; + const ref = timeRef.current; + + if (!ref || !audio) { return; }; - const t = this.startedPlaying ? this.getTime(el.currentTime) : this.getTime(el.duration); + const { m, s } = getTime(isPlaying.current ? audio.currentTime : audio.duration); - this.setState({ timeMetric: `${U.Common.sprintf('%02d', t.m)}:${U.Common.sprintf('%02d', t.s)}`}); - this.timeDragRef.setValue(el.currentTime / el.duration); + $(timeTextRef.current).text(`${U.Common.sprintf('%02d', m)}:${U.Common.sprintf('%02d', s)}`); + ref.setValue(audio.currentTime / audio.duration); }; - getTime (t: number): { m: number, s: number } { + const getTime = (t: number): { m: number, s: number } => { t = Number(t) || 0; const m = Math.floor(t / 60); @@ -289,6 +165,101 @@ class MediaAudio extends React.PureComponent<Props, State> { return { m, s }; }; -}; + useEffect(() => { + onVolume(1); + + return () => { + unbind(); + + if (nodeRef.current) { + resizeObserver.unobserve(nodeRef.current); + }; + }; + }, []); + + useEffect(() => { + resize(); + rebind(); + + if (nodeRef.current) { + resizeObserver.observe(nodeRef.current); + }; + setCurrent(playlist[0]); + }); + + useImperativeHandle(ref, () => ({ + updatePlaylist: (playlist: PlaylistItem[]) => { + playlist = playlist || []; + + if (playlist.length) { + setCurrent(playlist[0]); + }; + }, + })); + + return ( + <div + ref={nodeRef} + className="wrap resizable audio mediaAudio" + > + <audio ref={audioRef} preload="auto" src={src} /> + + <div className="controlsWrapper"> + <div className="name"> + <span>{name}</span> + </div> + + <div className="controls"> + <Icon + ref={playIconRef} + className="play" + onMouseDown={onPlayClick} + onClick={e => e.stopPropagation()} + /> + + <div className="timeDragWrapper"> + <DragHorizontal + id="timeDrag" + ref={timeRef} + value={0} + onStart={(e: any, v: number) => onTime(v)} + onMove={(e: any, v: number) => onTime(v)} + onEnd={(e: any, v: number) => onTimeEnd(v)} + /> + </div> + + <div className="timeText"> + <span ref={timeTextRef} /> + </div> + + <div className="volumeWrap" onMouseLeave={onVolumeLeave}> + <Icon + ref={volumeIconRef} + className={ci.join(' ')} + onMouseDown={onMute} + onMouseEnter={onVolumeEnter} + onClick={e => e.stopPropagation()} + /> + + <Floater + ref={floaterRef} + anchorEl={volumeIconRef.current} + offset={4} + > + <DragVertical + id="volume" + className="volume" + value={volume.current} + onChange={(e: any, v: number) => onVolume(v)} + onMouseEnter={onVolumeEnter} + /> + </Floater> + </div> + </div> + </div> + </div> + ); + +}); export default MediaAudio; \ No newline at end of file diff --git a/src/ts/component/util/media/mermaid.tsx b/src/ts/component/util/media/mermaid.tsx index 5dc17d17de..48b3702161 100644 --- a/src/ts/component/util/media/mermaid.tsx +++ b/src/ts/component/util/media/mermaid.tsx @@ -1,22 +1,24 @@ import React, { forwardRef, useRef, useEffect } from 'react'; import $ from 'jquery'; import mermaid from 'mermaid'; -import { useLocalObservable } from 'mobx-react'; +import { observer } from 'mobx-react'; import { J, S, U } from 'Lib'; interface Props { chart: string; }; -const MediaMermaid = forwardRef<HTMLDivElement, Props>(({ +const MediaMermaid = observer(forwardRef<HTMLDivElement, Props>(({ chart = '', }, ref) => { const nodeRef = useRef(null); - const { theme } = useLocalObservable(() => S.Common); + const chartRef = useRef(null); + const errorRef = useRef(null); + const themeClass = S.Common.getThemeClass(); const init = () => { - const themeVariables = (J.Theme[S.Common.getThemeClass()] || {}).mermaid; + const themeVariables = (J.Theme[themeClass] || {}).mermaid; if (!themeVariables) { return; }; @@ -28,50 +30,28 @@ const MediaMermaid = forwardRef<HTMLDivElement, Props>(({ }; mermaid.initialize({ theme: 'base', themeVariables }); - }; - - async function drawDiagram () { - const node = $(nodeRef.current); - - let svg: any = ''; - try { - const res = await mermaid.render('mermaid-chart', chart); + mermaid.contentLoaded(); - if (res.svg) { - svg = res.svg; - }; - } catch (e) { - console.error('[Mermaid].drawDiagram', e); - node.find('.error').text(e.message); - }; - - node.find('.mermaid').html(svg); - U.Common.renderLinks(node); + U.Common.renderLinks($(chartRef.current)); }; useEffect(() => { init(); - mermaid.contentLoaded(); - U.Common.renderLinks($(nodeRef.current)); }); useEffect(() => { - const node = $(nodeRef.current); - - node.find('.chart').removeAttr('data-processed'); - node.find('.error').text(''); - + $(chartRef.current).removeAttr('data-processed'); + $(errorRef.current).text(''); init(); - drawDiagram(); - }, [ theme, chart ]); + }, [ themeClass, chart ]); return ( <div ref={nodeRef} className="mermaidWrapper"> - <div className="error" /> - <div className="mermaid">{chart}</div> + <div ref={errorRef} className="error" /> + <div ref={chartRef} className="mermaid">{chart}</div> </div> ); -}); +})); export default MediaMermaid; \ No newline at end of file diff --git a/src/ts/component/util/media/pdf.tsx b/src/ts/component/util/media/pdf.tsx index cee444a9ae..80d3df755b 100644 --- a/src/ts/component/util/media/pdf.tsx +++ b/src/ts/component/util/media/pdf.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useImperativeHandle, MouseEvent, useRef, useState, useEffect } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { Loader } from 'Component'; @@ -16,89 +16,85 @@ interface Props { page: number; onDocumentLoad: (result: any) => void; onPageRender: () => void; - onClick: (e: any) => void; + onClick: (e: MouseEvent) => void; }; -interface State { - width: number; +interface MediaPdfRefProps { + resize: () => void; }; - -class MediaPdf extends React.Component<Props, State> { - _isMounted = false; - node = null; - state = { - width: 0, +const MediaPdf = forwardRef<MediaPdfRefProps, Props>(({ + id = '', + src = '', + page = 0, + onDocumentLoad, + onPageRender, + onClick, +}, ref) => { + + const [ width, setWidth ] = useState(0); + const nodeRef = useRef(null); + const frame = useRef(0); + const resizeObserver = new ResizeObserver(() => resize()); + const options = { + isEvalSupported: false, + cMapUrl: U.Common.fixAsarPath('./cmaps/'), }; - frame = 0; - render () { - const { width } = this.state; - const { src, page, onDocumentLoad, onClick } = this.props; - const options = { - isEvalSupported: false, - cMapUrl: U.Common.fixAsarPath('./cmaps/'), + const resize = () => { + if (frame.current) { + raf.cancel(frame.current); }; - return ( - <div ref={ref => this.node = ref}> - <Document - file={src} - onLoadSuccess={onDocumentLoad} - renderMode="canvas" - loading={<Loader />} - onClick={onClick} - options={options} - > - <Page - pageNumber={page} - loading={<Loader />} - width={width} - onRenderSuccess={this.onPageRender} - /> - </Document> - </div> - ); - }; - - componentDidMount(): void { - this._isMounted = true; - this.rebind(); - this.resize(); - }; - - componentWillUnmount(): void { - this._isMounted = false; - this.unbind(); - - raf.cancel(this.frame); - }; - - unbind () { - $(window).off(`resize.pdf-${this.props.id}`); + frame.current = raf(() => { + if (nodeRef.current) { + setWidth($(nodeRef.current).width()); + }; + }); }; - rebind () { - $(window).on(`resize.pdf-${this.props.id}`, () => this.resize()); + const onPageRenderHandler = () => { + onPageRender(); + resize(); }; - resize () { - if (this.frame) { - raf.cancel(this.frame); + useEffect(() => { + if (nodeRef.current) { + resizeObserver.observe(nodeRef.current); }; - this.frame = raf(() => { - if (this._isMounted) { - this.setState({ width: $(this.node).width() }); - }; - }); - }; - - onPageRender = () => { - this.props.onPageRender(); - this.resize(); - }; + return () => { + raf.cancel(frame.current); -}; + if (nodeRef.current) { + resizeObserver.unobserve(nodeRef.current); + }; + }; + }); + + useImperativeHandle(ref, () => ({ + resize, + })); + + return ( + <div ref={nodeRef}> + <Document + file={src} + onLoadSuccess={onDocumentLoad} + renderMode="canvas" + loading={<Loader />} + onClick={onClick} + options={options} + > + <Page + pageNumber={page} + loading={<Loader />} + width={width} + onRenderSuccess={onPageRenderHandler} + /> + </Document> + </div> + ); +}); export default MediaPdf; \ No newline at end of file diff --git a/src/ts/component/util/media/video.tsx b/src/ts/component/util/media/video.tsx index e337dc8fbb..fdf35b4546 100644 --- a/src/ts/component/util/media/video.tsx +++ b/src/ts/component/util/media/video.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect } from 'react'; import $ from 'jquery'; import { U } from 'Lib'; import { Icon } from 'Component'; @@ -9,122 +9,86 @@ interface Props { onPlay?(): void; onPause?(): void; onClick?(e: any): void; + onLoad?(): void; }; -class MediaVideo extends React.Component<Props> { +const MediaVideo = forwardRef<HTMLDivElement, Props>(({ + src = '', + canPlay = true, + onPlay, + onPause = () => {}, + onClick = () => {}, + onLoad = () => {}, +}, ref: any) => { - public static defaultProps: Props = { - canPlay: true, - src: '', - }; - - node: any = null; - speed = 1; - - constructor (props: Props) { - super(props); - - this.onPlayClick = this.onPlayClick.bind(this); - }; + const nodeRef = useRef(null); + const videoRef = useRef(null); - render () { - const { src, onClick } = this.props; + const rebind = () => { + unbind(); - return ( - <div - ref={ref => this.node = ref} - className="mediaVideo" - onClick={onClick} - > - <video className="media" controls={false} preload="auto" src={src} /> + const video = $(videoRef.current); - <div className="controls"> - <Icon className="play" onClick={this.onPlayClick} /> - </div> - </div> - ); + video.on('play', () => onPlayHandler()); + video.on('pause', () => onPause()); + video.on('ended', () => onEnded()); + video.on('canplay', () => onLoad()); }; - componentDidMount () { - this.rebind(); + const unbind = () => { + $(videoRef.current).off('canplay ended pause play'); }; - rebind () { - this.unbind(); - - const node = $(this.node); - const video = node.find('video'); - - video.on('play', () => this.onPlay()); - video.on('pause', () => this.onPause()); - video.on('ended', () => this.onEnded()); - }; - - unbind () { - const node = $(this.node); - const video = node.find('video'); - - video.off('canplay ended pause play'); - }; - - onPlay () { - const { onPlay } = this.props; - const node = $(this.node); - const video = node.find('video'); - - if (!video.length) { - return; + const onPlayHandler = () => { + if (videoRef.current) { + videoRef.current.controls = true; }; - - video.get(0).controls = true; - node.addClass('isPlaying'); + $(nodeRef.current).addClass('isPlaying'); if (onPlay) { onPlay(); }; }; - onPause () { - const { onPause } = this.props; - - if (onPause) { - onPause(); - }; - }; - - onEnded () { - const node = $(this.node); - const video = node.find('video'); - - if (!video.length) { - return; + const onEnded = () => { + if (videoRef.current) { + videoRef.current.controls = false; }; + $(nodeRef.current).removeClass('isPlaying'); - video.get(0).controls = false; - node.removeClass('isPlaying'); - - this.onPause(); + onPause(); }; - onPlayClick (e: any) { - if (!this.props.canPlay) { + const onPlayClick = (e: any) => { + if (!canPlay) { return; }; e.preventDefault(); e.stopPropagation(); - const node = $(this.node); - const video = node.find('video'); - - if (!video.length) { - return; - }; - U.Common.pauseMedia(); - video.get(0).play(); + videoRef.current?.play(); }; -}; + useEffect(() => { + rebind(); + return () => unbind(); + }); + + return ( + <div + ref={nodeRef} + className="mediaVideo" + onClick={onClick} + > + <video ref={videoRef} className="media" controls={false} preload="auto" src={src} /> + + <div className="controls"> + <Icon className="play" onClick={onPlayClick} /> + </div> + </div> + ); +}); export default MediaVideo; \ No newline at end of file diff --git a/src/ts/component/util/navigation.tsx b/src/ts/component/util/navigation.tsx deleted file mode 100644 index 5e39fcbc0e..0000000000 --- a/src/ts/component/util/navigation.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import * as React from 'react'; -import $ from 'jquery'; -import { observer } from 'mobx-react'; -import { Icon } from 'Component'; -import { I, C, S, U, J, keyboard, Preview, translate, analytics } from 'Lib'; - -const Navigation = observer(class Navigation extends React.Component { - - _isMounted = false; - node: any = null; - timeoutPlus = 0; - - constructor (props: object) { - super(props); - - this.onBack = this.onBack.bind(this); - this.onForward = this.onForward.bind(this); - this.onAdd = this.onAdd.bind(this); - this.onGraph = this.onGraph.bind(this); - this.onSearch = this.onSearch.bind(this); - }; - - render () { - const { navigationMenu } = S.Common; - const cmd = keyboard.cmdSymbol(); - const alt = keyboard.altSymbol(); - const isWin = U.Common.isPlatformWindows(); - const isLinux = U.Common.isPlatformLinux(); - const cb = isWin || isLinux ? `${alt} + ←` : `${cmd} + [`; - const cf = isWin || isLinux ? `${alt} + →` : `${cmd} + ]`; - const canWrite = U.Space.canMyParticipantWrite(); - - let buttonPlus: any = null; - if (canWrite) { - buttonPlus = { id: 'plus', tooltip: translate('commonCreateNewObject'), caption: `${cmd} + N / ${cmd} + ${alt} + N` }; - - switch (navigationMenu) { - case I.NavigationMenuMode.Context: { - buttonPlus.onClick = this.onAdd; - buttonPlus.onContextMenu = () => keyboard.onQuickCapture(false); - break; - }; - - case I.NavigationMenuMode.Click: { - buttonPlus.onClick = () => keyboard.onQuickCapture(false); - break; - }; - - case I.NavigationMenuMode.Hover: { - buttonPlus.onClick = this.onAdd; - buttonPlus.onMouseEnter = e => { - window.clearTimeout(this.timeoutPlus); - this.timeoutPlus = window.setTimeout(() => { - keyboard.onQuickCapture(false, { isSub: true, passThrough: false }); - }, 1000); - }; - buttonPlus.onMouseLeave = () => window.clearTimeout(this.timeoutPlus); - break; - }; - - }; - - }; - - const buttons: any[] = [ - { id: 'back', tooltip: translate('commonBack'), caption: cb, onClick: this.onBack, disabled: !keyboard.checkBack() }, - { id: 'forward', tooltip: translate('commonForward'), caption: cf, onClick: this.onForward, disabled: !keyboard.checkForward() }, - buttonPlus, - { id: 'graph', tooltip: translate('commonGraph'), caption: `${cmd} + ${alt} + O`, onClick: this.onGraph }, - { id: 'search', tooltip: translate('commonSearch'), caption: `${cmd} + S`, onClick: this.onSearch }, - ].filter(it => it).map(it => { - if (!it.onMouseEnter && !it.disabled) { - it.onMouseEnter = e => { - window.clearTimeout(this.timeoutPlus); - this.onTooltipShow(e, it.tooltip, it.caption); - }; - }; - if (!it.onMouseLeave) { - it.onMouseLeave = () => Preview.tooltipHide(false); - }; - return it; - }); - - return ( - <div - ref={node => this.node = node} - id="navigationPanel" - className="navigationPanel" - > - <div className="inner"> - {buttons.map(item => { - const cn = [ 'iconWrap' ]; - - if (item.disabled) { - cn.push('disabled'); - }; - - return ( - <div - key={item.id} - id={`button-navigation-${item.id}`} - className={cn.join(' ')} - onClick={e => { - window.clearTimeout(this.timeoutPlus); - item.onClick(e); - }} - onContextMenu={item.onContextMenu} - onMouseEnter={item.onMouseEnter} - onMouseLeave={item.onMouseLeave} - > - <Icon className={item.id} /> - </div> - ); - })} - </div> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - }; - - componentDidUpdate () { - }; - - componentWillUnmount () { - this._isMounted = false; - }; - - onBack () { - keyboard.onBack(); - }; - - onForward () { - keyboard.onForward(); - }; - - onAdd (e: any) { - e.altKey ? keyboard.onQuickCapture(false) : keyboard.pageCreate({}, analytics.route.navigation); - }; - - onGraph () { - U.Object.openAuto({ id: keyboard.getRootId(), layout: I.ObjectLayout.Graph }); - }; - - onSearch () { - keyboard.onSearchPopup(analytics.route.navigation); - }; - - position (sw: number, animate: boolean) { - const node = $(this.node); - const { ww } = U.Common.getWindowDimensions(); - const width = node.outerWidth(); - const x = (ww - sw) / 2 - width / 2 + sw; - - if (animate) { - node.addClass('sidebarAnimation'); - }; - - node.css({ left: `${x / ww * 100}%` }); - - if (animate) { - window.setTimeout(() => node.removeClass('sidebarAnimation'), J.Constant.delay.sidebar); - }; - }; - - onTooltipShow (e: any, text: string, caption?: string) { - const t = Preview.tooltipCaption(text, caption); - if (t) { - Preview.tooltipShow({ text: t, element: $(e.currentTarget), typeY: I.MenuDirection.Top }); - }; - }; - -}); - -export default Navigation; diff --git a/src/ts/component/util/pager.tsx b/src/ts/component/util/pager.tsx index 80530b1b68..e030120a38 100644 --- a/src/ts/component/util/pager.tsx +++ b/src/ts/component/util/pager.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { Icon } from 'Component'; interface Props { @@ -10,93 +10,50 @@ interface Props { onChange?: (page: number) => void; }; -class Pager extends React.Component<Props> { +const Pager: FC<Props> = ({ + offset = 0, + limit = 0, + total = 0, + isShort = false, + pageLimit = 10, + onChange, +}) => { - public static defaultProps = { - pageLimit: 10, - }; + offset = Number(offset) || 0; + total = Number(total) || 0; - render () { - const { pageLimit, limit, isShort } = this.props; - const offset = Number(this.props.offset) || 0; - const total = Number(this.props.total) || 0; - const pages = Math.ceil(total / limit); - const page = Math.ceil(offset / limit) + 1; + const pages = Math.ceil(total / limit); + const page = Math.ceil(offset / limit) + 1; + const cn = [ 'pager' ]; - let pageCnt = Math.ceil(pageLimit / 2); - if (page < pageCnt) { - pageCnt = pageLimit - page; - }; + if (isShort) { + cn.push('isShort'); + }; - const start = Math.max(1, page - pageCnt); - const end = Math.min(pages, page + pageCnt); - const items = []; + let startPage = null; + let endPage = null; + let list = null; + let pageCnt = Math.ceil(pageLimit / 2); - for (let i = start; i <= end ; i++) { - items.push({ id: i }); - }; + if (page < pageCnt) { + pageCnt = pageLimit - page; + }; - const Item = (item) => ( - <div className={'pageItem ' + (item.id == page ? 'active' : '')} onClick={() => this.onChange(item.id)}> - {item.id} - </div> - ); + const start = Math.max(1, page - pageCnt); + const end = Math.min(pages, page + pageCnt); + const items = []; - let startPage = null; - let endPage = null; - let list = null; - - if (!isShort && (start > 1)) { - startPage = ( - <React.Fragment> - <Item id="1" /> - <div className="dots">...</div> - </React.Fragment> - ); - }; - - if (!isShort && (end < pages)) { - endPage = ( - <React.Fragment> - <div className="dots">...</div> - <Item id={pages} /> - </React.Fragment> - ); - }; - - if (isShort) { - list = <div className="pageItem list">{page} of {pages}</div>; - } else { - list = ( - <React.Fragment> - {items.map((item, i) => ( - <Item key={i} {...item} /> - ))} - </React.Fragment> - ); - }; - - if (items.length > 1) { - return ( - <div className={[ 'pager', (isShort ? 'isShort' : '') ].join(' ')}> - {isShort ? <Icon className={[ 'arrow', 'end', 'left', (page == 1 ? 'disabled' : '') ].join(' ')} onClick={() => this.onChange(1)} /> : ''} - <Icon className={[ 'arrow', 'left', (page == 1 ? 'disabled' : '') ].join(' ')} onClick={() => this.onChange(page - 1)} /> - - {startPage} - {list} - {endPage} - - <Icon className={[ 'arrow', 'right', (page == pages ? 'disabled' : '') ].join(' ')} onClick={() => this.onChange(page + 1)} /> - {isShort ? <Icon className={[ 'arrow', 'end', 'right', (page == pages ? 'disabled' : '') ].join(' ')} onClick={() => this.onChange(pages)} /> : ''} - </div> - ); - } else { - return null; - }; + for (let i = start; i <= end ; i++) { + items.push({ id: i }); }; - - onChange (page) { - const { onChange, limit, total } = this.props; + + const Item = (item) => ( + <div className={'pageItem ' + (item.id == page ? 'active' : '')} onClick={() => onChangeHandler(item.id)}> + {item.id} + </div> + ); + + const onChangeHandler = (page: number) => { const pages = Math.ceil(total / limit); page = Math.max(1, page); @@ -104,7 +61,70 @@ class Pager extends React.Component<Props> { onChange(page); }; + + if (!isShort && (start > 1)) { + startPage = ( + <React.Fragment> + <Item id="1" /> + <div className="dots">...</div> + </React.Fragment> + ); + }; + + if (!isShort && (end < pages)) { + endPage = ( + <React.Fragment> + <div className="dots">...</div> + <Item id={pages} /> + </React.Fragment> + ); + }; + + if (isShort) { + list = <div className="pageItem list">{page} of {pages}</div>; + } else { + list = ( + <React.Fragment> + {items.map((item, i) => ( + <Item key={i} {...item} /> + ))} + </React.Fragment> + ); + }; + if (items.length > 1) { + return ( + <div className={cn.join(' ')}> + {isShort ? ( + <Icon + className={[ 'arrow', 'end', 'left', (page == 1 ? 'disabled' : '') ].join(' ')} + onClick={() => onChangeHandler(1)} + /> + ) : ''} + + <Icon + className={[ 'arrow', 'left', (page == 1 ? 'disabled' : '') ].join(' ')} + onClick={() => onChangeHandler(page - 1)} + /> + + {startPage} + {list} + {endPage} + + <Icon + className={[ 'arrow', 'right', (page == pages ? 'disabled' : '') ].join(' ')} + onClick={() => onChangeHandler(page + 1)} + /> + + {isShort ? ( + <Icon className={[ 'arrow', 'end', 'right', (page == pages ? 'disabled' : '') ].join(' ')} + onClick={() => onChangeHandler(pages)} /> + ) : ''} + </div> + ); + } else { + return null; + }; }; export default Pager; \ No newline at end of file diff --git a/src/ts/component/util/progress.tsx b/src/ts/component/util/progress.tsx index 8fceecdaf4..b693d11878 100644 --- a/src/ts/component/util/progress.tsx +++ b/src/ts/component/util/progress.tsx @@ -1,172 +1,151 @@ -import * as React from 'react'; +import React, { FC, useRef, useEffect } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { Icon, Label, Error } from 'Component'; import { I, S, U, C, J, Storage, keyboard, translate } from 'Lib'; -const Progress = observer(class Progress extends React.Component { - - _isMounted = false; - node: any = null; - obj: any = null; - dx = 0; - dy = 0; - width = 0; - height = 0; - - constructor (props: any) { - super(props); - - this.onCancel = this.onCancel.bind(this); - this.onDragStart = this.onDragStart.bind(this); - }; - - render () { - const { show } = S.Progress; - const list = S.Progress.getList(); - const cn = [ 'progress' ]; - - if (!show || !list.length) { - return null; - }; - - const Item = (item: any) => { - const percent = item.total > 0 ? Math.min(100, Math.ceil(item.current / item.total * 100)) : 0; - const isError = item.state == I.ProgressState.Error; - const canCancel = item.canCancel && !isError; - - return ( - <div className="item"> - <div className="nameWrap"> - <Label text={translate(U.Common.toCamelCase(`progress-${item.type}`))} /> - {canCancel ? <Icon className="close" onClick={() => this.onCancel(item.id)} /> : ''} - </div> +const Progress: FC = observer(() => { + + const { show } = S.Progress; + const skipState = [ I.ProgressState.Done, I.ProgressState.Canceled ]; + const skipType = [ I.ProgressType.Migrate ]; + const list = S.Progress.getList(it => !skipType.includes(it.type) && !skipState.includes(it.state)); + const percent = S.Progress.getPercent(list); + const nodeRef = useRef(null); + const innerRef = useRef(null); + const dx = useRef(0); + const dy = useRef(0); + const width = useRef(0); + const height = useRef(0); + const resizeObserver = new ResizeObserver(() => resize()); + const cn = [ 'progress' ]; + + const Item = (item: any) => { + const percent = item.total > 0 ? Math.min(100, Math.ceil(item.current / item.total * 100)) : 0; + const isError = item.state == I.ProgressState.Error; + const canCancel = item.canCancel && !isError; - {isError ? ( - <Error text={item.error} /> - ) : ( - <div className="bar"> - <div className="fill" style={{width: `${percent}%` }} /> - </div> - )} + return ( + <div className="item"> + <div className="nameWrap"> + <Label text={translate(U.Common.toCamelCase(`progress-${item.type}`))} /> + {canCancel ? <Icon className="close" onClick={() => onCancel(item.id)} /> : ''} </div> - ); - }; - return ( - <div - ref={node => this.node = node} - className={cn.join(' ')} - > - <div id="inner" className="inner" onMouseDown={this.onDragStart}> - <div className="titleWrap"> - <Label text={translate('commonProgress')} /> - <Label className="percent" text={`${S.Progress.getPercent()}%`} /> + {isError ? ( + <Error text={item.error} /> + ) : ( + <div className="bar"> + <div className="fill" style={{width: `${percent}%` }} /> </div> - <div className="items"> - {list.map(item => <Item key={item.id} {...item} />)} - </div> - </div> + )} </div> ); }; - componentDidMount () { - this._isMounted = true; - this.resize(); - }; - - componentDidUpdate () { - const win = $(window); - - this.resize(); - win.off('resize.progress').on('resize.progress', () => this.resize()); - }; - - componentWillUnmount () { - this._isMounted = false; - $(window).off('resize.progress'); - }; - - onCancel (id: string) { + const onCancel = (id: string) => { C.ProcessCancel(id); }; - onDragStart (e: any) { + const onDragStart = (e: any) => { const win = $(window); - const offset = this.obj.offset(); + const offset = $(innerRef.current).offset(); - this.dx = e.pageX - offset.left; - this.dy = e.pageY - offset.top; + dx.current = e.pageX - offset.left; + dy.current = e.pageY - offset.top; keyboard.disableSelection(true); keyboard.setDragging(true); win.off('mousemove.progress mouseup.progress'); - win.on('mousemove.progress', e => this.onDragMove(e)); - win.on('mouseup.progress', e => this.onDragEnd(e)); + win.on('mousemove.progress', e => onDragMove(e)); + win.on('mouseup.progress', e => onDragEnd(e)); }; - onDragMove (e: any) { + const onDragMove = (e: any) => { const win = $(window); - const x = e.pageX - this.dx - win.scrollLeft(); - const y = e.pageY - this.dy - win.scrollTop(); + const x = e.pageX - dx.current - win.scrollLeft(); + const y = e.pageY - dy.current - win.scrollTop(); - this.setStyle(x, y); + setStyle(x, y); Storage.set('progress', { x, y }, true); }; - onDragEnd (e: any) { + const onDragEnd = (e: any) => { keyboard.disableSelection(false); keyboard.setDragging(false); $(window).off('mousemove.progress mouseup.progress'); }; - checkCoords (x: number, y: number): { x: number, y: number } { + const checkCoords = (x: number, y: number): { x: number, y: number } => { const { ww, wh } = U.Common.getWindowDimensions(); - this.width = Number(this.width) || 0; - this.height = Number(this.height) || 0; + width.current = Number(width.current) || 0; + height.current = Number(height.current) || 0; x = Number(x) || 0; x = Math.max(0, x); - x = Math.min(ww - this.width, x); + x = Math.min(ww - width.current, x); y = Number(y) || 0; y = Math.max(J.Size.header, y); - y = Math.min(wh - this.height, y); + y = Math.min(wh - height.current, y); return { x, y }; }; - resize () { - if (!this._isMounted) { - return; - }; - - const node = $(this.node); + const resize = () => { + const obj = $(innerRef.current); const coords = Storage.get('progress'); - this.obj = node.find('#inner'); - if (!this.obj.length) { - return; - }; - - this.height = this.obj.outerHeight(); - this.width = this.obj.outerWidth(); + height.current = obj.outerHeight(); + width.current = obj.outerWidth(); if (coords) { - this.setStyle(coords.x, coords.y); + setStyle(coords.x, coords.y); }; }; - setStyle (x: number, y: number) { - const coords = this.checkCoords(x, y); + const setStyle = (x: number, y: number) => { + const coords = checkCoords(x, y); - this.obj.css({ margin: 0, left: coords.x, top: coords.y, bottom: 'auto' }); + $(innerRef.current).css({ margin: 0, left: coords.x, top: coords.y, bottom: 'auto' }); }; - + + useEffect(() => { + if (nodeRef.current) { + resizeObserver.observe(nodeRef.current); + }; + + resize(); + + return () => { + if (nodeRef.current) { + resizeObserver.unobserve(nodeRef.current); + }; + }; + }); + + useEffect(() => resize(), [ list.length ]); + + return show && list.length ? ( + <div + ref={nodeRef} + className={cn.join(' ')} + > + <div ref={innerRef} className="inner" onMouseDown={onDragStart}> + <div className="titleWrap"> + <Label text={translate('commonProgress')} /> + <Label className="percent" text={`${percent}%`} /> + </div> + <div className="items"> + {list.map(item => <Item key={item.id} {...item} />)} + </div> + </div> + </div> + ) : null; + }); export default Progress; \ No newline at end of file diff --git a/src/ts/component/util/progressBar.tsx b/src/ts/component/util/progressBar.tsx index 4d3b3e4d6e..7d3ed44ce3 100644 --- a/src/ts/component/util/progressBar.tsx +++ b/src/ts/component/util/progressBar.tsx @@ -1,65 +1,70 @@ -import * as React from 'react'; +import React, { FC, MouseEvent } from 'react'; +import $ from 'jquery'; import { Label } from 'Component'; -import { Preview } from 'Lib'; +import { Preview, U } from 'Lib'; + +interface Segment { + name: string; + caption: string; + percent: number; + isActive: boolean; +}; interface Props { - segments: { name: string; caption: string; percent: number; isActive: boolean; }[]; + segments: Segment[]; current?: string; max?: string; }; -class ProgressBar extends React.Component<Props> { - - node: any = null; - - constructor (props: Props) { - super(props); +const ProgressBar: FC<Props> = ({ + segments = [], + current = '', + max = '', +}) => { + const total = segments.reduce((res, current) => res += current.percent, 0); + const onTooltipShow = (e: MouseEvent, item: Segment) => { + const name = U.Common.htmlSpecialChars(item.name); + const caption = U.Common.htmlSpecialChars(item.caption); + const t = Preview.tooltipCaption(name, caption); - this.onTooltipShow = this.onTooltipShow.bind(this); + if (t) { + Preview.tooltipShow({ text: t, element: $(e.currentTarget) }); + }; }; - render () { - const { segments, current, max } = this.props; - const total = segments.reduce((res, current) => res += current.percent, 0); - - const Item = (item: any) => { - const cn = [ 'fill' ]; - if (item.isActive) { - cn.push('isActive'); - }; + const Item = (item: any) => { + const cn = [ 'fill' ]; - return ( - <div - className={cn.join(' ')} - style={{ width: `${item.percent * 100}%` }} - onMouseEnter={e => this.onTooltipShow(e, item)} - onMouseLeave={() => Preview.tooltipHide(false)} - /> - ); + if (item.isActive) { + cn.push('isActive'); }; return ( - <div className="progressBar"> - <div className="bar"> - {segments.map((item, i) => ( - <Item key={i} {...item} /> - ))} - <div className="fill empty" style={{ width: `${(1 - total) * 100}%` }} /> - </div> - - <div className="labels"> - {current ? <Label className="current" text={current} /> : ''} - {max ? <Label className="max" text={max} /> : '' } - </div> - </div> + <div + className={cn.join(' ')} + style={{ width: `${item.percent * 100}%` }} + onMouseEnter={e => onTooltipShow(e, item)} + onMouseLeave={() => Preview.tooltipHide(false)} + /> ); }; - onTooltipShow (e: any, item: any) { - const t = Preview.tooltipCaption(item.name, item.caption); - Preview.tooltipShow({ text: t, element: $(e.currentTarget) }); - }; - + return ( + <div className="progressBar"> + <div className="bar"> + {segments.map((item, i) => ( + <Item key={i} {...item} /> + ))} + <div className="fill empty" style={{ width: `${(1 - total) * 100}%` }} /> + </div> + + <div className="labels"> + {current ? <Label className="current" text={current} /> : ''} + {max ? <Label className="max" text={max} /> : '' } + </div> + </div> + ); + }; export default ProgressBar; \ No newline at end of file diff --git a/src/ts/component/util/qr.tsx b/src/ts/component/util/qr.tsx new file mode 100644 index 0000000000..eba8acc886 --- /dev/null +++ b/src/ts/component/util/qr.tsx @@ -0,0 +1,24 @@ +import React, { forwardRef } from 'react'; +import { J, S } from 'Lib'; +import QRCode from 'qrcode.react'; + +interface Props { + value: string; + size?: number; +}; + +const QR = forwardRef<HTMLDivElement, Props>(({ + value = '', + size = 122, +}, ref) => { + const theme = S.Common.getThemeClass(); + + return ( + <div className="qrInner"> + <QRCode value={value} fgColor={J.Theme[theme].qr.foreground} bgColor={J.Theme[theme].qr.bg} size={size} /> + </div> + ); + +}); + +export default QR; diff --git a/src/ts/component/util/share/banner.tsx b/src/ts/component/util/share/banner.tsx index b4617b4f48..39cdc084c3 100644 --- a/src/ts/component/util/share/banner.tsx +++ b/src/ts/component/util/share/banner.tsx @@ -1,59 +1,52 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; +import React, { FC, MouseEvent } from 'react'; import { Icon, Label } from 'Component'; -import { S, translate, U, Storage, analytics } from 'Lib'; +import { S, translate, Storage, analytics } from 'Lib'; interface Props { - onClose: () => void; + onClose?: () => void; }; -const ShareBanner = observer(class ShareBanner extends React.Component<Props, {}> { +const ShareBanner: FC<Props> = ({ + onClose, +}) => { - node: any = null; - - constructor (props: Props) { - super(props); - - this.onClose = this.onClose.bind(this); - }; - - render () { - if (!U.Space.isShareBanner()) { - return null; - }; - - return ( - <div - ref={ref => this.node = ref} - id="shareBanner" - className="shareBanner" - onClick={this.onClick} - > - <Icon className="close" onClick={this.onClose} /> - <div className="bannerInner"> - <Label text={translate('shareBannerLabel')} /> - <Icon className="smile" /> - </div> - </div> - ); - }; - - onClick () { - S.Popup.open('settings', { data: { page: 'spaceShare', isSpace: true }, className: 'isSpace' }); + const onClickHandler = () => { + S.Popup.open('settings', { + className: 'isSpace', + data: { + page: 'spaceShare', + isSpace: true, + }, + }); analytics.event('ClickOnboardingTooltip', { id: 'SpaceShare', type: 'OpenSettings' }); }; - onClose (e) { + const onCloseHandler = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); Storage.set('shareBannerClosed', true); - this.props.onClose(); + if (onClose) { + onClose(); + }; analytics.event('ClickOnboardingTooltip', { id: 'SpaceShare', type: 'Close' }); }; -}); + return ( + <div + id="shareBanner" + className="shareBanner" + onClick={onClickHandler} + > + <Icon className="close" onClick={onCloseHandler} /> + <div className="bannerInner"> + <Label text={translate('shareBannerLabel')} /> + <Icon className="smile" /> + </div> + </div> + ); +}; -export default ShareBanner; +export default ShareBanner; \ No newline at end of file diff --git a/src/ts/component/util/share/tooltip.tsx b/src/ts/component/util/share/tooltip.tsx index a4110fff2c..95c1c8a443 100644 --- a/src/ts/component/util/share/tooltip.tsx +++ b/src/ts/component/util/share/tooltip.tsx @@ -1,60 +1,30 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { observer } from 'mobx-react'; import { Icon, Label } from 'Component'; -import { U, S, translate, analytics, Preview, Storage } from 'Lib'; +import { S, translate, analytics } from 'Lib'; interface Props { + route: string; showOnce?: boolean; }; -const ShareTooltip = observer(class ShareTooltip extends React.Component<Props, {}> { - - node: any = null; - - constructor (props: Props) { - super(props); - - this.onClick = this.onClick.bind(this); - }; - - render () { - const { showOnce } = this.props; - - if (showOnce && !S.Common.shareTooltip) { - return null; - }; - - return ( - <div - ref={ref => this.node = ref} - id="shareTooltip" - className="shareTooltip" - onClick={this.onClick} - > - <Icon className="close" onClick={this.onClose} /> - <Icon className="smile" /> - <Label text={translate('shareTooltipLabel')} /> - </div> - ); - }; - - onClick () { - const { showOnce } = this.props; - +const ShareTooltip: FC<Props> = observer(({ + route = '', +}) => { + const onClickHandler = () => { S.Popup.open('share', {}); - Preview.shareTooltipHide(); - - analytics.event('ClickShareApp', { route: showOnce ? 'Onboarding' : 'Help' }); - }; - - onClose (e) { - e.preventDefault(); - e.stopPropagation(); - - Preview.shareTooltipHide(); - Storage.set('shareTooltip', true); + analytics.event('ClickShareApp', { route }); }; + return ( + <div + className="shareTooltip" + onClick={onClickHandler} + > + <Icon className="smile" /> + <Label text={translate('shareTooltipLabel')} /> + </div> + ); }); -export default ShareTooltip; +export default ShareTooltip; \ No newline at end of file diff --git a/src/ts/component/util/sync.tsx b/src/ts/component/util/sync.tsx index 74a3b6881d..80ead214cf 100644 --- a/src/ts/component/util/sync.tsx +++ b/src/ts/component/util/sync.tsx @@ -1,5 +1,5 @@ import React, { forwardRef, useRef, MouseEvent } from 'react'; -import { useLocalObservable } from 'mobx-react'; +import { observer } from 'mobx-react'; import { Icon, Label } from 'Component'; import { I, S, U, analytics, translate } from 'Lib'; @@ -9,17 +9,14 @@ interface Props { onClick: (e: any) => void; }; -const Sync = forwardRef<HTMLDivElement, Props>(({ +const Sync = observer(forwardRef<HTMLDivElement, Props>(({ id = '', className = '', onClick, }, ref) => { const nodeRef = useRef<HTMLDivElement>(null); - const { account, syncStatus } = useLocalObservable(() => ({ - syncStatus: S.Auth.getSyncStatus(), - account: S.Auth.account, - })); + const syncStatus = S.Auth.getSyncStatus(); const isDevelopment = U.Data.isDevelopmentNetwork(); const cn = [ 'sync', className ]; @@ -56,6 +53,6 @@ const Sync = forwardRef<HTMLDivElement, Props>(({ </div> ); -}); +})); export default Sync; \ No newline at end of file diff --git a/src/ts/component/util/tag.tsx b/src/ts/component/util/tag.tsx index 5960f88ab4..62385042b8 100644 --- a/src/ts/component/util/tag.tsx +++ b/src/ts/component/util/tag.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC, useRef, useEffect, MouseEvent } from 'react'; import $ from 'jquery'; interface Props { @@ -12,91 +12,77 @@ interface Props { onContextMenu?(e: any): void; }; -class Tag extends React.Component<Props> { - - node: any = null; - - constructor (props: Props) { - super(props); - - this.onRemove = this.onRemove.bind(this); - }; - - render () { - const { id, text, color, className, canEdit, onClick, onContextMenu } = this.props; - const cn = [ 'tagItem', 'tagColor', 'tagColor-' + (color || 'default') ]; - - if (className) { - cn.push(className); - }; - if (canEdit) { - cn.push('canEdit'); - }; - - let icon = null; - if (canEdit) { - icon = ( - <div className="tagRemove" onMouseDown={this.onRemove}> - <img id="remove" /> - </div> - ); - }; - - return ( - <span - id={id} - ref={node => this.node = node} - contentEditable={false} - className={cn.join(' ')} - onContextMenu={onContextMenu} - onClick={onClick} - > - <span className="inner">{text}</span> - {icon} - </span> - ); +const Tag: FC<Props> = ({ + id = '', + text = '', + className = '', + color = '', + canEdit = false, + onClick, + onRemove, + onContextMenu, +}) => { + + const nodeRef = useRef(null); + const cn = [ 'tagItem', 'tagColor', 'tagColor-' + (color || 'default') ]; + + if (className) { + cn.push(className); }; - - componentDidMount () { - this.setColor(); + if (canEdit) { + cn.push('canEdit'); }; - componentDidUpdate () { - this.setColor(); - }; + const onRemoveHandler = (e: MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); - setColor () { - const { canEdit } = this.props; - if (!canEdit) { - return; + if (canEdit && onRemove) { + onRemove(e); }; - - const node = $(this.node); - const remove = node.find('#remove'); - const color = node.css('color'); - - remove.attr({ src: this.removeSvg(color) }); }; - removeSvg (color: any) { + const removeSvg = (color: any) => { const svg = ` <svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M2.44194 1.55806C2.19786 1.31398 1.80214 1.31398 1.55806 1.55806C1.31398 1.80214 1.31398 2.19786 1.55806 2.44194L4.11612 5L1.55806 7.55806C1.31398 7.80214 1.31398 8.19786 1.55806 8.44194C1.80214 8.68602 2.19786 8.68602 2.44194 8.44194L5 5.88388L7.55806 8.44194C7.80214 8.68602 8.19786 8.68602 8.44194 8.44194C8.68602 8.19786 8.68602 7.80214 8.44194 7.55806L5.88388 5L8.44194 2.44194C8.68602 2.19786 8.68602 1.80214 8.44194 1.55806C8.19786 1.31398 7.80214 1.31398 7.55806 1.55806L5 4.11612L2.44194 1.55806Z" fill="${color}"/> </svg> `; - return 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svg))); + return `data:image/svg+xml;base64,${btoa(svg)}`; }; - onRemove (e: any) { - e.preventDefault(); - e.stopPropagation(); + const setColor = () => { + const node = $(nodeRef.current); + const remove = node.find('#remove'); + const color = node.css('color'); - const { canEdit, onRemove } = this.props; - if (canEdit && onRemove) { - onRemove(e); - }; + remove.attr({ src: removeSvg(color) }); }; + + let icon = null; + if (canEdit) { + icon = ( + <div className="tagRemove" onMouseDown={onRemoveHandler}> + <img id="remove" /> + </div> + ); + }; + + useEffect(() => setColor()); + return ( + <span + id={id} + ref={nodeRef} + contentEditable={false} + className={cn.join(' ')} + onContextMenu={onContextMenu} + onClick={onClick} + > + <span className="inner">{text}</span> + {icon} + </span> + ); }; export default Tag; \ No newline at end of file diff --git a/src/ts/component/util/toast.tsx b/src/ts/component/util/toast.tsx index d0741df5ce..0525de30aa 100644 --- a/src/ts/component/util/toast.tsx +++ b/src/ts/component/util/toast.tsx @@ -1,171 +1,121 @@ -import * as React from 'react'; +import React, { FC, useRef, useEffect, MouseEvent } from 'react'; import { observer } from 'mobx-react'; -import { Button, IconObject, ObjectName } from 'Component'; -import { I, S, U, Preview, Action, translate, keyboard, analytics } from 'Lib'; - -interface State { - object: any; - target: any; - origin: any; -}; - -const Toast = observer(class Toast extends React.Component<object, State> { - - state = { - object: null, - target: null, - origin: null, - }; - - constructor (props: any) { - super(props); - - this.close = this.close.bind(this); - }; - - render () { - const { toast } = S.Common; - if (!toast) { - return null; +import $ from 'jquery'; +import raf from 'raf'; +import { Button, IconObject, ObjectName, Icon } from 'Component'; +import { I, S, U, Preview, Action, translate, keyboard, analytics, sidebar } from 'Lib'; + +const Toast: FC = observer(() => { + const nodeRef = useRef(null); + const { toast } = S.Common; + const { count, action, text, value, object, target, origin, ids, icon } = toast || {}; + + let buttons = []; + let textObject = null; + let textAction = null; + let textOrigin = null; + let textActionTo = null; + let textTarget = null; + + const Element = (item: any) => ( + <div className="chunk"> + <IconObject object={item} size={18} /> + <ObjectName object={item} /> + </div> + ); + + switch (action) { + default: { + textAction = text; + break; }; - const { count, action, text, value, object, target, origin, ids } = toast; - - let buttons = []; - let textObject = null; - let textAction = null; - let textOrigin = null; - let textActionTo = null; - let textTarget = null; - - const Element = (item: any) => ( - <div className="chunk"> - <IconObject object={item} size={18} /> - <ObjectName object={item} /> - </div> - ); - - switch (action) { - default: { - textAction = text; - break; - }; + case I.ToastAction.Lock: { + textObject = object ? <Element {...object} /> : translate('commonObject'); + textAction = translate(value ? 'toastIsLocked' : 'toastIsUnlocked'); + break; + }; - case I.ToastAction.Lock: { - textObject = object ? <Element {...object} /> : translate('commonObject'); - textAction = translate(value ? 'toastIsLocked' : 'toastIsUnlocked'); + case I.ToastAction.Move: { + if (!target) { break; }; - case I.ToastAction.Move: { - if (!target) { - break; - }; - - const cnt = `${count} ${U.Common.plural(count, translate('pluralBlock'))}`; + const cnt = `${count} ${U.Common.plural(count, translate('pluralBlock'))}`; - textAction = U.Common.sprintf(translate('toastMovedTo'), cnt); - textTarget = <Element {...target} />; + textAction = U.Common.sprintf(translate('toastMovedTo'), cnt); + textTarget = <Element {...target} />; - if (origin) { - textAction = U.Common.sprintf(translate('toastMovedFrom'), cnt); - textActionTo = translate('commonTo'); - textOrigin = <Element {...origin} />; - }; - - buttons = buttons.concat([ - { action: 'open', label: translate('commonOpen') }, - { action: 'undo', label: translate('commonUndo') } - ]); - break; + if (origin) { + textAction = U.Common.sprintf(translate('toastMovedFrom'), cnt); + textActionTo = translate('commonTo'); + textOrigin = <Element {...origin} />; }; - case I.ToastAction.Collection: - case I.ToastAction.Link: { - if (!object || !target) { - break; - }; - - textAction = translate(action == I.ToastAction.Collection ? 'toastAddedToCollection' : 'toastLinkedTo'); - textObject = <Element {...object} />; - textTarget = <Element {...target} />; + buttons = buttons.concat([ + { action: 'open', label: translate('commonOpen') }, + { action: 'undo', label: translate('commonUndo') } + ]); + break; + }; - if (target.id != keyboard.getRootId()) { - buttons = buttons.concat([ - { action: 'open', label: translate('commonOpen') } - ]); - }; + case I.ToastAction.Collection: + case I.ToastAction.Link: { + if (!object || !target) { break; }; - case I.ToastAction.StorageFull: { - textAction = translate('toastUploadLimitExceeded'); + textAction = translate(action == I.ToastAction.Collection ? 'toastAddedToCollection' : 'toastLinkedTo'); + textObject = <Element {...object} />; + textTarget = <Element {...target} />; - buttons = buttons.concat([ - { action: 'manageStorage', label: translate('toastManageFiles') } + if (target.id != keyboard.getRootId()) { + buttons = buttons.concat([ + { action: 'open', label: translate('commonOpen') } ]); }; + break; + }; - case I.ToastAction.TemplateCreate: { - if (!object) { - break; - }; + case I.ToastAction.StorageFull: { + textAction = translate('toastUploadLimitExceeded'); + + buttons = buttons.concat([ + { action: 'manageStorage', label: translate('toastManageFiles') } + ]); + }; - textObject = <Element {...object} />; - textAction = translate('toastTemplateCreate'); + case I.ToastAction.TemplateCreate: { + if (!object) { break; }; - case I.ToastAction.Archive: { - if (!ids) { - break; - }; - - const cnt = `${ids.length} ${U.Common.plural(ids.length, translate('pluralObject'))}`; - textAction = U.Common.sprintf(translate('toastMovedToBin'), cnt); + textObject = <Element {...object} />; + textAction = translate('toastTemplateCreate'); + break; + }; - buttons = buttons.concat([ - { action: 'undoArchive', label: translate('commonUndo'), data: ids } - ]); + case I.ToastAction.Archive: { + if (!ids) { break; }; - }; - return ( - <div id="toast" className="toast" onClick={this.close}> - <div className="inner"> - <div className="message"> - {textObject} - {textAction ? <span dangerouslySetInnerHTML={{ __html: U.Common.sanitize(textAction) }} /> : ''} - {textOrigin} - {textActionTo ? <span dangerouslySetInnerHTML={{ __html: U.Common.sanitize(textActionTo) }} /> : ''} - {textTarget} - </div> + const cnt = `${ids.length} ${U.Common.plural(ids.length, translate('pluralObject'))}`; + textAction = U.Common.sprintf(translate('toastMovedToBin'), cnt); - {buttons.length ? ( - <div className="buttons"> - {buttons.map((item: any, i: number) => ( - <Button key={i} text={item.label} onClick={e => this.onClick(e, item)} /> - ))} - </div> - ) : ''} - </div> - </div> - ); - }; - - componentDidUpdate () { - Preview.toastPosition(); + buttons = buttons.concat([ + { action: 'undoArchive', label: translate('commonUndo'), data: ids } + ]); + break; + }; }; - close () { - Preview.toastHide(true); - }; + const onCloseHandler = () => Preview.toastHide(true); - onClick (e: any, item: any) { + const onClickHandler = (e: MouseEvent, item: any) => { switch (item.action) { case 'open': { - this.onOpen(e); + U.Object.openEvent(e, S.Common.toast.target); break; }; @@ -187,12 +137,47 @@ const Toast = observer(class Toast extends React.Component<object, State> { }; }; - this.close(); + onCloseHandler(); }; - onOpen (e: any) { - U.Object.openEvent(e, S.Common.toast.target); - }; + useEffect(() => { + const node = $(nodeRef.current); + const { ww } = U.Common.getWindowDimensions(); + const y = 32; + const sw = sidebar.getDummyWidth(); + const x = (ww - sw) / 2 - node.outerWidth() / 2 + sw; + + node.show().css({ opacity: 0, transform: 'scale3d(0.7,0.7,1)' }); + + raf(() => { + node.css({ left: x, top: y, opacity: 1, transform: 'scale3d(1,1,1)' }); + }); + }); + + return toast ? ( + <div ref={nodeRef} id="toast" className="toast" onClick={onCloseHandler}> + <div className="inner"> + + {icon ? <Icon className={icon} /> : ''} + + <div className="message"> + {textObject} + {textAction ? <span dangerouslySetInnerHTML={{ __html: U.Common.sanitize(textAction) }} /> : ''} + {textOrigin} + {textActionTo ? <span dangerouslySetInnerHTML={{ __html: U.Common.sanitize(textActionTo) }} /> : ''} + {textTarget} + </div> + + {buttons.length ? ( + <div className="buttons"> + {buttons.map((item: any, i: number) => ( + <Button key={i} text={item.label} onClick={e => onClickHandler(e, item)} /> + ))} + </div> + ) : ''} + </div> + </div> + ) : null; }); diff --git a/src/ts/component/vault/index.tsx b/src/ts/component/vault/index.tsx index 728b970d08..53d0b201fa 100644 --- a/src/ts/component/vault/index.tsx +++ b/src/ts/component/vault/index.tsx @@ -296,7 +296,7 @@ const Vault = observer(class Vault extends React.Component { classNameWrap: 'fromSidebar', element: `#vault #item-${item.id}`, vertical: I.MenuDirection.Center, - route: analytics.route.navigation, + route: analytics.route.vault, }); }; diff --git a/src/ts/component/vault/item.tsx b/src/ts/component/vault/item.tsx index 900c606c8a..190b62bec6 100644 --- a/src/ts/component/vault/item.tsx +++ b/src/ts/component/vault/item.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { FC } from 'react'; import { observer } from 'mobx-react'; import { IconObject } from 'Component'; import { U } from 'Lib'; @@ -12,37 +12,38 @@ interface Props { onContextMenu: (e: any) => void; }; -const VaultItem = observer(class Vault extends React.Component<Props> { - - render () { - const { id, isButton, onClick, onMouseEnter, onMouseLeave, onContextMenu } = this.props; - const cn = [ 'item' ]; +const VaultItem: FC<Props> = observer(({ + id = '', + isButton = false, + onClick, + onMouseEnter, + onMouseLeave, + onContextMenu, +}) => { + const cn = [ 'item' ]; - let icon = null; - - if (!isButton) { - const object = U.Menu.getVaultItems().find(it => it.id == id); - icon = <IconObject object={object} size={36} iconSize={36} />; - } else { - cn.push(`isButton ${id}`); - }; - - return ( - <div - id={`item-${id}`} - className={cn.join(' ')} - onClick={onClick} - onMouseEnter={onMouseEnter} - onMouseLeave={onMouseLeave} - onContextMenu={onContextMenu} - > - <div className="iconWrap"> - {icon} - </div> - </div> - ); + let icon = null; + if (!isButton) { + const object = U.Menu.getVaultItems().find(it => it.id == id); + icon = <IconObject object={object} size={36} iconSize={36} />; + } else { + cn.push(`isButton ${id}`); }; + return ( + <div + id={`item-${id}`} + className={cn.join(' ')} + onClick={onClick} + onMouseEnter={onMouseEnter} + onMouseLeave={onMouseLeave} + onContextMenu={onContextMenu} + > + <div className="iconWrap"> + {icon} + </div> + </div> + ); }); export default VaultItem; \ No newline at end of file diff --git a/src/ts/component/widget/buttons.tsx b/src/ts/component/widget/buttons.tsx deleted file mode 100644 index b55b563c81..0000000000 --- a/src/ts/component/widget/buttons.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import * as React from 'react'; -import { observer } from 'mobx-react'; -import { Icon } from 'Component'; -import { I, S, U, sidebar, translate } from 'Lib'; - -const WidgetButtons = observer(class WidgetSpace extends React.Component<I.WidgetComponent> { - - isSubcribed = false; - - constructor (props: I.WidgetComponent) { - super(props); - - this.onClick = this.onClick.bind(this); - }; - - render (): React.ReactNode { - const items = this.getItems(); - const space = U.Space.getSpaceview(); - const participants = U.Space.getParticipantsList([ I.ParticipantStatus.Active ]); - - return ( - <div className="body"> - {items.map((item, i) => { - let button = null; - let cnt = null; - - if (item.id == 'member') { - if (space.isShared) { - cnt = <div className="cnt">{participants.length}</div>; - } else { - button = <div className="btn">{translate('commonShare')}</div>; - }; - }; - - return ( - <div - key={i} - id={`item-${item.id}`} - className="item" - onClick={e => this.onClick(e, item)} - > - <div className="side left"> - <Icon className={item.id} /> - <div className="name"> - {item.name} - {cnt} - </div> - </div> - <div className="side right"> - {button} - </div> - </div> - ); - })} - </div> - ); - }; - - getItems () { - const space = U.Space.getSpaceview(); - const ret = [ - { id: 'all', name: translate('commonAllContent') }, - ]; - - if (space.isShared) { - ret.unshift({ id: 'member', name: translate('commonMembers') }); - }; - - if (space.chatId && U.Common.isChatAllowed()) { - ret.push({ id: 'chat', name: translate('commonMainChat') }); - }; - - return ret; - }; - - onClick (e: any, item: any) { - e.preventDefault(); - e.stopPropagation(); - - const space = U.Space.getSpaceview(); - - switch (item.id) { - case 'member': { - S.Popup.open('settings', { data: { page: 'spaceShare', isSpace: true }, className: 'isSpace' }); - break; - }; - - case 'all': { - sidebar.objectContainerToggle(); - break; - }; - - case 'chat': { - U.Object.openAuto({ id: S.Block.workspace, layout: I.ObjectLayout.Chat }); - break; - }; - }; - }; - -}); - -export default WidgetButtons; \ No newline at end of file diff --git a/src/ts/component/widget/index.tsx b/src/ts/component/widget/index.tsx index dfd38fbd51..3eba05bdd3 100644 --- a/src/ts/component/widget/index.tsx +++ b/src/ts/component/widget/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect, useState, MouseEvent } from 'react'; import $ from 'jquery'; import raf from 'raf'; import { observer } from 'mobx-react'; @@ -6,7 +6,6 @@ import { Icon, ObjectName, DropTarget } from 'Component'; import { C, I, S, U, J, translate, Storage, Action, analytics, Dataview, keyboard, Relation } from 'Lib'; import WidgetSpace from './space'; -import WidgetButtons from './buttons'; import WidgetView from './view'; import WidgetTree from './tree'; @@ -19,340 +18,119 @@ interface Props extends I.WidgetComponent { onDragOver?: (e: React.MouseEvent, blockId: string) => void; }; -const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { - - node = null; - ref = null; - timeout = 0; - subId = ''; - - constructor (props: Props) { - super(props); - - this.onSetPreview = this.onSetPreview.bind(this); - this.onRemove = this.onRemove.bind(this); - this.onClick = this.onClick.bind(this); - this.onOptions = this.onOptions.bind(this); - this.onToggle = this.onToggle.bind(this); - this.onDragEnd = this.onDragEnd.bind(this); - this.onContext = this.onContext.bind(this); - this.onCreateClick = this.onCreateClick.bind(this); - this.onCreate = this.onCreate.bind(this); - this.isSystemTarget = this.isSystemTarget.bind(this); - this.getData = this.getData.bind(this); - this.getLimit = this.getLimit.bind(this); - this.getTraceId = this.getTraceId.bind(this); - this.sortFavorite = this.sortFavorite.bind(this); - this.canCreate = this.canCreate.bind(this); - }; - - render () { - const { block, isPreview, isEditing, className, onDragStart, onDragOver, setPreview } = this.props; - const child = this.getTargetBlock(); - const root = ''; - const childrenIds = S.Block.getChildrenIds(root, root); - const { viewId } = block.content; - const object = this.getObject(); - const favCnt = this.getFavoriteIds().length; - const limit = this.getLimit(block.content); - - let layout = block.content.layout; - if (object) { - const layoutOptions = U.Menu.getWidgetLayoutOptions(object.id, object.layout).map(it => it.id); - - if (layoutOptions.length && !layoutOptions.includes(layout)) { - layout = layoutOptions[0]; - }; - }; - - const hasChild = ![ I.WidgetLayout.Space, I.WidgetLayout.Buttons ].includes(layout); - - if (!child && hasChild) { - return null; - }; - - const canWrite = U.Space.canMyParticipantWrite(); - const { targetBlockId } = child?.content || {}; - const cn = [ 'widget' ]; +const WidgetIndex = observer(forwardRef<{}, Props>((props, ref) => { - const withSelect = !this.isSystemTarget() && (!isPreview || !U.Common.isPlatformMac()); - const childKey = `widget-${child?.id}-${layout}`; - const canCreate = this.canCreate(); - const canDrop = object && !this.isSystemTarget() && !isEditing && S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Block ]); - const isFavorite = targetBlockId == J.Constant.widgetId.favorite; + const { block, isPreview, isEditing, className, setEditing, onDragStart, onDragOver, setPreview } = props; + const { viewId } = block.content; + const { root, widgets } = S.Block; + const childrenIds = S.Block.getChildrenIds(widgets, block.id); + const child = childrenIds.length ? S.Block.getLeaf(widgets, childrenIds[0]) : null; + const targetId = child ? child.getTargetObjectId() : ''; - const props = { - ...this.props, - ref: ref => this.ref = ref, - key: childKey, - parent: block, - block: child, - canCreate, - isSystemTarget: this.isSystemTarget, - getData: this.getData, - getLimit: this.getLimit, - getTraceId: this.getTraceId, - sortFavorite: this.sortFavorite, - addGroupLabels: this.addGroupLabels, - onContext: this.onContext, - onCreate: this.onCreate, - }; + const isSystemTarget = (): boolean => { + return child ? isSystemTargetId(child.getTargetObjectId()) : false; + }; - if (className) { - cn.push(className); - }; + const isSystemTargetId = (id: string): boolean => { + return U.Menu.isSystemWidget(id); + }; - if (isPreview) { - cn.push('isPreview'); + const getObject = () => { + if (!child) { + return null; }; - if (withSelect) { - cn.push('withSelect'); + let object = null; + if (isSystemTargetId(targetId)) { + object = { + id: targetId, + name: translate(U.Common.toCamelCase(`widget-${targetId}`)), + }; + } else { + object = S.Detail.get(widgets, targetId); }; + return object; + }; - let head = null; - let content = null; - let back = null; - let buttons = null; - let targetTop = null; - let targetBot = null; - let isDraggable = canWrite; - + const getLimit = ({ limit, layout }): number => { if (isPreview) { - back = ( - <div className="iconWrap back"> - <Icon - className="back" - onClick={() => { - setPreview(''); - analytics.event('ScreenHome', { view: 'Widget' }); - }} - /> - </div> - ); - - isDraggable = false; - } else { - buttons = ( - <div className="buttons"> - {isEditing ? ( - <div className="iconWrap more"> - <Icon className="options" tooltip={translate('widgetOptions')} onClick={this.onOptions} /> - </div> - ) : ''} - {canCreate ? ( - <div className="iconWrap create"> - <Icon className="plus" tooltip={translate('commonCreateNewObject')} onClick={this.onCreateClick} /> - </div> - ) : ''} - <div className="iconWrap collapse"> - <Icon className="collapse" tooltip={translate('widgetToggle')} onClick={this.onToggle} /> - </div> - </div> - ); + return J.Constant.limit.menuRecords; }; - if (hasChild) { - const onClick = this.isSystemTarget() ? this.onSetPreview : this.onClick; - - head = ( - <div className="head" onClick={onClick}> - {back} - <div className="clickable"> - <ObjectName object={object} /> - {isFavorite && (favCnt > limit) ? <span className="count">{favCnt}</span> : ''} - </div> - {buttons} - </div> - ); - - if (canDrop) { - head = ( - <DropTarget - cacheKey={[ block.id, object.id ].join('-')} - id={object.id} - rootId={targetBlockId} - targetContextId={object.id} - dropType={I.DropType.Menu} - canDropMiddle={true} - className="targetHead" - > - {head} - </DropTarget> - ); - }; - - targetTop = ( - <DropTarget - {...this.props} - isTargetTop={true} - rootId={S.Block.widgets} - id={block.id} - dropType={I.DropType.Widget} - canDropMiddle={false} - onClick={onClick} - /> - ); + const options = U.Menu.getWidgetLimitOptions(layout).map(it => Number(it.id)); - targetBot = ( - <DropTarget - {...this.props} - isTargetBottom={true} - rootId={S.Block.widgets} - id={block.id} - dropType={I.DropType.Widget} - canDropMiddle={false} - /> - ); + if (!limit || !options.includes(limit)) { + limit = options[0]; }; - switch (layout) { - case I.WidgetLayout.Space: { - cn.push('widgetSpace'); - content = <WidgetSpace {...props} />; - - isDraggable = false; - break; - }; - - case I.WidgetLayout.Buttons: { - cn.push('widgetButtons'); - content = <WidgetButtons {...props} />; - - isDraggable = false; - break; - }; - - case I.WidgetLayout.Link: { - cn.push('widgetLink'); - break; - }; + return limit; + }; - case I.WidgetLayout.Tree: { - cn.push('widgetTree'); - content = <WidgetTree {...props} />; - break; - }; + const object = getObject(); + const limit = getLimit(block.content); + const [ dummy, setDummy ] = useState(0); + const nodeRef = useRef(null); + const childRef = useRef(null); + const subId = useRef(''); + const timeout = useRef(0); + const recordIds = S.Record.getRecords(subId.current).filter(it => !it.isArchived && !it.isDeleted).map(it => it.id) + const isFavorite = targetId == J.Constant.widgetId.favorite; + const favCnt = isFavorite ? recordIds.length : 0; - case I.WidgetLayout.List: - case I.WidgetLayout.Compact: - case I.WidgetLayout.View: { - cn.push('widgetView'); - content = <WidgetView {...props} />; - break; - }; + let layout = block.content.layout; + if (object) { + const layoutOptions = U.Menu.getWidgetLayoutOptions(object.id, object.layout).map(it => it.id); + if (layoutOptions.length && !layoutOptions.includes(layout)) { + layout = layoutOptions[0]; }; - - return ( - <div - ref={node => this.node = node} - id={`widget-${block.id}`} - className={cn.join(' ')} - draggable={isDraggable} - onDragStart={e => onDragStart(e, block.id)} - onDragOver={e => onDragOver ? onDragOver(e, block.id) : null} - onDragEnd={this.onDragEnd} - onContextMenu={this.onOptions} - > - <Icon className="remove" inner={<div className="inner" />} onClick={this.onRemove} /> - - {head} - - <div id="wrapper" className="contentWrapper"> - {content} - </div> - - <div className="dimmer" /> - - {targetTop} - {targetBot} - </div> - ); }; - componentDidMount(): void { - this.rebind(); - this.forceUpdate(); - }; + const hasChild = ![ I.WidgetLayout.Space ].includes(layout); + const canWrite = U.Space.canMyParticipantWrite(); + const cn = [ 'widget' ]; + const withSelect = !isSystemTarget() && (!isPreview || !U.Common.isPlatformMac()); + const childKey = `widget-${child?.id}-${layout}`; + const canDrop = object && !isSystemTarget() && !isEditing && S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Block ]); - componentDidUpdate(): void { - this.initToggle(); - }; - - componentWillUnmount(): void { - this.unbind(); - window.clearTimeout(this.timeout); - }; - - unbind () { - const { block } = this.props; + const unbind = () => { const events = [ 'updateWidgetData', 'updateWidgetViews' ]; $(window).off(events.map(it => `${it}.${block.id}`).join(' ')); }; - rebind () { - const { block } = this.props; + const rebind = () => { const win = $(window); - this.unbind(); + unbind(); - win.on(`updateWidgetData.${block.id}`, () => this.ref && this.ref.updateData && this.ref.updateData()); - win.on(`updateWidgetViews.${block.id}`, () => this.ref && this.ref.updateViews && this.ref.updateViews()); + win.on(`updateWidgetData.${block.id}`, () => childRef.current?.updateData && childRef.current?.updateData()); + win.on(`updateWidgetViews.${block.id}`, () => childRef.current?.updateViews && childRef.current?.updateViews()); }; - getTargetBlock (): I.Block { - const { widgets } = S.Block; - const { block } = this.props; - const childrenIds = S.Block.getChildrenIds(widgets, block.id); - - return childrenIds.length ? S.Block.getLeaf(widgets, childrenIds[0]) : null; - }; - - getObject () { - const { widgets } = S.Block; - const child = this.getTargetBlock(); - - if (!child) { - return null; - }; - - const id = child.getTargetObjectId(); - - let object = null; - if (this.isSystemTargetId(id)) { - object = { id, name: translate(U.Common.toCamelCase(`widget-${id}`)) }; - } else { - object = S.Detail.get(widgets, id); - }; - return object; - }; - - onRemove (e: React.MouseEvent): void { + const onRemove = (e: MouseEvent): void => { e.stopPropagation(); - Action.removeWidget(this.props.block.id, this.getObject()); + Action.removeWidget(block.id, object); }; - onClick (e: React.MouseEvent): void { + const onClick = (e: MouseEvent): void => { if (!e.button) { - U.Object.openEvent(e, { ...this.getObject(), _routeParam_: { viewId: this.props.block.content.viewId } }); + U.Object.openEvent(e, { ...object, _routeParam_: { viewId: block.content.viewId } }); }; }; - onCreateClick (e: React.MouseEvent): void { + const onCreateClick = (e: MouseEvent): void => { e.preventDefault(); e.stopPropagation(); - this.onCreate(); + onCreate({ route: analytics.route.widget }); }; - onCreate (param?: any): void { + const onCreate = (param?: any): void => { param = param || {}; - const { block } = this.props; const { viewId, layout } = block.content; - const object = this.getObject(); + const route = param.route || analytics.route.widget; if (!object) { return; @@ -372,7 +150,7 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }; if (isSetOrCollection) { - const rootId = this.getRootId(); + const rootId = getRootId(); if (!rootId) { return; }; @@ -433,19 +211,19 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { const newObject = message.details; if (isFavorite) { - Action.setIsFavorite([ newObject.id ], true, analytics.route.widget); + Action.setIsFavorite([ newObject.id ], true, route); }; if (isCollection) { C.ObjectCollectionAdd(object.id, [ newObject.id ]); }; - U.Object.openAuto(newObject); - analytics.createObject(newObject.type, newObject.layout, analytics.route.widget, message.middleTime); + U.Object.openConfig(newObject); + analytics.createObject(newObject.type, newObject.layout, route, message.middleTime); }); }; - onOptions (e: React.MouseEvent): void { + const onOptions = (e: MouseEvent): void => { e.preventDefault(); e.stopPropagation(); @@ -453,14 +231,11 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { return; }; - const { block, setEditing } = this.props; - const object = this.getObject(); - const node = $(this.node); - if (!object || object._empty_) { return; }; + const node = $(nodeRef.current); const { x, y } = keyboard.mouse.page; S.Menu.open('widget', { @@ -480,9 +255,8 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }); }; - initToggle () { - const { block, isPreview } = this.props; - const node = $(this.node); + const initToggle = () => { + const node = $(nodeRef.current); const innerWrap = node.find('#innerWrap'); const icon = node.find('.icon.collapse'); const isClosed = Storage.checkToggle('widget', block.id); @@ -495,32 +269,30 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }; }; - onToggle (e: any) { + const onToggle = (e: any) => { e.preventDefault(); e.stopPropagation(); - const { block } = this.props; const isClosed = Storage.checkToggle('widget', block.id); - isClosed ? this.open() : this.close(); + isClosed ? open() : close(); Storage.setToggle('widget', block.id, !isClosed); }; - open () { - const { block } = this.props; - const node = $(this.node); + const open = () => { + const node = $(nodeRef.current); const icon = node.find('.icon.collapse'); const innerWrap = node.find('#innerWrap').show(); const wrapper = node.find('#wrapper').css({ height: 'auto' }); const height = wrapper.outerHeight(); - const minHeight = this.getMinHeight(); + const minHeight = getMinHeight(); node.addClass('isClosed'); icon.removeClass('isClosed'); wrapper.css({ height: minHeight }); - if (this.ref && this.ref.onOpen) { - this.ref.onOpen(); + if (childRef.current?.onOpen) { + childRef.current?.onOpen(); }; raf(() => { @@ -528,8 +300,8 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { innerWrap.css({ opacity: 1 }); }); - window.clearTimeout(this.timeout); - this.timeout = window.setTimeout(() => { + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => { const isClosed = Storage.checkToggle('widget', block.id); if (!isClosed) { @@ -539,13 +311,12 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }, J.Constant.delay.widget); }; - close () { - const { block } = this.props; - const node = $(this.node); + const close = () => { + const node = $(nodeRef.current); const icon = node.find('.icon.collapse'); const innerWrap = node.find('#innerWrap'); const wrapper = node.find('#wrapper'); - const minHeight = this.getMinHeight(); + const minHeight = getMinHeight(); wrapper.css({ height: wrapper.outerHeight() }); icon.addClass('isClosed'); @@ -556,8 +327,8 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { wrapper.css({ height: minHeight }); }); - window.clearTimeout(this.timeout); - this.timeout = window.setTimeout(() => { + window.clearTimeout(timeout.current); + timeout.current = window.setTimeout(() => { const isClosed = Storage.checkToggle('widget', block.id); if (isClosed) { @@ -567,21 +338,17 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }, J.Constant.delay.widget); }; - getMinHeight () { - return [ I.WidgetLayout.List, I.WidgetLayout.Compact, I.WidgetLayout.Tree ].includes(this.props.block.content.layout) ? 8 : 0; + const getMinHeight = () => { + return [ I.WidgetLayout.List, I.WidgetLayout.Compact, I.WidgetLayout.Tree ].includes(block.content.layout) ? 8 : 0; }; - getData (subId: string, callBack?: () => void) { - const { block } = this.props; - const child = this.getTargetBlock(); - + const getData = (subscriptionId: string, callBack?: () => void) => { if (!child) { return; }; - this.subId = subId; + subId.current = subscriptionId; - const { targetBlockId } = child.content; const space = U.Space.getSpaceview(); const templateType = S.Record.getTemplateType(); const sorts = []; @@ -589,13 +356,13 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { { relationKey: 'layout', condition: I.FilterCondition.NotIn, value: U.Object.getFileAndSystemLayouts() }, { relationKey: 'type', condition: I.FilterCondition.NotEqual, value: templateType?.id }, ]; - let limit = this.getLimit(block.content); + let limit = getLimit(block.content); - if (targetBlockId != J.Constant.widgetId.recentOpen) { + if (targetId != J.Constant.widgetId.recentOpen) { sorts.push({ relationKey: 'lastModifiedDate', type: I.SortType.Desc }); }; - switch (targetBlockId) { + switch (targetId) { case J.Constant.widgetId.favorite: { filters.push({ relationKey: 'isFavorite', condition: I.FilterCondition.Equal, value: true }); limit = 0; @@ -625,7 +392,7 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }; U.Data.searchSubscribe({ - subId, + subId: subId.current, filters, sorts, limit, @@ -637,21 +404,11 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }); }; - getFavoriteIds (): string[] { - return S.Record.getRecords(this.subId).filter(it => !it.isArchived && !it.isDeleted).map(it => it.id); - }; - - getFavoriteBlockIds (): string[] { - const { root } = S.Block; - const ids = S.Block.getChildren(root, root, it => it.isLink()).map(it => it.getTargetObjectId()); - const items = ids.map(id => S.Detail.get(root, id)).filter(it => !it.isArchived && !it.isDeleted).map(it => it.id); - - return items; - }; - - sortFavorite (records: string[]): string[] { - const { block, isPreview } = this.props; - const ids = this.getFavoriteBlockIds(); + const sortFavorite = (records: string[]): string[] => { + const ids = S.Block.getChildren(root, root, it => it.isLink()). + map(it => it.getTargetObjectId()). + map(id => S.Detail.get(root, id)). + filter(it => !it.isArchived && !it.isDeleted).map(it => it.id); let sorted = U.Common.objectCopy(records || []).sort((c1: string, c2: string) => { const i1 = ids.indexOf(c1); @@ -663,17 +420,13 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { }); if (!isPreview) { - sorted = sorted.slice(0, this.getLimit(block.content)); + sorted = sorted.slice(0, getLimit(block.content)); }; return sorted; }; - onSetPreview () { - const { block, isPreview, setPreview } = this.props; - const object = this.getObject(); - const child = this.getTargetBlock(); - + const onSetPreview = () => { if (!child) { return; }; @@ -686,51 +439,34 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { if (!isPreview) { blockId = block.id; event = 'SelectHomeTab'; - data.tab = this.isSystemTarget() ? object.name : analytics.typeMapper(object.type); + data.tab = isSystemTarget() ? object.name : analytics.typeMapper(object.type); }; setPreview(blockId); analytics.event(event, data); }; - onDragEnd () { - const { block } = this.props; - const { layout } = block.content; - + const onDragEnd = () => { analytics.event('ReorderWidget', { layout, - params: { target: this.getObject() } + params: { target: object } }); }; - isSystemTarget (): boolean { - const target = this.getTargetBlock(); - return target ? this.isSystemTargetId(target.getTargetObjectId()) : false; - }; - - isSystemTargetId (id: string): boolean { - return U.Menu.isSystemWidget(id); - }; - - canCreate (): boolean { - const object = this.getObject(); - const { block, isEditing } = this.props; - + const canCreateHandler = (): boolean => { if (!object || isEditing || !U.Space.canMyParticipantWrite()) { return false; }; - const { layout } = block.content; - const target = this.getTargetBlock(); const layoutWithPlus = [ I.WidgetLayout.List, I.WidgetLayout.Tree, I.WidgetLayout.Compact, I.WidgetLayout.View ].includes(layout); - const isRecent = target ? [ J.Constant.widgetId.recentOpen, J.Constant.widgetId.recentEdit ].includes(target.getTargetObjectId()) : null; + const isRecent = [ J.Constant.widgetId.recentOpen, J.Constant.widgetId.recentEdit ].includes(targetId); if (isRecent || !layoutWithPlus) { return false; }; if (U.Object.isInSetLayouts(object.layout)) { - const rootId = this.getRootId(); + const rootId = getRootId(); const typeId = Dataview.getTypeId(rootId, J.Constant.blockId.dataview, object.id); const type = S.Record.getTypeById(typeId); const layouts = U.Object.getFileLayouts().concat(I.ObjectLayout.Participant); @@ -754,28 +490,15 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { return true; }; - getRootId (): string { - const target = this.getTargetBlock(); - return target ? [ target.getTargetObjectId(), 'widget', target.id ].join('-') : ''; + const getRootId = (): string => { + return child ? [ targetId, 'widget', child.id ].join('-') : ''; }; - getTraceId (): string { - const target = this.getTargetBlock(); - return target ? [ 'widget', target.id ].join('-') : ''; - }; - - getLimit ({ limit, layout }): number { - const { isPreview } = this.props; - const options = U.Menu.getWidgetLimitOptions(layout).map(it => Number(it.id)); - - if (!limit || !options.includes(limit)) { - limit = options[0]; - }; - - return isPreview ? J.Constant.limit.menuRecords : limit; + const getTraceId = (): string => { + return child ? [ 'widget', child.id ].join('-') : ''; }; - addGroupLabels (records: any[], widgetId: string) { + const addGroupLabels = (records: any[], widgetId: string) => { let relationKey; if (widgetId == J.Constant.widgetId.recentOpen) { relationKey = 'lastOpenedDate'; @@ -786,7 +509,7 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { return U.Data.groupDateSections(records, relationKey, { type: '', links: [] }); }; - onContext (param: any) { + const onContext = (param: any) => { const { node, element, withElement, subId, objectId, data } = param; const menuParam: any = { @@ -815,6 +538,200 @@ const WidgetIndex = observer(class WidgetIndex extends React.Component<Props> { S.Menu.open('dataviewContext', menuParam); }; -}); + const canCreate = canCreateHandler(); + const childProps = { + ...props, + ref: childRef, + key: childKey, + parent: block, + block: child, + canCreate, + isSystemTarget: isSystemTarget, + getData, + getLimit, + getTraceId, + sortFavorite, + addGroupLabels, + onContext, + onCreate, + }; + + if (className) { + cn.push(className); + }; + + if (isPreview) { + cn.push('isPreview'); + }; + + if (withSelect) { + cn.push('withSelect'); + }; + + let head = null; + let content = null; + let back = null; + let buttons = null; + let targetTop = null; + let targetBot = null; + let isDraggable = canWrite; + + if (isPreview) { + back = ( + <div className="iconWrap back"> + <Icon + className="back" + onClick={() => { + setPreview(''); + analytics.event('ScreenHome', { view: 'Widget' }); + }} + /> + </div> + ); + + isDraggable = false; + } else { + buttons = ( + <div className="buttons"> + {isEditing ? ( + <div className="iconWrap more"> + <Icon className="options" tooltip={translate('widgetOptions')} onClick={onOptions} /> + </div> + ) : ''} + {canCreate ? ( + <div className="iconWrap create"> + <Icon className="plus" tooltip={translate('commonCreateNewObject')} onClick={onCreateClick} /> + </div> + ) : ''} + <div className="iconWrap collapse"> + <Icon className="collapse" tooltip={translate('widgetToggle')} onClick={onToggle} /> + </div> + </div> + ); + }; + + if (hasChild) { + const onClickHandler = isSystemTarget() ? onSetPreview : onClick; + + head = ( + <div className="head" onClick={onClickHandler}> + {back} + <div className="clickable"> + <ObjectName object={object} /> + {favCnt > limit ? <span className="count">{favCnt}</span> : ''} + </div> + {buttons} + </div> + ); + + if (canDrop) { + head = ( + <DropTarget + cacheKey={[ block.id, object.id ].join('-')} + id={object.id} + rootId={targetId} + targetContextId={object.id} + dropType={I.DropType.Menu} + canDropMiddle={true} + className="targetHead" + > + {head} + </DropTarget> + ); + }; + + targetTop = ( + <DropTarget + {...props} + isTargetTop={true} + rootId={S.Block.widgets} + id={block.id} + dropType={I.DropType.Widget} + canDropMiddle={false} + onClick={onClickHandler} + /> + ); + + targetBot = ( + <DropTarget + {...props} + isTargetBottom={true} + rootId={S.Block.widgets} + id={block.id} + dropType={I.DropType.Widget} + canDropMiddle={false} + /> + ); + }; + + switch (layout) { + case I.WidgetLayout.Space: { + cn.push('widgetSpace'); + content = <WidgetSpace {...childProps} />; + + isDraggable = false; + break; + }; + + case I.WidgetLayout.Link: { + cn.push('widgetLink'); + break; + }; + + case I.WidgetLayout.Tree: { + cn.push('widgetTree'); + content = <WidgetTree {...childProps} />; + break; + }; + + case I.WidgetLayout.List: + case I.WidgetLayout.Compact: + case I.WidgetLayout.View: { + cn.push('widgetView'); + content = <WidgetView {...childProps} />; + break; + }; + + }; + + useEffect(() => { + rebind(); + setDummy(dummy + 1); + + return () => { + unbind(); + window.clearTimeout(timeout.current); + }; + }, []); + + useEffect(() => initToggle()); + + return ( + <div + ref={nodeRef} + id={`widget-${block.id}`} + className={cn.join(' ')} + draggable={isDraggable} + onDragStart={e => onDragStart(e, block.id)} + onDragOver={e => onDragOver ? onDragOver(e, block.id) : null} + onDragEnd={onDragEnd} + onContextMenu={onOptions} + > + <Icon className="remove" inner={<div className="inner" />} onClick={onRemove} /> + + {head} + + <div id="wrapper" className="contentWrapper"> + {content} + </div> + + <div className="dimmer" /> + + {targetTop} + {targetBot} + </div> + ); + +})); export default WidgetIndex; diff --git a/src/ts/component/widget/space.tsx b/src/ts/component/widget/space.tsx index 2081bf5564..5446decbb2 100644 --- a/src/ts/component/widget/space.tsx +++ b/src/ts/component/widget/space.tsx @@ -1,34 +1,85 @@ -import * as React from 'react'; +import React, { forwardRef, MouseEvent } from 'react'; import { observer } from 'mobx-react'; -import { IconObject, ObjectName } from 'Component'; -import { I, S, U } from 'Lib'; +import { Icon, IconObject, ObjectName } from 'Component'; +import { I, S, U, translate, sidebar, keyboard, analytics } from 'Lib'; -const WidgetSpace = observer(class WidgetSpace extends React.Component<I.WidgetComponent> { +const WidgetSpace = observer(forwardRef<I.WidgetComponent>(() => { - node = null; + const space = U.Space.getSpaceview(); + const participants = U.Space.getParticipantsList([ I.ParticipantStatus.Active, I.ParticipantStatus.Joining, I.ParticipantStatus.Removing ]); + const members = participants.filter(it => it.isActive); + const requestCnt = participants.filter(it => it.isJoining || it.isRemoving).length; + const isSpaceOwner = U.Space.isMyOwner(); + const cn = [ 'body' ]; + const cmd = keyboard.cmdSymbol(); + const buttons = [ + space.chatId && U.Object.isAllowedChat() ? { id: 'chat', name: translate('commonMainChat') } : null, + space.isShared ? { id: 'member', name: translate('commonMembers') } : null, + { id: 'all', name: translate('commonAllContent') }, + ].filter(it => it); - constructor (props: I.WidgetComponent) { - super(props); + if (isSpaceOwner && requestCnt) { + cn.push('withCnt'); + }; - this.onSettings = this.onSettings.bind(this); - this.onRequest = this.onRequest.bind(this); + const openSettings = (page: string) => { + S.Popup.open('settings', { data: { page, isSpace: true }, className: 'isSpace' }); + }; + + const onSettings = (e: MouseEvent) => { + e.stopPropagation(); + openSettings('spaceIndex'); }; - render (): React.ReactNode { - const space = U.Space.getSpaceview(); + const onRequest = (e: MouseEvent) => { + e.stopPropagation(); + openSettings('spaceShare'); + }; + + const onSearch = (e: MouseEvent) => { + e.stopPropagation(); + keyboard.onSearchPopup(analytics.route.widget); + }; + + const onCreate = (e: MouseEvent) => { + e.stopPropagation(); + keyboard.pageCreate({}, analytics.route.widget); + }; + + const onButtonClick = (e: any, item: any) => { + e.preventDefault(); + e.stopPropagation(); + + switch (item.id) { + case 'member': { + S.Popup.open('settings', { data: { page: 'spaceShare', isSpace: true }, className: 'isSpace' }); + break; + }; + + case 'all': { + sidebar.objectContainerToggle(); + break; + }; - return ( - <div - ref={ref => this.node = ref} - className="body" - onClick={this.onSettings} - > + case 'chat': { + U.Object.openAuto({ id: S.Block.workspace, layout: I.ObjectLayout.Chat }); + break; + }; + }; + }; + + return ( + <div + className={cn.join(' ')} + onClick={onSettings} + > + <div className="sides"> <div className="side left"> <IconObject id="widget-space-icon" object={{ ...space, layout: I.ObjectLayout.SpaceView }} - size={32} - iconSize={32} + size={18} + iconSize={18} menuParam={{ className: 'fixed' }} /> <div className="txt"> @@ -36,47 +87,41 @@ const WidgetSpace = observer(class WidgetSpace extends React.Component<I.WidgetC </div> </div> <div className="side right"> - <div id="cnt" className="cnt" onClick={this.onRequest} /> + <Icon className="search withBackground" onClick={onSearch} tooltip={translate('commonSearch')} tooltipCaption={`${cmd} + S`} /> + <Icon className="plus withBackground" onClick={onCreate} tooltip={translate('commonCreateNewObject')} tooltipCaption={`${cmd} + N`} /> + <div className="cnt" onClick={onRequest}>{requestCnt}</div> </div> </div> - ); - }; - componentDidMount(): void { - this.setCnt(); - }; + <div className="buttons"> + {buttons.map((item, i) => { + let cnt = null; - componentDidUpdate (): void { - this.setCnt(); - }; + if (item.id == 'member') { + cnt = <div className="cnt">{members.length}</div>; + }; - onSettings (e: React.MouseEvent) { - e.stopPropagation(); - this.openSettings('spaceIndex'); - }; - - onRequest (e: any) { - e.stopPropagation(); - this.openSettings('spaceShare'); - }; - - openSettings (page: string) { - S.Popup.open('settings', { data: { page, isSpace: true }, className: 'isSpace' }); - }; - - setCnt () { - const node = $(this.node); - const cnt = node.find('#cnt'); - const participants = U.Space.getParticipantsList([ I.ParticipantStatus.Active, I.ParticipantStatus.Joining, I.ParticipantStatus.Removing ]); - const requestCnt = participants.filter(it => it.isJoining || it.isRemoving).length; - const isSpaceOwner = U.Space.isMyOwner(); - const showCnt = isSpaceOwner && !!requestCnt; - - showCnt ? cnt.show() : cnt.hide(); - node.toggleClass('withCnt', showCnt); - cnt.text(requestCnt); - }; - -}); + return ( + <div + key={i} + id={`item-${item.id}`} + className="item" + onClick={e => onButtonClick(e, item)} + > + <div className="side left"> + <Icon className={item.id} /> + <div className="name"> + {item.name} + {cnt} + </div> + </div> + <div className="side right" /> + </div> + ); + })} + </div> + </div> + ); +})); export default WidgetSpace; \ No newline at end of file diff --git a/src/ts/component/widget/tree/index.tsx b/src/ts/component/widget/tree/index.tsx index fd8cae5606..89790ee835 100644 --- a/src/ts/component/widget/tree/index.tsx +++ b/src/ts/component/widget/tree/index.tsx @@ -1,230 +1,51 @@ -import * as React from 'react'; +import React, { forwardRef, useImperativeHandle, useEffect, useRef, useState, MouseEvent } from 'react'; import $ from 'jquery'; import sha1 from 'sha1'; import { observer } from 'mobx-react'; import { AutoSizer, CellMeasurer, CellMeasurerCache, InfiniteLoader, List } from 'react-virtualized'; -import { Loader, Label, Button } from 'Component'; +import { Label, Button } from 'Component'; import { I, C, S, U, J, analytics, Relation, Storage, translate } from 'Lib'; import Item from './item'; -interface State { - loading: boolean; -}; - const MAX_DEPTH = 15; // Maximum depth of the tree const LIMIT = 20; // Number of nodes to load at a time const HEIGHT = 28; // Height of each row -const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetComponent, State> { - - private _isMounted: boolean = false; - - node: any = null; - state = { - loading: false, - }; - top = 0; - id = ''; - cache: CellMeasurerCache = null; - subscriptionHashes: { [ key: string ]: string } = {}; - branches: string[] = []; - refList = null; - deletedIds = new Set(); - links = []; - - constructor (props: I.WidgetComponent) { - super(props); - - this.onScroll = this.onScroll.bind(this); - this.onClick = this.onClick.bind(this); - this.onToggle = this.onToggle.bind(this); - this.getSubId = this.getSubId.bind(this); - this.initCache = this.initCache.bind(this); - this.getSubKey = this.getSubKey.bind(this); - }; - - render () { - const { loading } = this.state; - const { isPreview, canCreate, onCreate } = this.props; - const nodes = this.loadTree(); - const length = nodes.length; - - if (!this.cache) { - return null; - }; - - this.getDeleted(); - - let content = null; - - if (loading) { - content = <Loader />; - } else - if (!length) { - content = ( - <div className="emptyWrap"> - <Label className="empty" text={canCreate ? translate('widgetEmptyLabelCreate') : translate('widgetEmptyLabel')} /> - {canCreate ? <Button text={translate('commonCreateObject')} color="blank" className="c28" onClick={onCreate} /> : ''} - </div> - ); - } else - if (isPreview) { - const rowRenderer = ({ index, parent, style }) => { - const node: I.WidgetTreeItem = nodes[index]; - const key = this.getTreeKey(node); - - return ( - <CellMeasurer - key={key} - parent={parent} - cache={this.cache} - columnIndex={0} - rowIndex={index} - fixedWidth - > - <Item - {...this.props} - {...node} - index={index} - treeKey={key} - style={style} - onClick={this.onClick} - onToggle={this.onToggle} - getSubId={this.getSubId} - getSubKey={this.getSubKey} - /> - </CellMeasurer> - ); - }; - - content = ( - <InfiniteLoader - rowCount={nodes.length} - loadMoreRows={() => {}} - isRowLoaded={() => true} - threshold={LIMIT} - > - {({ onRowsRendered }) => ( - <AutoSizer className="scrollArea"> - {({ width, height }) => ( - <List - ref={ref => this.refList = ref} - width={width} - height={height} - deferredMeasurmentCache={this.cache} - rowCount={nodes.length} - rowHeight={({ index }) => this.getRowHeight(nodes[index], index)} - rowRenderer={rowRenderer} - onRowsRendered={onRowsRendered} - overscanRowCount={LIMIT} - onScroll={this.onScroll} - scrollToAlignment="center" - /> - )} - </AutoSizer> - )} - </InfiniteLoader> - ); - } else { - content = ( - <div className="ReactVirtualized__List"> - {nodes.map((node, i: number) => { - const key = this.getTreeKey(node); - - return ( - <Item - key={key} - {...this.props} - {...node} - index={i} - treeKey={key} - onClick={this.onClick} - onToggle={this.onToggle} - getSubId={this.getSubId} - getSubKey={this.getSubKey} - /> - ); - })} - </div> - ); - }; - - return ( - <div - ref={node => this.node = node} - id="innerWrap" - className="innerWrap" - > - {content} - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - - const { block, isSystemTarget, getData, getTraceId } = this.props; - const object = this.getTarget(); - - this.links = object.links; - - if (isSystemTarget()) { - getData(this.getSubId(), this.initCache); - } else { - this.initCache(); - C.ObjectShow(block.getTargetObjectId(), getTraceId(), U.Router.getRouteSpaceId()); - }; - - this.getDeleted(); - }; - - componentDidUpdate () { - const object = this.getTarget(); - - this.resize(); - this.getDeleted(); - - // Reload the tree if the links have changed - if (!U.Common.compareJSON(this.links, object.links)) { - this.clear(); - this.initCache(); - this.links = object.links; - }; - - if (this.refList) { - this.refList.recomputeRowHeights(0); - this.refList.scrollToPosition(this.top); - }; - }; - - componentWillUnmount () { - this._isMounted = false; - this.unsubscribe(); - }; - - getDeleted () { - const deleted = S.Record.getRecordIds(J.Constant.subId.deleted, ''); - const length = deleted.length; - - this.deletedIds = new Set(deleted); - return this.deletedIds; - }; +interface WidgetTreeRefProps { + updateData: () => void; +}; - unsubscribe () { - const subIds = Object.keys(this.subscriptionHashes).map(this.getSubId); +const WidgetTree = observer(forwardRef<WidgetTreeRefProps, I.WidgetComponent>((props, ref) => { + + const { block, parent, isPreview, canCreate, onCreate, isSystemTarget, getData, getTraceId, sortFavorite, addGroupLabels } = props; + const targetId = block ? block.getTargetObjectId() : ''; + const nodeRef = useRef(null); + const listRef = useRef(null); + const deletedIds = new Set(S.Record.getRecordIds(J.Constant.subId.deleted, '')); + const object = S.Detail.get(S.Block.widgets, targetId); + const subKey = block ? `widget${block.id}` : ''; + const links = useRef([]); + const top = useRef(0); + const branches = useRef([]); + const subscriptionHashes = useRef({}); + const cache = useRef(new CellMeasurerCache()); + const [ dummy, setDummy ] = useState(0); + const isRecent = [ J.Constant.widgetId.recentOpen, J.Constant.widgetId.recentEdit ].includes(targetId); + + const unsubscribe = () => { + const subIds = Object.keys(subscriptionHashes.current).map(getSubId); if (subIds.length) { C.ObjectSearchUnsubscribe(subIds); - - this.clear(); + clear(); }; }; - clear () { - const subIds = Object.keys(this.subscriptionHashes).map(this.getSubId); + const clear = () => { + const subIds = Object.keys(subscriptionHashes.current).map(getSubId); - this.subscriptionHashes = {}; - this.branches = []; + subscriptionHashes.current = {}; + branches.current = []; subIds.forEach(subId => { S.Record.recordsClear(subId, ''); @@ -232,63 +53,52 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom }); }; - updateData () { - const { isSystemTarget, getData } = this.props; - + const updateData = () => { if (isSystemTarget()) { - getData(this.getSubId(), this.initCache); + getData(getSubId(), initCache); }; }; - initCache () { - const nodes = this.loadTree(); + const initCache = () => { + const nodes = loadTree(); - this.cache = new CellMeasurerCache({ + cache.current = new CellMeasurerCache({ fixedWidth: true, - defaultHeight: i => this.getRowHeight(nodes[i], i), + defaultHeight: i => getRowHeight(nodes[i], i), keyMapper: i => (nodes[i] || {}).id, }); - this.forceUpdate(); - }; - - getTarget () { - return S.Detail.get(S.Block.widgets, this.props.block.getTargetObjectId()); + setDummy(dummy + 1); }; - loadTree (): I.WidgetTreeItem[] { - const { block, isSystemTarget, isPreview, sortFavorite, addGroupLabels } = this.props; - const { targetBlockId } = block.content; - const object = this.getTarget(); - const isRecent = [ J.Constant.widgetId.recentOpen, J.Constant.widgetId.recentEdit ].includes(targetBlockId); - - this.branches = []; + const loadTree = (): I.WidgetTreeItem[] => { + branches.current = []; let children = []; if (isSystemTarget()) { - const subId = this.getSubId(targetBlockId); + const subId = getSubId(targetId); let records = S.Record.getRecordIds(subId, ''); - if (targetBlockId == J.Constant.widgetId.favorite) { + if (targetId == J.Constant.widgetId.favorite) { records = sortFavorite(records); }; - children = records.map(id => this.mapper(S.Detail.get(subId, id, J.Relation.sidebar))); + children = records.map(id => mapper(S.Detail.get(subId, id, J.Relation.sidebar))); } else { - children = this.getChildNodesDetails(object.id); - this.subscribeToChildNodes(object.id, Relation.getArrayValue(object.links)); + children = getChildNodesDetails(object.id); + subscribeToChildNodes(object.id, Relation.getArrayValue(object.links)); }; if (isPreview && isRecent) { // add group labels - children = addGroupLabels(children, targetBlockId); + children = addGroupLabels(children, targetId); }; - return this.loadTreeRecursive(object.id, object.id, [], children, 1, ''); + return loadTreeRecursive(object.id, object.id, [], children, 1, ''); }; // Recursive function which returns the tree structure - loadTreeRecursive (rootId: string, parentId: string, treeNodeList: I.WidgetTreeItem[], childNodeList: I.WidgetTreeDetails[], depth: number, branch: string): I.WidgetTreeItem[] { + const loadTreeRecursive = (rootId: string, parentId: string, treeNodeList: I.WidgetTreeItem[], childNodeList: I.WidgetTreeDetails[], depth: number, branch: string): I.WidgetTreeItem[] => { if (!childNodeList.length || depth >= MAX_DEPTH) { return treeNodeList; }; @@ -296,12 +106,12 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom for (const childNode of childNodeList) { const childBranch = [ branch, childNode.id ].join('-'); - const links = this.filterDeletedLinks(Relation.getArrayValue(childNode.links)).filter(nodeId => { + const links = filterDeletedLinks(Relation.getArrayValue(childNode.links)).filter(nodeId => { const branchId = [ childBranch, nodeId ].join('-'); - if (this.branches.includes(branchId)) { + if (branches.current.includes(branchId)) { return false; } else { - this.branches.push(branchId); + branches.current.push(branchId); return true; }; }); @@ -322,50 +132,45 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom continue; }; - const isOpen = Storage.checkToggle(this.getSubKey(), this.getTreeKey(node)); + const isOpen = Storage.checkToggle(subKey, getTreeKey(node)); if (isOpen) { - this.subscribeToChildNodes(childNode.id, childNode.links); - treeNodeList = this.loadTreeRecursive(rootId, childNode.id, treeNodeList, this.getChildNodesDetails(childNode.id), depth + 1, childBranch); + subscribeToChildNodes(childNode.id, childNode.links); + treeNodeList = loadTreeRecursive(rootId, childNode.id, treeNodeList, getChildNodesDetails(childNode.id), depth + 1, childBranch); }; }; return treeNodeList; }; - filterDeletedLinks (ids: string[]): string[] { - return ids.filter(id => !this.deletedIds.has(id)); + const filterDeletedLinks = (ids: string[]): string[] => { + return ids.filter(id => !deletedIds.has(id)); }; // return the child nodes details for the given subId - getChildNodesDetails (nodeId: string): I.WidgetTreeDetails[] { - return S.Record.getRecords(this.getSubId(nodeId), [ 'id', 'layout', 'links' ], true).map(it => this.mapper(it)); + const getChildNodesDetails = (nodeId: string): I.WidgetTreeDetails[] => { + return S.Record.getRecords(getSubId(nodeId), [ 'id', 'layout', 'links' ], true).map(it => mapper(it)); }; - mapper (item) { - let links = []; - if (item.layout != I.ObjectLayout.Set) { - links = this.filterDeletedLinks(Relation.getArrayValue(item.links)); - }; - - item.links = links; - return item; + const mapper = (o) => { + o.links = U.Object.isSetLayout(o.layout) ? [] : filterDeletedLinks(Relation.getArrayValue(o.links)); + return o; }; // Subscribe to changes to child nodes for a given node Id and its links - subscribeToChildNodes (nodeId: string, links: string[]): void { + const subscribeToChildNodes = (nodeId: string, links: string[]): void => { if (!links.length) { return; }; const hash = sha1(U.Common.arrayUnique(links).join('-')); - const subId = this.getSubId(nodeId); + const subId = getSubId(nodeId); // if already subscribed to the same links, dont subscribe again - if (this.subscriptionHashes[nodeId] && (this.subscriptionHashes[nodeId] == hash)) { + if (subscriptionHashes.current[nodeId] && (subscriptionHashes.current[nodeId] == hash)) { return; }; - this.subscriptionHashes[nodeId] = hash; + subscriptionHashes.current[nodeId] = hash; U.Data.subscribeIds({ subId, ids: links, @@ -375,58 +180,38 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom }; // Utility methods - - getSubKey () { - return `widget${this.props.block.id}`; - }; - - getSubId (nodeId?: string): string { - return S.Record.getSubId(this.getSubKey(), nodeId || this.props.block.getTargetObjectId()); + const getSubId = (nodeId?: string): string => { + return S.Record.getSubId(subKey, nodeId || targetId); }; // a composite key for the tree node in the form rootId-parentId-Id-depth - getTreeKey (node: I.WidgetTreeItem): string { - const { block } = this.props; - const { depth, branch } = node; - - return [ block.id, branch, depth ].join('-'); - }; - - sortByIds (ids: string[], id1: string, id2: string) { - const i1 = ids.indexOf(id1); - const i2 = ids.indexOf(id2); - if (i1 > i2) return 1; - if (i1 < i2) return -1; - return 0; + const getTreeKey = (node: I.WidgetTreeItem): string => { + return [ block.id, node.branch, node.depth ].join('-'); }; // Event handlers - onToggle (e: React.MouseEvent, node: I.WidgetTreeItem): void { - if (!this._isMounted) { - return; - }; - + const onToggle = (e: MouseEvent, node: I.WidgetTreeItem): void => { e.preventDefault(); e.stopPropagation(); - const subKey = this.getSubKey(); - const treeKey = this.getTreeKey(node); + const treeKey = getTreeKey(node); const isOpen = Storage.checkToggle(subKey, treeKey); Storage.setToggle(subKey, treeKey, !isOpen); analytics.event(!isOpen ? 'OpenSidebarObjectToggle' : 'CloseSidebarObjectToggle'); - this.forceUpdate(); + + setDummy(dummy + 1); }; - onScroll ({ scrollTop }): void { + const onScroll = ({ scrollTop }): void => { const dragProvider = S.Common.getRef('dragProvider'); - this.top = scrollTop; + top.current = scrollTop; dragProvider?.onScroll(); }; - onClick (e: React.MouseEvent, item: unknown): void { + const onClick = (e: MouseEvent, item: unknown): void => { if (e.button) { return; }; @@ -438,19 +223,11 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom analytics.event('OpenSidebarObject'); }; - getTotalHeight () { - const nodes = this.loadTree(); - - let height = 0; - - nodes.forEach((node, index) => { - height += this.getRowHeight(node, index); - }); - - return height; + const getTotalHeight = () => { + return loadTree().reduce((acc, node, index) => acc + getRowHeight(node, index), 0); }; - getRowHeight (node: any, index: number) { + const getRowHeight = (node: any, index: number) => { let h = HEIGHT; if (node && node.isSection && index) { h += 12; @@ -458,12 +235,11 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom return h; }; - resize () { - const { parent, isPreview } = this.props; - const nodes = this.loadTree(); - const node = $(this.node); + const resize = () => { + const nodes = loadTree(); + const node = $(nodeRef.current); const length = nodes.length; - const css: any = { height: this.getTotalHeight() + 16, paddingBottom: '' }; + const css: any = { height: getTotalHeight() + 16, paddingBottom: '' }; const emptyWrap = node.find('.emptyWrap'); if (isPreview) { @@ -481,6 +257,150 @@ const WidgetTree = observer(class WidgetTree extends React.Component<I.WidgetCom node.css(css); }; -}); -export default WidgetTree; + const nodes = loadTree(); + const length = nodes.length; + + let content = null; + + if (!length) { + content = ( + <div className="emptyWrap"> + <Label className="empty" text={canCreate ? translate('widgetEmptyLabelCreate') : translate('widgetEmptyLabel')} /> + {canCreate ? ( + <Button + text={translate('commonCreateObject')} + color="blank" + className="c28" + onClick={() => onCreate({ route: analytics.route.inWidget })} + /> + ) : ''} + </div> + ); + } else + if (isPreview) { + const rowRenderer = ({ index, parent, style }) => { + const node: I.WidgetTreeItem = nodes[index]; + const key = getTreeKey(node); + + return ( + <CellMeasurer + key={key} + parent={parent} + cache={cache.current} + columnIndex={0} + rowIndex={index} + fixedWidth + > + <Item + {...props} + {...node} + index={index} + treeKey={key} + style={style} + onClick={onClick} + onToggle={onToggle} + getSubId={getSubId} + getSubKey={() => subKey} + /> + </CellMeasurer> + ); + }; + + content = ( + <InfiniteLoader + rowCount={nodes.length} + loadMoreRows={() => {}} + isRowLoaded={() => true} + threshold={LIMIT} + > + {({ onRowsRendered }) => ( + <AutoSizer className="scrollArea"> + {({ width, height }) => ( + <List + ref={listRef} + width={width} + height={height} + deferredMeasurmentCache={cache.current} + rowCount={nodes.length} + rowHeight={({ index }) => getRowHeight(nodes[index], index)} + rowRenderer={rowRenderer} + onRowsRendered={onRowsRendered} + overscanRowCount={LIMIT} + onScroll={onScroll} + scrollToAlignment="center" + /> + )} + </AutoSizer> + )} + </InfiniteLoader> + ); + } else { + content = ( + <div className="ReactVirtualized__List"> + {nodes.map((node, i: number) => { + const key = getTreeKey(node); + + return ( + <Item + key={key} + {...props} + {...node} + index={i} + treeKey={key} + onClick={onClick} + onToggle={onToggle} + getSubId={getSubId} + getSubKey={() => subKey} + /> + ); + })} + </div> + ); + }; + + useEffect(() => { + links.current = object.links; + + if (isSystemTarget()) { + getData(getSubId(), initCache); + } else { + initCache(); + C.ObjectShow(targetId, getTraceId(), U.Router.getRouteSpaceId()); + }; + + return () => unsubscribe(); + }, []); + + useEffect(() => { + resize(); + + // Reload the tree if the links have changed + if (!U.Common.compareJSON(links.current, object.links)) { + clear(); + initCache(); + + links.current = object.links; + }; + + listRef.current?.recomputeRowHeights(0); + listRef.current?.scrollToPosition(top.current); + }); + + useImperativeHandle(ref, () => ({ + updateData, + })); + + return ( + <div + ref={nodeRef} + id="innerWrap" + className="innerWrap" + > + {content} + </div> + ); + +})); + +export default WidgetTree; \ No newline at end of file diff --git a/src/ts/component/widget/tree/item.tsx b/src/ts/component/widget/tree/item.tsx index f7f1f79aa1..cb01ae9dbd 100644 --- a/src/ts/component/widget/tree/item.tsx +++ b/src/ts/component/widget/tree/item.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useState, MouseEvent, SyntheticEvent } from 'react'; import { observer } from 'mobx-react'; import { DropTarget, Icon, IconObject, ObjectName, Label } from 'Component'; import { I, S, U, J, keyboard, Storage, translate } from 'Lib'; @@ -17,70 +17,75 @@ interface Props extends I.WidgetTreeItem { onContext?(param: any): void; }; -const TreeItem = observer(class Node extends React.Component<Props> { - - node = null; +const TreeItem = observer(forwardRef<{}, Props>((props, ref) => { + + const { id, parentId, treeKey, depth, style, numChildren, isEditing, isSection, getSubKey, getSubId, onContext, onClick, onToggle } = props; + const nodeRef = useRef(null); + const moreRef = useRef(null); + const subKey = getSubKey(); + const subId = getSubId(parentId); + const isOpen = Storage.checkToggle(subKey, treeKey); + const object = S.Detail.get(subId, id, J.Relation.sidebar); + const { isReadonly, isArchived, type, restrictions, done, layout } = object; + const cn = [ 'item', 'c' + id, (isOpen ? 'isOpen' : '') ]; + const rootId = keyboard.getRootId(); + const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); + const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); + const paddingLeft = depth > 1 ? (depth - 1) * 8 : 4; + const hasMore = U.Space.canMyParticipantWrite(); + const [ dummy, setDummy ] = useState(0); + + const onContextHandler = (e: SyntheticEvent, withElement: boolean): void => { + e.preventDefault(); + e.stopPropagation(); - constructor (props: Props) { - super(props); + const node = $(nodeRef.current); + const element = $(moreRef.current); - this.onToggle = this.onToggle.bind(this); + onContext({ node, element, withElement, subId, objectId: id }); }; - render () { - const { id, parentId, treeKey, depth, style, numChildren, isEditing, onClick, isSection, getSubKey, getSubId } = this.props; - const subKey = getSubKey(); - const subId = getSubId(parentId); - const isOpen = Storage.checkToggle(subKey, treeKey); - const object = S.Detail.get(subId, id, J.Relation.sidebar); - const { isReadonly, isArchived, type, restrictions, done, layout } = object; - const cn = [ 'item', 'c' + id, (isOpen ? 'isOpen' : '') ]; - const rootId = keyboard.getRootId(); - const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); - const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); - const paddingLeft = depth > 1 ? (depth - 1) * 8 : 4; - const hasMore = U.Space.canMyParticipantWrite(); - - let arrow = null; - let onArrowClick = null; - let more = null; - - if (isSection) { - cn.push('isSection'); - - return ( - <div - ref={node => this.node = node} - style={style} - id={treeKey} - className={cn.join(' ')} - > - <div className="inner"> - <Label text={translate(U.Common.toCamelCase([ 'common', id ].join('-')))} /> - </div> - </div> - ); - }; + const onToggleHandler = (e: MouseEvent): void => { + e.preventDefault(); + e.stopPropagation(); - if (U.Object.isSetLayout(layout) || (U.Object.isCollectionLayout(layout) && !numChildren)) { - arrow = <Icon className="set" />; - } else - if (numChildren > 0) { - onArrowClick = this.onToggle; - arrow = <Icon className="arrow" />; - } else { - arrow = <Icon className="blank" />; - }; + onToggle(e, { ...props, details: object }); + setDummy(dummy + 1); + }; - if (arrow) { - arrow = <div className="arrowWrap" onMouseDown={onArrowClick}>{arrow}</div>; - }; + let arrow = null; + let onArrowClick = null; + let onContextMenu = null; + let more = null; + let inner = null; + + if (U.Object.isSetLayout(layout) || (U.Object.isCollectionLayout(layout) && !numChildren)) { + arrow = <Icon className="set" />; + } else + if (numChildren > 0) { + onArrowClick = onToggleHandler; + arrow = <Icon className="arrow" />; + } else { + arrow = <Icon className="blank" />; + }; - if (hasMore) { - more = <Icon className="more" tooltip={translate('widgetOptions')} onMouseDown={e => this.onContext(e, true)} />; - }; + if (arrow) { + arrow = <div className="arrowWrap" onMouseDown={onArrowClick}>{arrow}</div>; + }; + + if (hasMore) { + more = <Icon ref={moreRef} className="more" tooltip={translate('widgetOptions')} onMouseDown={e => onContextHandler(e, true)} />; + }; - let inner = ( + if (isSection) { + inner = ( + <div className="inner"> + <Label text={translate(U.Common.toCamelCase([ 'common', id ].join('-')))} /> + </div> + ); + } else { + onContextMenu = e => onContextHandler(e, false); + inner = ( <div className="inner" style={{ paddingLeft }}> <div className="clickable" @@ -118,43 +123,20 @@ const TreeItem = observer(class Node extends React.Component<Props> { </DropTarget> ); }; - - return ( - <div - ref={node => this.node = node} - id={treeKey} - className={cn.join(' ')} - style={style} - onContextMenu={e => this.onContext(e, false)} - > - {inner} - </div> - ); - }; - - onContext = (e: React.SyntheticEvent, withElement: boolean): void => { - e.preventDefault(); - e.stopPropagation(); - - const { id, parentId, getSubId, onContext } = this.props; - const subId = getSubId(parentId); - const node = $(this.node); - const element = node.find('.icon.more'); - - onContext({ node, element, withElement, subId, objectId: id }); }; - onToggle (e: React.MouseEvent): void { - e.preventDefault(); - e.stopPropagation(); - - const { id, parentId, onToggle, getSubId } = this.props; - const object = S.Detail.get(getSubId(parentId), id, J.Relation.sidebar, true); - - onToggle(e, { ...this.props, details: object }); - this.forceUpdate(); - }; - -}); - -export default TreeItem; + return ( + <div + ref={nodeRef} + id={treeKey} + className={cn.join(' ')} + style={style} + onContextMenu={onContextMenu} + > + {inner} + </div> + ); + +})); + +export default TreeItem; \ No newline at end of file diff --git a/src/ts/component/widget/view/board/group.tsx b/src/ts/component/widget/view/board/group.tsx index 9bf61af536..a1541039a4 100644 --- a/src/ts/component/widget/view/board/group.tsx +++ b/src/ts/component/widget/view/board/group.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect } from 'react'; import { observer } from 'mobx-react'; import { Icon } from 'Component'; import { I, S, U, J, translate, Dataview, Storage } from 'Lib'; @@ -12,95 +12,18 @@ interface Props extends I.WidgetViewComponent { value: any; }; -const Group = observer(class Group extends React.Component<Props> { +const Group = observer(forwardRef<{}, Props>((props, ref) => { - node = null; - - constructor (props: Props) { - super(props); - - this.onAll = this.onAll.bind(this); - this.onToggle = this.onToggle.bind(this); - this.onCreate = this.onCreate.bind(this); - }; - - render () { - const { rootId, block, id, getView, value, getViewLimit, canCreate } = this.props; - const view = getView(); - const subId = this.getSubId(); - const items = this.getItems(); - const limit = getViewLimit(); - const { total } = S.Record.getMeta(subId, ''); - const head = {}; - - head[view.groupRelationKey] = value; - - // Subscriptions - items.forEach((item: any) => { - const object = S.Detail.get(subId, item.id, [ view.groupRelationKey ]); - }); - - return ( - <div - ref={node => this.node = node} - className="group" - > - <div id={`item-${id}`} className="clickable" onClick={this.onToggle}> - <Icon className="arrow" /> - <Cell - id={`board-head-${id}`} - rootId={rootId} - subId={subId} - block={S.Block.getLeaf(rootId, J.Constant.blockId.dataview)} - relationKey={view.groupRelationKey} - viewType={I.ViewType.Board} - getRecord={() => head} - readonly={true} - arrayLimit={2} - withName={true} - placeholder={translate('commonUncategorized')} - /> - {canCreate ? <Icon className="plus" tooltip={translate('commonCreateNewObject')} onClick={this.onCreate} /> : ''} - </div> - - <div id={`item-${id}-children`} className="items"> - {!items.length ? ( - <div className="item empty">{translate('commonNoObjects')}</div> - ) : ( - <React.Fragment> - {items.map(item => ( - <Item - {...this.props} - key={`widget-${block.id}-item-${item.id}`} - subId={subId} - id={item.id} - hideIcon={view.hideIcon} - /> - ))} - {total > limit ? <div className="item more" onClick={this.onAll}>{translate('widgetShowAll')}</div> : ''} - </React.Fragment> - )} - </div> - </div> - ); - }; - - componentDidMount () { - this.load(); - this.initToggle(); - }; - - componentWillUnmount () { - this.clear(); - }; - - load () { - const { id, getView, getObject, getViewLimit, value } = this.props; - const subId = this.getSubId(); - const object = getObject(); - const view = getView(); - const isCollection = U.Object.isCollectionLayout(object.layout); + const nodeRef = useRef(null); + const { rootId, block, parent, id, value, canCreate, getView, getViewLimit, getObject } = props; + const view = getView(); + const subId = S.Record.getGroupSubId(rootId, J.Constant.blockId.dataview, id); + const object = getObject(); + const limit = getViewLimit(); + const { total } = S.Record.getMeta(subId, ''); + const head = { [view.groupRelationKey]: value }; + const load = () => { if (!view || !object) { return; }; @@ -110,12 +33,12 @@ const Group = observer(class Group extends React.Component<Props> { return; }; + const isCollection = U.Object.isCollectionLayout(object.layout); const filters: I.Filter[] = [ { relationKey: 'layout', condition: I.FilterCondition.NotIn, value: U.Object.excludeFromSet() }, Dataview.getGroupFilter(relation, value), ].concat(view.filters); const sorts: I.Sort[] = [].concat(view.sorts); - const limit = getViewLimit(); U.Data.searchSubscribe({ subId, @@ -128,47 +51,30 @@ const Group = observer(class Group extends React.Component<Props> { ignoreDeleted: true, collectionId: (isCollection ? object.id : ''), }, () => { - S.Record.recordsSet(subId, '', this.applyObjectOrder(id, S.Record.getRecordIds(subId, ''))); + S.Record.recordsSet(subId, '', applyObjectOrder(id, S.Record.getRecordIds(subId, ''))); }); }; - clear () { - S.Record.recordsClear(this.getSubId(), ''); - }; - - getSubId () { - const { rootId, id } = this.props; - - return S.Record.getGroupSubId(rootId, J.Constant.blockId.dataview, id); - }; - - getItems () { - const { id } = this.props; - const subId = this.getSubId(); - const records = U.Common.objectCopy(S.Record.getRecordIds(subId, '')); - - return this.applyObjectOrder(id, records).map(id => ({ id })); + const getItems = () => { + return applyObjectOrder(id, U.Common.objectCopy(S.Record.getRecordIds(subId, ''))); }; - applyObjectOrder (groupId: string, ids: string[]): any[] { - const { rootId, parent } = this.props; - + const applyObjectOrder = (groupId: string, ids: string[]): any[] => { return Dataview.applyObjectOrder(rootId, J.Constant.blockId.dataview, parent.content.viewId, groupId, ids); }; - getToggleKey () { - return `widget${this.props.block.id}`; + const getToggleKey = () => { + return `widget${block.id}`; }; - initToggle () { - const { id } = this.props; - const isOpen = Storage.checkToggle(this.getToggleKey(), id); + const initToggle = () => { + const isOpen = Storage.checkToggle(getToggleKey(), id); if (!isOpen) { return; }; - const node = $(this.node); + const node = $(nodeRef.current); const item = node.find(`#item-${id}`); const children = node.find(`#item-${id}-children`); @@ -176,11 +82,10 @@ const Group = observer(class Group extends React.Component<Props> { children.show(); }; - onToggle () { - const { id } = this.props; - const subKey = this.getToggleKey(); + const onToggle = () => { + const subKey = getToggleKey(); const isOpen = Storage.checkToggle(subKey, id); - const node = $(this.node); + const node = $(nodeRef.current); const item = node.find(`#item-${id}`); const children = node.find(`#item-${id}-children`); @@ -209,14 +114,12 @@ const Group = observer(class Group extends React.Component<Props> { Storage.setToggle(subKey, id, !isOpen); }; - onCreate (e: any) { + const onCreate = (e: any) => { e.preventDefault(); e.stopPropagation(); - const { onCreate, getView, value } = this.props; const view = getView(); - const { id } = this.props; - const isOpen = Storage.checkToggle(this.getToggleKey(), id); + const isOpen = Storage.checkToggle(getToggleKey(), id); const details = {}; details[view.groupRelationKey] = value; @@ -224,17 +127,74 @@ const Group = observer(class Group extends React.Component<Props> { onCreate({ details }); if (!isOpen) { - this.onToggle(); + onToggle(); }; }; - onAll (e: any) { - const { getObject, parent } = this.props; - const object = getObject(); - + const onAll = (e: any) => { U.Object.openEvent(e, { ...object, _routeParam_: { viewId: parent.content.viewId } }); }; -}); + useEffect(() => { + load(); + initToggle(); + + return () => { + S.Record.recordsClear(subId, ''); + }; + }, []); + + const items = getItems(); + + // Subscriptions + items.forEach(id => { + const object = S.Detail.get(subId, id, [ view.groupRelationKey ]); + }); + + return ( + <div + ref={nodeRef} + className="group" + > + <div id={`item-${id}`} className="clickable" onClick={onToggle}> + <Icon className="arrow" /> + <Cell + id={`board-head-${id}`} + rootId={rootId} + subId={subId} + block={S.Block.getLeaf(rootId, J.Constant.blockId.dataview)} + relationKey={view.groupRelationKey} + viewType={I.ViewType.Board} + getRecord={() => head} + readonly={true} + arrayLimit={2} + withName={true} + placeholder={translate('commonUncategorized')} + /> + {canCreate ? <Icon className="plus" tooltip={translate('commonCreateNewObject')} onClick={onCreate} /> : ''} + </div> + + <div id={`item-${id}-children`} className="items"> + {!items.length ? ( + <div className="item empty">{translate('commonNoObjects')}</div> + ) : ( + <React.Fragment> + {items.map(id => ( + <Item + {...props} + key={`widget-${block.id}-item-${id}`} + subId={subId} + id={id} + hideIcon={view.hideIcon} + /> + ))} + {total > limit ? <div className="item more" onClick={onAll}>{translate('widgetShowAll')}</div> : ''} + </React.Fragment> + )} + </div> + </div> + ); + +})); export default Group; \ No newline at end of file diff --git a/src/ts/component/widget/view/board/index.tsx b/src/ts/component/widget/view/board/index.tsx index 0a165ceaba..f279b18ffb 100644 --- a/src/ts/component/widget/view/board/index.tsx +++ b/src/ts/component/widget/view/board/index.tsx @@ -1,69 +1,45 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect, useState } from 'react'; import { observer } from 'mobx-react'; import { I, S, J, Dataview } from 'Lib'; import Group from './group'; -const WidgetViewBoard = observer(class WidgetViewBoard extends React.Component<I.WidgetViewComponent> { +const WidgetViewBoard = observer(forwardRef<{}, I.WidgetViewComponent>((props, ref) => { - node = null; - - constructor (props: I.WidgetViewComponent) { - super(props); - }; - - render (): React.ReactNode { - const { block, getView } = this.props; - const view = getView(); - const groups = this.getGroups(false); - - return ( - <div ref={ref => this.node = ref} className="body"> - {groups.map(group => ( - <Group - key={`widget-${view.id}-group-${block.id}-${group.id}`} - {...this.props} - {...group} - /> - ))} - </div> - ); - }; - - componentDidMount(): void { - this.load(); - }; - - load () { - const { rootId, getView, getObject } = this.props; - const view = getView(); - const blockId = J.Constant.blockId.dataview; - const object = getObject(); + const { rootId, block, getView, getObject } = props; + const view = getView(); + const object = getObject(); + const blockId = J.Constant.blockId.dataview; + const groups = Dataview.getGroups(rootId, blockId, view.id, false); + const [ dummy, setDummy ] = useState(0); + const load = () => { if (!view) { return; }; S.Record.groupsClear(rootId, blockId); - if (!view.groupRelationKey) { - this.forceUpdate(); - return; - }; - - Dataview.loadGroupList(rootId, blockId, view.id, object); - }; - - getGroups (withHidden: boolean) { - const { rootId, getView } = this.props; - const view = getView(); - - if (!view) { - return []; + if (view.groupRelationKey) { + Dataview.loadGroupList(rootId, blockId, view.id, object); + } else { + setDummy(dummy + 1); }; - - return Dataview.getGroups(rootId, J.Constant.blockId.dataview, view.id, withHidden); }; -}); + useEffect(() => load(), []); + + return ( + <div className="body"> + {groups.map(group => ( + <Group + key={`widget-${view.id}-group-${block.id}-${group.id}`} + {...props} + {...group} + /> + ))} + </div> + ); + +})); export default WidgetViewBoard; \ No newline at end of file diff --git a/src/ts/component/widget/view/board/item.tsx b/src/ts/component/widget/view/board/item.tsx index 406fe01242..1bcf126c76 100644 --- a/src/ts/component/widget/view/board/item.tsx +++ b/src/ts/component/widget/view/board/item.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, MouseEvent, SyntheticEvent } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { ObjectName, Icon, IconObject, DropTarget } from 'Component'; @@ -11,86 +11,21 @@ interface Props extends I.WidgetViewComponent { hideIcon?: boolean; }; -const WidgetBoardItem = observer(class WidgetBoardItem extends React.Component<Props> { - - node = null; - frame = 0; - - constructor (props: Props) { - super(props); - - this.onClick = this.onClick.bind(this); - this.onContext = this.onContext.bind(this); - }; - - render () { - const { subId, id, block, isEditing, hideIcon } = this.props; - const rootId = keyboard.getRootId(); - const object = S.Detail.get(subId, id, J.Relation.sidebar); - const { isReadonly, isArchived, restrictions } = object; - const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); - const iconKey = `widget-icon-${block.id}-${id}`; - const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); - const hasMore = U.Space.canMyParticipantWrite(); - const more = hasMore ? <Icon className="more" tooltip={translate('widgetOptions')} onMouseDown={e => this.onContext(e, true)} /> : null; - - let icon = null; - if (!hideIcon) { - icon = ( - <IconObject - id={iconKey} - key={iconKey} - object={object} - size={18} - iconSize={18} - canEdit={!isReadonly && !isArchived && allowedDetails && U.Object.isTaskLayout(object.layout)} - menuParam={{ - className: 'fixed', - classNameWrap: 'fromSidebar', - }} - /> - ); - }; - - let inner = ( - <div className="inner" onMouseDown={this.onClick}> - {icon} - <ObjectName object={object} /> - - <div className="buttons"> - {more} - </div> - </div> - ); - - if (canDrop) { - inner = ( - <DropTarget - cacheKey={[ block.id, object.id ].join('-')} - id={object.id} - rootId={rootId} - targetContextId={object.id} - dropType={I.DropType.Menu} - canDropMiddle={true} - > - {inner} - </DropTarget> - ); - }; - - return ( - <div - ref={node => this.node = node} - className="item" - key={object.id} - onContextMenu={e => this.onContext(e, false)} - > - {inner} - </div> - ); - }; - - onClick (e: React.MouseEvent) { +const WidgetBoardItem = observer(forwardRef<{}, Props>((props, ref) => { + + const { subId, id, block, isEditing, hideIcon, onContext, getView } = props; + const nodeRef = useRef(null); + const moreRef = useRef(null); + const rootId = keyboard.getRootId(); + const view = getView(); + const object = S.Detail.get(subId, id, J.Relation.sidebar); + const { isReadonly, isArchived, restrictions } = object; + const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); + const iconKey = `widget-icon-${block.id}-${id}`; + const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); + const hasMore = U.Space.canMyParticipantWrite(); + + const onClick = (e: MouseEvent) => { if (e.button) { return; }; @@ -98,24 +33,15 @@ const WidgetBoardItem = observer(class WidgetBoardItem extends React.Component<P e.preventDefault(); e.stopPropagation(); - const { subId, id, } = this.props; - const object = S.Detail.get(subId, id, J.Relation.sidebar); - U.Object.openEvent(e, object); analytics.event('OpenSidebarObject'); }; - onContext (e: React.SyntheticEvent, withElement: boolean) { + const onContextHandler = (e: SyntheticEvent, withElement: boolean) => { e.preventDefault(); e.stopPropagation(); - const { subId, id, getView, onContext } = this.props; - const view = getView(); - if (!view) { - return; - }; - - const node = $(this.node); + const node = $(nodeRef.current); const element = node.find('.icon.more'); onContext({ @@ -130,6 +56,74 @@ const WidgetBoardItem = observer(class WidgetBoardItem extends React.Component<P }); }; -}); + let icon = null; + let more = null; + + if (hasMore) { + more = ( + <Icon + ref={moreRef} + className="more" + tooltip={translate('widgetOptions')} + onMouseDown={e => onContextHandler(e, true)} + /> + ); + }; + + if (!hideIcon) { + icon = ( + <IconObject + id={iconKey} + key={iconKey} + object={object} + size={18} + iconSize={18} + canEdit={!isReadonly && !isArchived && allowedDetails && U.Object.isTaskLayout(object.layout)} + menuParam={{ + className: 'fixed', + classNameWrap: 'fromSidebar', + }} + /> + ); + }; + + let inner = ( + <div className="inner" onMouseDown={onClick}> + {icon} + <ObjectName object={object} /> + + <div className="buttons"> + {more} + </div> + </div> + ); + + if (canDrop) { + inner = ( + <DropTarget + cacheKey={[ block.id, object.id ].join('-')} + id={object.id} + rootId={rootId} + targetContextId={object.id} + dropType={I.DropType.Menu} + canDropMiddle={true} + > + {inner} + </DropTarget> + ); + }; + + return ( + <div + ref={nodeRef} + className="item" + key={object.id} + onContextMenu={e => onContextHandler(e, false)} + > + {inner} + </div> + ); + +})); export default WidgetBoardItem; \ No newline at end of file diff --git a/src/ts/component/widget/view/calendar/index.tsx b/src/ts/component/widget/view/calendar/index.tsx index 2e5403f6f3..cd97d35539 100644 --- a/src/ts/component/widget/view/calendar/index.tsx +++ b/src/ts/component/widget/view/calendar/index.tsx @@ -1,118 +1,30 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useEffect, useRef, useImperativeHandle } from 'react'; import { observer } from 'mobx-react'; import { Select, Icon } from 'Component'; import { I, S, U, J, translate } from 'Lib'; -interface State { - value: number; +interface WidgetViewCalendarRefProps { + getFilters: () => I.Filter[]; }; -const WidgetViewCalendar = observer(class WidgetViewCalendar extends React.Component<I.WidgetViewComponent, State> { +const WidgetViewCalendar = observer(forwardRef<WidgetViewCalendarRefProps, I.WidgetViewComponent>((props, ref: any) => { - node = null; - refMonth = null; - refYear = null; - state = { - value: U.Date.now(), - }; - - constructor (props: I.WidgetViewComponent) { - super(props); + const [ value, setValue ] = useState(U.Date.now()); + const { rootId, block, canCreate, getView, reload, onCreate } = props; + const monthRef = useRef(null); + const yearRef = useRef(null); + const view = getView(); + const { groupRelationKey } = view; + const data = U.Date.getCalendarMonth(value); - this.onArrow = this.onArrow.bind(this); + const getDateParam = (t: number) => { + const [ d, m, y ] = U.Date.date('j,n,Y', t).split(',').map(it => Number(it)); + return { d, m, y }; }; - render (): React.ReactNode { - const { value } = this.state; - const { block, getView } = this.props; - const view = getView(); - - if (!view) { - return null; - }; - - const data = U.Date.getCalendarMonth(value); - const { m, y } = this.getDateParam(value); - const today = this.getDateParam(U.Date.now()); - const days = U.Date.getWeekDays(); - const months = U.Date.getMonths(); - const years = U.Date.getYears(0, 3000); - const { groupRelationKey } = view; - const dotMap = this.getDotMap(groupRelationKey); - - return ( - <div ref={ref => this.node = ref} className="body"> - <div id="dateSelect" className="dateSelect"> - <div className="side left"> - <Select - ref={ref => this.refMonth = ref} - id={`widget-${block.id}-calendar-month`} - value={m} - options={months} - className="month" - onChange={m => this.setValue(U.Date.timestamp(y, m, 1))} - /> - <Select - ref={ref => this.refYear = ref} - id={`widget-${block.id}-calendar-year`} - value={y} - options={years} - className="year" - onChange={y => this.setValue(U.Date.timestamp(y, m, 1))} - /> - </div> - - <div className="side right"> - <Icon className="arrow left" onClick={() => this.onArrow(-1)} /> - <Icon className="arrow right" onClick={() => this.onArrow(1)} /> - </div> - </div> - - <div className="table"> - <div className="tableHead"> - {days.map((item, i) => ( - <div key={i} className="item"> - <div className="inner">{item.name.substring(0, 2)}</div> - </div> - ))} - </div> - - <div className="tableBody"> - {data.map((item, i) => { - const cn = [ 'day' ]; - if (m != item.m) { - cn.push('other'); - }; - if ((today.d == item.d) && (today.m == item.m) && (today.y == item.y)) { - cn.push('today'); - }; - if (i < 7) { - cn.push('first'); - }; - - const check = dotMap.get([ item.d, item.m, item.y ].join('-')); - return ( - <div - id={[ 'day', item.d, item.m, item.y ].join('-')} - key={i} - className={cn.join(' ')} - onClick={() => this.onClick(item.d, item.m, item.y)} - onContextMenu={(e: any) => this.onContextMenu(e, item)} - > - <div className="inner"> - {item.d} - {check ? <div className="bullet" /> : ''} - </div> - </div> - ); - })} - </div> - </div> - </div> - ); - }; + let { m, y } = getDateParam(value); - onContextMenu = (e: any, item: any) => { + const onContextMenu = (e: any, item: any) => { e.preventDefault(); e.stopPropagation(); @@ -123,31 +35,18 @@ const WidgetViewCalendar = observer(class WidgetViewCalendar extends React.Compo data: { options: [ { id: 'open', icon: 'expand', name: translate('commonOpenObject') } ], onSelect: () => { - U.Object.openDateByTimestamp(U.Date.timestamp(item.y, item.m, item.d), 'auto'); + U.Object.openDateByTimestamp(groupRelationKey, U.Date.timestamp(item.y, item.m, item.d), 'auto'); } } }); }; - componentDidMount(): void { - this.setSelectsValue(this.state.value); - }; - - setSelectsValue (value: number) { - const { m, y } = this.getDateParam(value); - - this.refMonth?.setValue(m); - this.refYear?.setValue(y); + const setSelectsValue = () => { + monthRef.current.setValue(m); + yearRef.current.setValue(y); }; - getDateParam (t: number) { - const [ d, m, y ] = U.Date.date('j,n,Y', t).split(',').map(it => Number(it)); - return { d, m, y }; - }; - - onArrow (dir: number) { - let { m, y } = this.getDateParam(this.state.value); - + const onArrow = (dir: number) => { m += dir; if (m < 0) { m = 12; @@ -158,18 +57,10 @@ const WidgetViewCalendar = observer(class WidgetViewCalendar extends React.Compo y++; }; - this.setValue(U.Date.timestamp(y, m, 1)); + setValue(U.Date.timestamp(y, m, 1)); }; - setValue (value: number) { - this.state.value = value; - this.setState({ value }, () => this.props.reload()); - this.setSelectsValue(value); - }; - - onClick (d: number, m: number, y: number) { - const { rootId, getView, canCreate, onCreate } = this.props; - const view = getView(); + const onClick = (d: number, m: number, y: number) => { const element = `#day-${d}-${m}-${y}`; S.Menu.closeAll([ 'dataviewCalendarDay' ], () => { @@ -202,16 +93,13 @@ const WidgetViewCalendar = observer(class WidgetViewCalendar extends React.Compo }); }; - getFilters (): I.Filter[] { - const { getView } = this.props; - const view = getView(); + const getFilters = (): I.Filter[] => { const relation = S.Record.getRelationByKey(view.groupRelationKey); - if (!relation) { return []; }; - const data = U.Date.getCalendarMonth(this.state.value); + const data = U.Date.getCalendarMonth(value); if (!data.length) { return; }; @@ -239,9 +127,7 @@ const WidgetViewCalendar = observer(class WidgetViewCalendar extends React.Compo ]; }; - getDotMap (relationKey: string): Map<string, boolean> { - const { value } = this.state; - const { rootId } = this.props; + const getDotMap = (relationKey: string): Map<string, boolean> => { const data = U.Date.getCalendarMonth(value); const items = S.Record.getRecords(S.Record.getSubId(rootId, J.Constant.blockId.dataview), [ relationKey ]); const ret = new Map(); @@ -257,6 +143,93 @@ const WidgetViewCalendar = observer(class WidgetViewCalendar extends React.Compo return ret; }; -}); + useEffect(() => setSelectsValue(), []); + useEffect(() => { + setSelectsValue(); + reload(); + }, [ value ]); + + useImperativeHandle(ref, () => ({ + getFilters, + })); + + const today = getDateParam(U.Date.now()); + const days = U.Date.getWeekDays(); + const months = U.Date.getMonths(); + const years = U.Date.getYears(0, 3000); + const dotMap = getDotMap(groupRelationKey); + + return ( + <div className="body"> + <div id="dateSelect" className="dateSelect"> + <div className="side left"> + <Select + ref={monthRef} + id={`widget-${block.id}-calendar-month`} + value={m} + options={months} + className="month" + onChange={m => setValue(U.Date.timestamp(y, m, 1))} + /> + <Select + ref={yearRef} + id={`widget-${block.id}-calendar-year`} + value={y} + options={years} + className="year" + onChange={y => setValue(U.Date.timestamp(y, m, 1))} + /> + </div> + + <div className="side right"> + <Icon className="arrow left" onClick={() => onArrow(-1)} /> + <Icon className="arrow right" onClick={() => onArrow(1)} /> + </div> + </div> + + <div className="table"> + <div className="tableHead"> + {days.map((item, i) => ( + <div key={i} className="item"> + <div className="inner">{item.name.substring(0, 2)}</div> + </div> + ))} + </div> + + <div className="tableBody"> + {data.map((item, i) => { + const cn = [ 'day' ]; + if (m != item.m) { + cn.push('other'); + }; + if ((today.d == item.d) && (today.m == item.m) && (today.y == item.y)) { + cn.push('today'); + }; + if (i < 7) { + cn.push('first'); + }; + + const check = dotMap.get([ item.d, item.m, item.y ].join('-')); + return ( + <div + id={[ 'day', item.d, item.m, item.y ].join('-')} + key={i} + className={cn.join(' ')} + onClick={() => onClick(item.d, item.m, item.y)} + onContextMenu={(e: any) => onContextMenu(e, item)} + > + <div className="inner"> + {item.d} + {check ? <div className="bullet" /> : ''} + </div> + </div> + ); + })} + </div> + </div> + </div> + ); + +})); export default WidgetViewCalendar; \ No newline at end of file diff --git a/src/ts/component/widget/view/gallery/index.tsx b/src/ts/component/widget/view/gallery/index.tsx index e4caf547cb..9f321e029b 100644 --- a/src/ts/component/widget/view/gallery/index.tsx +++ b/src/ts/component/widget/view/gallery/index.tsx @@ -1,39 +1,30 @@ -import * as React from 'react'; +import React, { forwardRef } from 'react'; import { observer } from 'mobx-react'; -import { I, S, U, J } from 'Lib'; +import { I, S, J } from 'Lib'; import Item from './item'; -const WidgetViewGallery = observer(class WidgetViewGallery extends React.Component<I.WidgetViewComponent> { +const WidgetViewGallery = observer(forwardRef<{}, I.WidgetViewComponent>((props, ref) => { + + const { block, subId, getView, getRecordIds } = props; + const view = getView(); + const items = getRecordIds().map(id => S.Detail.get(subId, id, J.Relation.sidebar)); - node = null; - - render (): React.ReactNode { - const { block, subId, getView } = this.props; - const view = getView(); - const items = this.getItems(); - - return ( - <div ref={ref => this.node = ref} className="body"> - <div id="items" className="items"> - {items.map(item => ( - <Item - {...this.props} - key={`widget-${block.id}-item-${item.id}`} - subId={subId} - id={item.id} - hideIcon={view.hideIcon} - /> - ))} - </div> + return ( + <div className="body"> + <div id="items" className="items"> + {items.map(item => ( + <Item + {...props} + key={`widget-${block.id}-item-${item.id}`} + subId={subId} + id={item.id} + hideIcon={view.hideIcon} + /> + ))} </div> - ); - }; - - getItems () { - const { getRecordIds, subId } = this.props; - return getRecordIds().map(id => S.Detail.get(subId, id, J.Relation.sidebar)); - }; + </div> + ); -}); +})); export default WidgetViewGallery; \ No newline at end of file diff --git a/src/ts/component/widget/view/gallery/item.tsx b/src/ts/component/widget/view/gallery/item.tsx index 694a0bcea1..c5c0b0b2ee 100644 --- a/src/ts/component/widget/view/gallery/item.tsx +++ b/src/ts/component/widget/view/gallery/item.tsx @@ -1,6 +1,5 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect } from 'react'; import $ from 'jquery'; -import raf from 'raf'; import { observer } from 'mobx-react'; import { ObjectName, IconObject, DropTarget, ObjectCover } from 'Component'; import { I, S, U, J, keyboard, analytics, Dataview } from 'Lib'; @@ -12,97 +11,31 @@ interface Props extends I.WidgetViewComponent { hideIcon?: boolean; }; -const WidgetBoardItem = observer(class WidgetBoardItem extends React.Component<Props> { - - node = null; - frame = 0; - - constructor (props: Props) { - super(props); - - this.onClick = this.onClick.bind(this); - this.onContext = this.onContext.bind(this); - }; - - render () { - const { subId, id, block, isEditing, hideIcon } = this.props; - const rootId = keyboard.getRootId(); - const object = S.Detail.get(subId, id, J.Relation.sidebar); - const { isReadonly, isArchived, restrictions } = object; - const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); - const iconKey = `widget-icon-${block.id}-${id}`; - const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); - const cn = [ 'item' ]; - const cover = this.getCoverObject(); - - if (cover) { - cn.push('withCover'); - }; - - let icon = null; - if (!hideIcon) { - icon = ( - <IconObject - id={iconKey} - key={iconKey} - object={object} - size={16} - iconSize={16} - canEdit={!isReadonly && !isArchived && allowedDetails && U.Object.isTaskLayout(object.layout)} - menuParam={{ - className: 'fixed', - classNameWrap: 'fromSidebar', - }} - /> - ); - }; - - let inner = ( - <div className="inner" onMouseDown={this.onClick}> - <ObjectCover object={cover} /> - - <div className="info"> - {icon} - <ObjectName object={object} /> - </div> - </div> - ); - - if (canDrop) { - inner = ( - <DropTarget - cacheKey={[ block.id, object.id ].join('-')} - id={object.id} - rootId={rootId} - targetContextId={object.id} - dropType={I.DropType.Menu} - canDropMiddle={true} - > - {inner} - </DropTarget> - ); - }; - - return ( - <div - ref={node => this.node = node} - className={cn.join(' ')} - onContextMenu={this.onContext} - > - {inner} - </div> - ); - }; - - componentDidMount (): void { - this.resize(); +const WidgetGalleryItem = observer(forwardRef<{}, Props>(({ + subId = '', + id = '', + block, + isEditing = false, + hideIcon = false, + getView, +}, ref) => { + + const nodeRef = useRef(null); + const view = getView(); + const rootId = keyboard.getRootId(); + const object = S.Detail.get(subId, id, J.Relation.sidebar); + const { isReadonly, isArchived, restrictions } = object; + const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); + const iconKey = `widget-icon-${block.id}-${id}`; + const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); + const cn = [ 'item' ]; + const cover = view ? Dataview.getCoverObject(subId, object, view.coverRelationKey) : null; + + if (cover) { + cn.push('withCover'); }; - componentDidUpdate (): void { - this.resize(); - }; - - onClick (e: React.MouseEvent) { + const onClick = (e: React.MouseEvent) => { if (e.button) { return; }; @@ -110,31 +43,15 @@ const WidgetBoardItem = observer(class WidgetBoardItem extends React.Component<P e.preventDefault(); e.stopPropagation(); - U.Object.openEvent(e, this.getObject()); + U.Object.openEvent(e, object); analytics.event('OpenSidebarObject'); }; - getObject () { - const { subId, id, } = this.props; - return S.Detail.get(subId, id); - }; - - onContext (e: React.MouseEvent) { + const onContext = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); - const { subId, id, getView } = this.props; - const view = getView(); - if (!view) { - return; - }; - - const canWrite = U.Space.canMyParticipantWrite(); - if (!canWrite) { - return; - }; - - const node = $(this.node); + const node = $(nodeRef.current); S.Menu.open('dataviewContext', { element: node, @@ -152,25 +69,68 @@ const WidgetBoardItem = observer(class WidgetBoardItem extends React.Component<P }); }; - getCoverObject (): any { - const { getView, subId } = this.props; - const view = getView(); + const resize = () => { + const node = $(nodeRef.current); - return view ? Dataview.getCoverObject(subId, this.getObject(), view.coverRelationKey) : null; + node.toggleClass('withIcon', !!node.find('.iconObject').length); }; - resize () { - if (this.frame) { - raf.cancel(this.frame); - }; + let icon = null; + if (!hideIcon) { + icon = ( + <IconObject + id={iconKey} + key={iconKey} + object={object} + size={16} + iconSize={16} + canEdit={!isReadonly && !isArchived && allowedDetails && U.Object.isTaskLayout(object.layout)} + menuParam={{ + className: 'fixed', + classNameWrap: 'fromSidebar', + }} + /> + ); + }; - this.frame = raf(() => { - const node = $(this.node); + let inner = ( + <div className="inner" onMouseDown={onClick}> + <ObjectCover object={cover} /> - node.toggleClass('withIcon', !!node.find('.iconObject').length); - }); + <div className="info"> + {icon} + <ObjectName object={object} /> + </div> + </div> + ); + + if (canDrop) { + inner = ( + <DropTarget + cacheKey={[ block.id, object.id ].join('-')} + id={object.id} + rootId={rootId} + targetContextId={object.id} + dropType={I.DropType.Menu} + canDropMiddle={true} + > + {inner} + </DropTarget> + ); }; -}); + useEffect(() => resize()); + + return ( + <div + ref={nodeRef} + className={cn.join(' ')} + onContextMenu={onContext} + > + {inner} + </div> + ); + +})); -export default WidgetBoardItem; \ No newline at end of file +export default WidgetGalleryItem; \ No newline at end of file diff --git a/src/ts/component/widget/view/graph/index.tsx b/src/ts/component/widget/view/graph/index.tsx index adeb42bd24..6b1208bf9f 100644 --- a/src/ts/component/widget/view/graph/index.tsx +++ b/src/ts/component/widget/view/graph/index.tsx @@ -1,59 +1,16 @@ -import * as React from 'react'; +import React, { forwardRef, useEffect, useRef } from 'react'; import { observer } from 'mobx-react'; import { I, C, S, U, J, Dataview } from 'Lib'; import { GraphProvider } from 'Component'; -const WidgetViewGraph = observer(class WidgetViewGraph extends React.Component<I.WidgetViewComponent> { +const WidgetViewGraph = observer(forwardRef<{}, I.WidgetViewComponent>((props, ref) => { + + const { block, getView, getObject } = props; + const graphRef = useRef(null); + const data = useRef({} as any); + const view = getView(); - _isMounted = false; - node: any = null; - data: any = { - nodes: [], - edges: [], - }; - ids: string[] = []; - refGraph: any = null; - rootId = ''; - - render () { - const { block } = this.props; - - return ( - <div - ref={node => this.node = node} - className="wrap" - > - <GraphProvider - key="graph" - {...this.props} - ref={ref => this.refGraph = ref} - id={block.id} - rootId="" - data={this.data} - storageKey={J.Constant.graphId.dataview} - /> - </div> - ); - }; - - componentDidMount () { - this._isMounted = true; - - this.resize(); - this.load(); - }; - - componentDidUpdate () { - this.resize(); - }; - - componentWillUnmount () { - this._isMounted = false; - }; - - load () { - const { getView, getObject } = this.props; - const view = getView(); + const load = () => { if (!view) { return; }; @@ -63,24 +20,36 @@ const WidgetViewGraph = observer(class WidgetViewGraph extends React.Component<I const isCollection = U.Object.isCollectionLayout(object.layout); C.ObjectGraph(S.Common.space, filters, 0, [], J.Relation.graph, (isCollection ? object.id : ''), object.setOf, (message: any) => { - if (!this._isMounted || message.error.code) { - return; - }; - - this.data.edges = message.edges; - this.data.nodes = message.nodes; - this.forceUpdate(); + data.current.edges = message.edges; + data.current.nodes = message.nodes; - if (this.refGraph) { - this.refGraph.init(); + if (graphRef.current) { + graphRef.current.init(); }; }); }; - resize () { - this.refGraph?.resize(); + const resize = () => { + graphRef.current.resize(); }; -}); + useEffect(() => load(), []); + useEffect(() => resize()); + + return ( + <div className="wrap"> + <GraphProvider + key="graph" + {...props} + ref={graphRef} + id={block.id} + rootId="" + data={data.current} + storageKey={J.Constant.graphId.dataview} + /> + </div> + ); + +})); export default WidgetViewGraph; \ No newline at end of file diff --git a/src/ts/component/widget/view/index.tsx b/src/ts/component/widget/view/index.tsx index 3c1e7c1b2b..f586941eff 100644 --- a/src/ts/component/widget/view/index.tsx +++ b/src/ts/component/widget/view/index.tsx @@ -1,7 +1,7 @@ -import * as React from 'react'; +import React, { forwardRef, useState, useRef, useEffect, useImperativeHandle } from 'react'; import { observer } from 'mobx-react'; import { Select, Label, Button } from 'Component'; -import { I, C, M, S, U, J, Dataview, Relation, keyboard, translate } from 'Lib'; +import { I, C, M, S, U, J, Dataview, Relation, keyboard, translate, analytics } from 'Lib'; import WidgetViewList from './list'; import WidgetViewGallery from './gallery'; @@ -9,181 +9,28 @@ import WidgetViewBoard from './board'; import WidgetViewCalendar from './calendar'; import WidgetViewGraph from './graph'; -interface State { - isLoading: boolean; +interface WidgetViewRefProps { + updateData: () => void; + updateViews: () => void; + onOpen: () => void; }; -const WidgetView = observer(class WidgetView extends React.Component<I.WidgetComponent, State> { - - node = null; - state = { - isLoading: false, - }; - refSelect = null; - refChild = null; - - constructor (props: I.WidgetComponent) { - super(props); - - this.getView = this.getView.bind(this); - this.getViewType = this.getViewType.bind(this); - this.getSubId = this.getSubId.bind(this); - this.getRecordIds = this.getRecordIds.bind(this); - this.getObject = this.getObject.bind(this); - this.getLimit = this.getLimit.bind(this); - this.reload = this.reload.bind(this); - this.onChangeView = this.onChangeView.bind(this); - }; - - render (): React.ReactNode { - const { parent, block, isSystemTarget, onCreate } = this.props; - const { viewId, limit, layout } = parent.content; - const { targetBlockId } = block.content; - const { isLoading } = this.state; - const rootId = this.getRootId(); - const subId = this.getSubId(); - const records = this.getRecordIds(); - const length = records.length; - const views = S.Record.getViews(rootId, J.Constant.blockId.dataview).map(it => ({ ...it, name: it.name || translate('defaultNamePage') })); - const viewType = this.getViewType(); - const cn = [ 'innerWrap' ]; - const showEmpty = ![ I.ViewType.Calendar, I.ViewType.Board ].includes(viewType); - const canCreate = this.props.canCreate && this.isAllowedObject(); - const props = { - ...this.props, - ref: ref => this.refChild = ref, - reload: this.reload, - getRecordIds: this.getRecordIds, - getView: this.getView, - getViewType: this.getViewType, - getObject: this.getObject, - getViewLimit: this.getLimit, - rootId, - subId, - }; - - let content = null; - let viewSelect = null; - - if (!isSystemTarget() && (views.length > 1)) { - viewSelect = ( - <Select - ref={ref => this.refSelect = ref} - id={`select-view-${rootId}`} - value={viewId} - options={views} - onChange={this.onChangeView} - arrowClassName="light" - menuParam={{ - width: 300, - className: 'fixed', - classNameWrap: 'fromSidebar', - }} - /> - ); - }; - - if (!isLoading && !length && showEmpty) { - content = ( - <div className="emptyWrap"> - <Label className="empty" text={canCreate ? translate('widgetEmptyLabelCreate') : translate('widgetEmptyLabel')} /> - {canCreate ? <Button text={translate('commonCreateObject')} color="blank" className="c28" onClick={onCreate} /> : ''} - </div> - ); - } else { - if (layout == I.WidgetLayout.View) { - cn.push(`view${I.ViewType[viewType]}`); - switch (viewType) { - default: { - content = <WidgetViewList {...props} />; - break; - }; - - case I.ViewType.Gallery: { - content = <WidgetViewGallery {...props} />; - break; - }; - - case I.ViewType.Board: { - content = <WidgetViewBoard {...props} />; - break; - }; - - case I.ViewType.Calendar: { - content = <WidgetViewCalendar {...props} />; - break; - }; - - case I.ViewType.Graph: { - content = <WidgetViewGraph {...props} />; - break; - }; - }; - } else { - cn.push('viewList'); - content = <WidgetViewList {...props} />; - }; - }; - - return ( - <div - ref={node => this.node = node} - id="innerWrap" - className={cn.join(' ')} - > - {viewSelect ? <div id="viewSelect">{viewSelect}</div> : ''} - {content} - </div> - ); - }; - - componentDidMount (): void { - const { block, isSystemTarget, getData, getTraceId } = this.props; - const { targetBlockId } = block.content; - - if (isSystemTarget()) { - getData(this.getSubId()); - } else { - this.setState({ isLoading: true }); - - C.ObjectShow(targetBlockId, getTraceId(), U.Router.getRouteSpaceId(), () => { - this.setState({ isLoading: false }); - - const view = this.getView(); - if (view) { - this.load(view.id); - }; - }); - }; - }; - - componentDidUpdate (): void { - const { parent, isSystemTarget } = this.props; - const { viewId } = parent.content; - const view = Dataview.getView(this.getRootId(), J.Constant.blockId.dataview); - - if (!isSystemTarget() && view && viewId && (viewId != view.id)) { - const ref = this.refSelect; - - if (ref) { - const selectValue = ref.getValue(); - if (viewId != selectValue) { - ref.setValue(viewId); - }; - }; - - this.load(viewId); - }; - }; - - componentWillUnmount(): void { - C.ObjectSearchUnsubscribe([ this.getSubId() ]); - }; - - updateData () { - const { block, isSystemTarget, getData } = this.props; - const targetId = block.getTargetObjectId(); - const rootId = this.getRootId(); +const WidgetView = observer(forwardRef<WidgetViewRefProps, I.WidgetComponent>((props, ref: any) => { + + const { parent, block, isSystemTarget, onCreate, getData, getTraceId, getLimit, sortFavorite } = props; + const { viewId, limit, layout } = parent.content; + const targetId = block ? block.getTargetObjectId() : ''; + const [ isLoading, setIsLoading ] = useState(false); + const nodeRef = useRef(null); + const selectRef = useRef(null); + const childRef = useRef(null); + const rootId = block ? [ targetId, 'widget', block.id ].join('-') : ''; + const subId = S.Record.getSubId(rootId, J.Constant.blockId.dataview); + const object = S.Detail.get(S.Block.widgets, targetId); + const view = Dataview.getView(rootId, J.Constant.blockId.dataview, viewId); + const viewType = view ? view.type : I.ViewType.List; + + const updateData = () =>{ const srcObject = S.Detail.get(targetId, targetId); const srcBlock = S.Block.getLeaf(targetId, J.Constant.blockId.dataview); @@ -201,56 +48,35 @@ const WidgetView = observer(class WidgetView extends React.Component<I.WidgetCom }; if (isSystemTarget()) { - getData(this.getSubId()); + getData(subId); } else { - const view = Dataview.getView(this.getRootId(), J.Constant.blockId.dataview); + const view = Dataview.getView(subId, J.Constant.blockId.dataview); if (view) { - this.load(view.id); + load(view.id); }; }; }; - updateViews () { - const { block } = this.props; - const { targetBlockId } = block.content; - const views = U.Common.objectCopy(S.Record.getViews(targetBlockId, J.Constant.blockId.dataview)).map(it => new M.View(it)); - const rootId = this.getRootId(); + const updateViews = () => { + const views = U.Common.objectCopy(S.Record.getViews(targetId, J.Constant.blockId.dataview)).map(it => new M.View(it)); - if (!views.length || (targetBlockId != keyboard.getRootId())) { + if (!views.length || (targetId != keyboard.getRootId())) { return; }; S.Record.viewsClear(rootId, J.Constant.blockId.dataview); S.Record.viewsSet(rootId, J.Constant.blockId.dataview, views); - if (this.refSelect) { - this.refSelect.setOptions(views); - }; + selectRef.current?.setOptions(views); }; - getSubId () { - return S.Record.getSubId(this.getRootId(), J.Constant.blockId.dataview); - }; - - getRootId = (): string => { - const { block } = this.props; - const { targetBlockId } = block.content; - - return [ targetBlockId, 'widget', block.id ].join('-'); - }; - - load (viewId: string) { - const subId = this.getSubId(); - - if (this.refChild && this.refChild.load) { - this.refChild.load(); - S.Record.metaSet(this.getSubId(), '', { viewId }); + const load = (viewId: string) => { + if (childRef.current?.load) { + childRef.current?.load(); + S.Record.metaSet(subId, '', { viewId }); return; }; - const rootId = this.getRootId(); - const blockId = J.Constant.blockId.dataview; - const object = this.getObject(); const setOf = Relation.getArrayValue(object.setOf); const isCollection = U.Object.isCollectionLayout(object.layout); @@ -259,91 +85,63 @@ const WidgetView = observer(class WidgetView extends React.Component<I.WidgetCom return; }; - const limit = this.getLimit(); - const view = this.getView(); + const view = getView(); if (!view) { return; }; Dataview.getData({ rootId, - blockId, + blockId: J.Constant.blockId.dataview, newViewId: viewId, sources: setOf, - limit, - filters: this.getFilters(), + limit: getLimitHandler(), + filters: getFilters(), collectionId: (isCollection ? object.id : ''), keys: J.Relation.sidebar.concat([ view.groupRelationKey, view.coverRelationKey ]).concat(J.Relation.cover), }); }; - reload () { - this.load(this.props.parent.content.viewId); - }; - - getFilters () { - const view = this.getView(); + const getFilters = () => { if (!view) { return []; }; let filters: I.Filter[] = []; - if (this.refChild && this.refChild.getFilters) { - filters = filters.concat(this.refChild.getFilters()); + if (childRef.current?.getFilters) { + filters = filters.concat(childRef.current?.getFilters()); }; return filters; }; - getView () { - return Dataview.getView(this.getRootId(), J.Constant.blockId.dataview, this.props.parent.content.viewId); - }; - - getViewType () { - const view = this.getView(); - return view ? view.type : I.ViewType.List; - }; - - getObject () { - return S.Detail.get(S.Block.widgets, this.props.block.getTargetObjectId()); + const getView = () => { + return Dataview.getView(rootId, J.Constant.blockId.dataview, parent.content.viewId); }; - getLimit (): number { - const { parent, getLimit } = this.props; - const { layout } = parent.content; - const viewType = this.getViewType(); - + const getLimitHandler = (): number => { let limit = getLimit(parent.content); - if ((layout == I.WidgetLayout.View) && (viewType == I.ViewType.Calendar)) { limit = 1000; }; - return limit; }; - onChangeView (viewId: string) { - C.BlockWidgetSetViewId(S.Block.widgets, this.props.parent.id, viewId); + const onChangeView = (viewId: string) => { + C.BlockWidgetSetViewId(S.Block.widgets, parent.id, viewId); }; - getRecordIds () { - const { parent, block, sortFavorite } = this.props; - const { targetBlockId } = block.content; - const rootId = this.getRootId(); - const subId = this.getSubId(); + const getRecordIds = () => { const records = S.Record.getRecordIds(subId, ''); const views = S.Record.getViews(rootId, J.Constant.blockId.dataview); const viewId = parent.content.viewId || (views.length ? views[0].id : ''); const ret = Dataview.applyObjectOrder(rootId, J.Constant.blockId.dataview, viewId, '', U.Common.objectCopy(records)); - return (targetBlockId == J.Constant.widgetId.favorite) ? sortFavorite(ret) : ret; + return (targetId == J.Constant.widgetId.favorite) ? sortFavorite(ret) : ret; }; - isAllowedObject () { - const { isSystemTarget } = this.props; - const rootId = this.getRootId(); - const object = this.getObject(); + const isAllowedObject = () => { const isCollection = U.Object.isCollectionLayout(object.layout); if (isSystemTarget()) { @@ -359,7 +157,7 @@ const WidgetView = observer(class WidgetView extends React.Component<I.WidgetCom return true; }; - const sources = this.getSources(); + const sources = getSources(); if (!sources.length) { return false; }; @@ -377,26 +175,162 @@ const WidgetView = observer(class WidgetView extends React.Component<I.WidgetCom return isAllowed; }; - getSources (): string[] { - const object = this.getObject(); - + const getSources = (): string[] => { if (U.Object.isCollectionLayout(object.layout)) { return []; }; - const rootId = this.getRootId(); const types = Relation.getSetOfObjects(rootId, object.id, I.ObjectLayout.Type).map(it => it.id); const relations = Relation.getSetOfObjects(rootId, object.id, I.ObjectLayout.Relation).map(it => it.id); return [].concat(types).concat(relations); }; - onOpen () { - if (this.refChild && this.refChild.onOpen) { - this.refChild.onOpen(); + const onOpen = () => { + if (childRef.current?.onOpen) { + childRef.current?.onOpen(); }; }; -}); + const records = getRecordIds(); + const length = records.length; + const views = S.Record.getViews(rootId, J.Constant.blockId.dataview).map(it => ({ ...it, name: it.name || translate('defaultNamePage') })); + const cn = [ 'innerWrap' ]; + const showEmpty = ![ I.ViewType.Calendar, I.ViewType.Board ].includes(viewType); + const canCreate = props.canCreate && isAllowedObject(); + const childProps = { + ...props, + ref: childRef, + rootId, + subId, + reload: () => load(viewId), + getRecordIds, + getView: () => view, + getViewType: () => viewType, + getObject: () => object, + getViewLimit: getLimitHandler, + }; + + let content = null; + let viewSelect = null; + + if (!isSystemTarget() && (views.length > 1)) { + viewSelect = ( + <Select + ref={selectRef} + id={`select-view-${rootId}`} + value={viewId} + options={views} + onChange={onChangeView} + arrowClassName="light" + menuParam={{ + width: 300, + className: 'fixed', + classNameWrap: 'fromSidebar', + }} + /> + ); + }; + + if (!isLoading && !length && showEmpty) { + content = ( + <div className="emptyWrap"> + <Label className="empty" text={canCreate ? translate('widgetEmptyLabelCreate') : translate('widgetEmptyLabel')} /> + {canCreate ? ( + <Button + text={translate('commonCreateObject')} + color="blank" + className="c28" + onClick={() => onCreate({ route: analytics.route.inWidget })} + /> + ) : ''} + </div> + ); + } else { + if (layout == I.WidgetLayout.View) { + cn.push(`view${I.ViewType[viewType]}`); + switch (viewType) { + default: { + content = <WidgetViewList {...childProps} />; + break; + }; + + case I.ViewType.Gallery: { + content = <WidgetViewGallery {...childProps} />; + break; + }; + + case I.ViewType.Board: { + content = <WidgetViewBoard {...childProps} />; + break; + }; + + case I.ViewType.Calendar: { + content = <WidgetViewCalendar {...childProps} />; + break; + }; + + case I.ViewType.Graph: { + content = <WidgetViewGraph {...childProps} />; + break; + }; + }; + } else { + cn.push('viewList'); + content = <WidgetViewList {...childProps} />; + }; + }; + + useEffect(() => { + if (isSystemTarget()) { + getData(subId); + } else { + setIsLoading(true); + + C.ObjectShow(targetId, getTraceId(), U.Router.getRouteSpaceId(), () => { + setIsLoading(false); + + const view = getView(); + if (view) { + load(view.id); + }; + }); + }; + + return () => { + C.ObjectSearchUnsubscribe([ subId ]); + }; + }, []); + + useEffect(() => { + if (!isSystemTarget() && view && viewId && (viewId != view.id)) { + if (selectRef.current) { + if (viewId != selectRef.current.getValue()) { + selectRef.current.setValue(viewId); + }; + }; + + load(viewId); + }; + }); + + useImperativeHandle(ref, () => ({ + updateData, + updateViews, + onOpen, + })); + + return ( + <div + ref={nodeRef} + id="innerWrap" + className={cn.join(' ')} + > + {viewSelect ? <div id="viewSelect">{viewSelect}</div> : ''} + {content} + </div> + ); + +})); export default WidgetView; \ No newline at end of file diff --git a/src/ts/component/widget/view/list/index.tsx b/src/ts/component/widget/view/list/index.tsx index ffbf816fe8..98d20c0475 100644 --- a/src/ts/component/widget/view/list/index.tsx +++ b/src/ts/component/widget/view/list/index.tsx @@ -1,4 +1,4 @@ -import * as React from 'react'; +import React, { forwardRef, useRef, useEffect } from 'react'; import raf from 'raf'; import { observer } from 'mobx-react'; import { AutoSizer, CellMeasurer, CellMeasurerCache, InfiniteLoader, List as VList } from 'react-virtualized'; @@ -11,166 +11,32 @@ const LIMIT = 30; const HEIGHT_COMPACT = 28; const HEIGHT_LIST = 64; -const WidgetViewList = observer(class WidgetViewList extends React.Component<I.WidgetViewComponent> { +const WidgetViewList = observer(forwardRef<{}, I.WidgetViewComponent>((props, ref) => { - node = null; - refList = null; - cache: any = null; - top = 0; + const { parent, block, isPreview, subId, getRecordIds, addGroupLabels } = props; + const cache = useRef({}); + const nodeRef = useRef(null); + const listRef = useRef(null); + const top = useRef(0); + const { total } = S.Record.getMeta(subId, ''); + const isCompact = [ I.WidgetLayout.Compact, I.WidgetLayout.View ].includes(parent.content.layout); - constructor (props: I.WidgetViewComponent) { - super(props); - - this.onSortStart = this.onSortStart.bind(this); - this.onSortEnd = this.onSortEnd.bind(this); - this.onScroll = this.onScroll.bind(this); - }; - - render (): React.ReactNode { - const { parent, block, isPreview, subId } = this.props; - const { total } = S.Record.getMeta(subId, ''); - const items = this.getItems(); - const length = items.length; - const isCompact = this.isCompact(); - const cn = [ 'body' ]; - - if (!this.cache) { - return null; - }; - - if (isCompact) { - cn.push('isCompact'); - }; - - let content = null; - - if (isPreview) { - const rowRenderer = ({ index, key, parent, style }) => ( - <CellMeasurer - key={key} - parent={parent} - cache={this.cache} - columnIndex={0} - rowIndex={index} - fixedWidth - > - <WidgetListItem - {...this.props} - {...items[index]} - subId={subId} - id={items[index].id} - style={style} - index={index} - isCompact={isCompact} - /> - </CellMeasurer> - ); - - const List = SortableContainer(() => ( - <div className="items"> - <InfiniteLoader - rowCount={total} - loadMoreRows={() => {}} - isRowLoaded={() => true} - threshold={LIMIT} - > - {({ onRowsRendered }) => ( - <AutoSizer className="scrollArea"> - {({ width, height }) => ( - <VList - ref={ref => this.refList = ref} - width={width} - height={height} - deferredMeasurmentCache={this.cache} - rowCount={length} - rowHeight={({ index }) => this.getRowHeight(items[index], index, isCompact)} - rowRenderer={rowRenderer} - onRowsRendered={onRowsRendered} - overscanRowCount={LIMIT} - scrollToAlignment="center" - onScroll={this.onScroll} - /> - )} - </AutoSizer> - )} - </InfiniteLoader> - </div> - )); - - content = ( - <List - axis="y" - lockAxis="y" - lockToContainerEdges={true} - transitionDuration={150} - distance={10} - onSortStart={this.onSortStart} - onSortEnd={this.onSortEnd} - useDragHandle={true} - helperClass="isDragging" - helperContainer={() => $(`#widget-${parent.id} .items`).get(0)} - /> - ); - } else { - content = ( - <React.Fragment> - {items.map((item: any) => ( - <WidgetListItem - key={`widget-${block.id}-${item.id}`} - {...this.props} - {...item} - subId={subId} - id={item.id} - isCompact={isCompact} - /> - ))} - </React.Fragment> - ); - }; - - return ( - <div ref={ref => this.node = ref} className={cn.join(' ')}> - {content} - </div> - ); - }; - - componentDidMount (): void { - this.initCache(); - this.forceUpdate(); - }; - - componentDidUpdate (): void { - if (this.refList && this.top) { - this.refList.scrollToPosition(this.top); - }; - - this.initCache(); - this.resize(); - }; - - initCache () { - if (this.cache) { - return; - }; + const initCache = () => { + const items = getItems(); - const items = this.getItems(); - const isCompact = this.isCompact(); - - this.cache = new CellMeasurerCache({ + cache.current = new CellMeasurerCache({ fixedWidth: true, - defaultHeight: i => this.getRowHeight(items[i], i, isCompact), + defaultHeight: i => getRowHeight(items[i], i, isCompact), keyMapper: i => items[i], }); }; - onSortStart () { + const onSortStart = () => { keyboard.disableSelection(true); }; - onSortEnd (result: any) { + const onSortEnd = (result: any) => { const { oldIndex, newIndex } = result; - const { block, getRecordIds } = this.props; const { targetBlockId } = block.content; keyboard.disableSelection(false); @@ -195,8 +61,7 @@ const WidgetViewList = observer(class WidgetViewList extends React.Component<I.W Action.move(root, root, target.id, [ current.id ], position); }; - getItems () { - const { block, addGroupLabels, isPreview, getRecordIds, subId } = this.props; + const getItems = () => { const { targetBlockId } = block.content; const isRecent = [ J.Constant.widgetId.recentOpen, J.Constant.widgetId.recentEdit ].includes(targetBlockId); @@ -210,19 +75,17 @@ const WidgetViewList = observer(class WidgetViewList extends React.Component<I.W return items; }; - resize () { - const { parent, isPreview } = this.props; - const length = this.getItems().length; + const resize = () => { + const length = getItems().length; raf(() => { const container = $('#sidebar #containerWidget #list'); const obj = $(`#widget-${parent.id}`); - const node = $(this.node); + const node = $(nodeRef.current); const head = obj.find('.head'); const viewSelect = obj.find('#viewSelect'); - const offset = isPreview ? 12 : 0; - let height = this.getTotalHeight() + offset; + let height = getTotalHeight() + (isPreview ? 26 : 0); if (isPreview) { let maxHeight = container.height() - head.outerHeight(true); @@ -245,37 +108,131 @@ const WidgetViewList = observer(class WidgetViewList extends React.Component<I.W }); }; - getTotalHeight () { - const items = this.getItems(); - const isCompact = this.isCompact(); - - let height = 0; - - items.forEach((item, index) => { - height += this.getRowHeight(item, index, isCompact); - }); - - return height; + const getTotalHeight = () => { + return getItems().reduce((r, c) => r + getRowHeight(c, 0, isCompact), 0); }; - getRowHeight (item: any, index: number, isCompact: boolean) { + const getRowHeight = (item: any, index: number, isCompact: boolean) => { if (item && item.isSection) { return index ? HEIGHT_COMPACT + 12 : HEIGHT_COMPACT; }; return isCompact ? HEIGHT_COMPACT : HEIGHT_LIST; }; - onScroll ({ scrollTop }) { + const onScroll = ({ scrollTop }) => { if (scrollTop) { - this.top = scrollTop; + top.current = scrollTop; }; }; - isCompact () { - const { parent } = this.props; - return [ I.WidgetLayout.Compact, I.WidgetLayout.View ].includes(parent.content.layout); + const items = getItems(); + const length = items.length; + const cn = [ 'body' ]; + + if (isCompact) { + cn.push('isCompact'); + }; + + let content = null; + + if (isPreview) { + const rowRenderer = ({ index, key, parent, style }) => ( + <CellMeasurer + key={key} + parent={parent} + cache={cache.current} + columnIndex={0} + rowIndex={index} + fixedWidth + > + <WidgetListItem + {...props} + {...items[index]} + subId={subId} + id={items[index].id} + style={style} + index={index} + isCompact={isCompact} + /> + </CellMeasurer> + ); + + const List = SortableContainer(() => ( + <div className="items"> + <InfiniteLoader + rowCount={total} + loadMoreRows={() => {}} + isRowLoaded={() => true} + threshold={LIMIT} + > + {({ onRowsRendered }) => ( + <AutoSizer className="scrollArea"> + {({ width, height }) => ( + <VList + ref={listRef} + width={width} + height={height} + deferredMeasurmentCache={cache.current} + rowCount={length} + rowHeight={({ index }) => getRowHeight(items[index], index, isCompact)} + rowRenderer={rowRenderer} + onRowsRendered={onRowsRendered} + overscanRowCount={LIMIT} + scrollToAlignment="center" + onScroll={onScroll} + /> + )} + </AutoSizer> + )} + </InfiniteLoader> + </div> + )); + + content = ( + <List + axis="y" + lockAxis="y" + lockToContainerEdges={true} + transitionDuration={150} + distance={10} + onSortStart={onSortStart} + onSortEnd={onSortEnd} + useDragHandle={true} + helperClass="isDragging" + helperContainer={() => $(`#widget-${parent.id} .items`).get(0)} + /> + ); + } else { + content = ( + <React.Fragment> + {items.map((item: any) => ( + <WidgetListItem + key={`widget-${block.id}-${item.id}`} + {...props} + {...item} + subId={subId} + id={item.id} + isCompact={isCompact} + /> + ))} + </React.Fragment> + ); }; -}); + useEffect(() => { + listRef.current?.scrollToPosition(top.current); + + initCache(); + resize(); + }); + + + return ( + <div ref={nodeRef} className={cn.join(' ')}> + {content} + </div> + ); + +})); -export default WidgetViewList; \ No newline at end of file +export default WidgetViewList; diff --git a/src/ts/component/widget/view/list/item.tsx b/src/ts/component/widget/view/list/item.tsx index c8b53e0bb7..ff912c4df5 100644 --- a/src/ts/component/widget/view/list/item.tsx +++ b/src/ts/component/widget/view/list/item.tsx @@ -1,5 +1,4 @@ -import * as React from 'react'; -import raf from 'raf'; +import React, { forwardRef, useEffect, useRef } from 'react'; import $ from 'jquery'; import { observer } from 'mobx-react'; import { ObjectName, Icon, IconObject, ObjectDescription, DropTarget, Label } from 'Component'; @@ -17,178 +16,156 @@ interface Props extends I.WidgetViewComponent { isSection?: boolean; }; -const WidgetListItem = observer(class WidgetListItem extends React.Component<Props> { - - node = null; - frame = 0; - - constructor (props: Props) { - super(props); - - this.onClick = this.onClick.bind(this); - this.onContext = this.onContext.bind(this); +const WidgetListItem = observer(forwardRef<{}, Props>((props, ref) => { + + const { subId, id, block, style, isCompact, isEditing, index, isPreview, isSection, onContext } = props; + const rootId = keyboard.getRootId(); + const object = S.Detail.get(subId, id, J.Relation.sidebar); + const { isReadonly, isArchived, restrictions, source } = object; + const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); + const iconKey = `widget-icon-${block.id}-${id}`; + const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); + const canDrag = isPreview && (block.getTargetObjectId() == J.Constant.widgetId.favorite); + const hasMore = U.Space.canMyParticipantWrite(); + const nodeRef = useRef(null); + const cn = [ 'item' ]; + + if (canDrag) { + cn.push('canDrag'); }; - render () { - const { subId, id, block, style, isCompact, isEditing, index, isPreview, isSection } = this.props; - const rootId = keyboard.getRootId(); - const object = S.Detail.get(subId, id, J.Relation.sidebar); - const { isReadonly, isArchived, restrictions, source } = object; - const allowedDetails = S.Block.isAllowed(restrictions, [ I.RestrictionObject.Details ]); - const iconKey = `widget-icon-${block.id}-${id}`; - const canDrop = !isEditing && S.Block.isAllowed(restrictions, [ I.RestrictionObject.Block ]); - const canDrag = isPreview && (block.getTargetObjectId() == J.Constant.widgetId.favorite); - const hasMore = U.Space.canMyParticipantWrite(); - - if (isSection) { - return ( - <div - ref={node => this.node = node} - style={style} - className={[ 'item', 'isSection' ].join(' ')} - > - <div className="inner"> - <Label text={translate(U.Common.toCamelCase([ 'common', id ].join('-')))} /> - </div> - </div> - ); + const onClick = (e: React.MouseEvent) => { + if (e.button) { + return; }; - const Handle = SortableHandle(() => ( - <Icon className="dnd" /> - )); + e.preventDefault(); + e.stopPropagation(); - let descr = null; - let more = null; + U.Object.openEvent(e, object); + analytics.event('OpenSidebarObject'); + }; - if (!isCompact) { - if (U.Object.isBookmarkLayout(object.layout)) { - descr = <div className="descr">{U.Common.shortUrl(source)}</div>; - } else { - descr = <ObjectDescription object={object} />; - }; - }; + const onContextHandler = (e: React.SyntheticEvent, withElement: boolean) => { + e.preventDefault(); + e.stopPropagation(); - if (hasMore) { - more = <Icon className="more" tooltip={translate('widgetOptions')} onMouseDown={e => this.onContext(e, true)} />; - }; - - let inner = ( - <div className="inner" onMouseDown={this.onClick}> - <IconObject - id={iconKey} - key={iconKey} - object={object} - size={isCompact ? 18 : 48} - iconSize={isCompact ? 18 : 28} - canEdit={!isReadonly && !isArchived && allowedDetails && U.Object.isTaskLayout(object.layout)} - menuParam={{ - className: 'fixed', - classNameWrap: 'fromSidebar', - }} - /> - - <div className="info"> - <ObjectName object={object} /> - {descr} - </div> + const node = $(nodeRef.current); + const element = node.find('.icon.more'); - <div className="buttons"> - {more} - </div> - </div> - ); + onContext({ node, element, withElement, subId, objectId: id }); + }; - if (canDrag) { - inner = ( - <React.Fragment> - <Handle /> - {inner} - </React.Fragment> - ); - }; + const resize = () => { + const node = $(nodeRef.current); - if (canDrop) { - inner = ( - <DropTarget - cacheKey={[ block.id, object.id ].join('-')} - id={object.id} - rootId={rootId} - targetContextId={object.id} - dropType={I.DropType.Menu} - canDropMiddle={true} - > - {inner} - </DropTarget> - ); - }; + node.toggleClass('withIcon', !!node.find('.iconObject').length); + }; + + useEffect(() => resize()); - const content = ( + if (isSection) { + return ( <div - ref={node => this.node = node} - className={[ 'item', (canDrag ? 'canDrag' : '') ].join(' ')} - key={object.id} - onContextMenu={e => this.onContext(e, false)} + ref={nodeRef} style={style} + className={[ 'item', 'isSection' ].join(' ')} > - {inner} + <div className="inner"> + <Label text={translate(U.Common.toCamelCase([ 'common', id ].join('-')))} /> + </div> </div> ); - - if (canDrag) { - const Element = SortableElement(() => content); - return <Element index={index} />; - } else { - return content; - }; }; - componentDidMount (): void { - this.resize(); - }; + const Handle = SortableHandle(() => ( + <Icon className="dnd" /> + )); - componentDidUpdate (): void { - this.resize(); - }; + let descr = null; + let more = null; - onClick (e: React.MouseEvent) { - if (e.button) { - return; + if (!isCompact) { + if (U.Object.isBookmarkLayout(object.layout)) { + descr = <div className="descr">{U.Common.shortUrl(source)}</div>; + } else { + descr = <ObjectDescription object={object} />; }; - - e.preventDefault(); - e.stopPropagation(); - - const { subId, id, } = this.props; - const object = S.Detail.get(subId, id, J.Relation.sidebar); - - U.Object.openEvent(e, object); - analytics.event('OpenSidebarObject'); }; - onContext (e: React.SyntheticEvent, withElement: boolean) { - e.preventDefault(); - e.stopPropagation(); + if (hasMore) { + more = <Icon className="more" tooltip={translate('widgetOptions')} onMouseDown={e => onContextHandler(e, true)} />; + }; + + let inner = ( + <div className="inner" onMouseDown={onClick}> + <IconObject + id={iconKey} + key={iconKey} + object={object} + size={isCompact ? 18 : 48} + iconSize={isCompact ? 18 : 28} + canEdit={!isReadonly && !isArchived && allowedDetails && U.Object.isTaskLayout(object.layout)} + menuParam={{ + className: 'fixed', + classNameWrap: 'fromSidebar', + }} + /> + + <div className="info"> + <ObjectName object={object} /> + {descr} + </div> - const { subId, id, onContext } = this.props; - const node = $(this.node); - const element = node.find('.icon.more'); + <div className="buttons"> + {more} + </div> + </div> + ); - onContext({ node, element, withElement, subId, objectId: id }); + if (canDrag) { + inner = ( + <React.Fragment> + <Handle /> + {inner} + </React.Fragment> + ); }; - resize () { - if (this.frame) { - raf.cancel(this.frame); - }; - - this.frame = raf(() => { - const node = $(this.node); + if (canDrop) { + inner = ( + <DropTarget + cacheKey={[ block.id, object.id ].join('-')} + id={object.id} + rootId={rootId} + targetContextId={object.id} + dropType={I.DropType.Menu} + canDropMiddle={true} + > + {inner} + </DropTarget> + ); + }; - node.toggleClass('withIcon', !!node.find('.iconObject').length); - }); + const content = ( + <div + ref={nodeRef} + className={cn.join(' ')} + key={object.id} + onContextMenu={e => onContextHandler(e, false)} + style={style} + > + {inner} + </div> + ); + + if (canDrag) { + const Element = SortableElement(() => content); + return <Element index={index} />; + } else { + return content; }; -}); +})); -export default WidgetListItem; +export default WidgetListItem; \ No newline at end of file diff --git a/src/ts/docs/help/onboarding.ts b/src/ts/docs/help/onboarding.ts index c3b8a344c6..731c76ae13 100644 --- a/src/ts/docs/help/onboarding.ts +++ b/src/ts/docs/help/onboarding.ts @@ -101,10 +101,14 @@ export default { '#widget-buttons', '.widget', '#containerWidget #list .buttons', + '#containerWidget #body', + '.shareBanner', ], + /* onClose: () => { Onboarding.start('emailCollection', false); }, + */ }, items: [ { @@ -195,7 +199,7 @@ export default { vertical: I.MenuDirection.Bottom, horizontal: I.MenuDirection.Right, stickToElementEdge: I.MenuDirection.None, - highlightElements: [ '#menuSyncStatus', '#button-header-sync' ], + highlightElements: [ '#menuSyncStatus', '#sidebarSync' ], offsetY: 14, } }, diff --git a/src/ts/docs/help/whatsNew.ts b/src/ts/docs/help/whatsNew.ts index 3d980dec91..3adfe07179 100644 --- a/src/ts/docs/help/whatsNew.ts +++ b/src/ts/docs/help/whatsNew.ts @@ -14,11 +14,73 @@ const bullet = (t: string) => block(I.TextStyle.Bulleted, t); const caption = (t: string) => block(I.TextStyle.Paragraph, `<i>${t}</i>`, I.BlockHAlign.Center); const div = () => ({ type: I.BlockType.Div, style: I.DivStyle.Dot }); const video = (src: string, c?: string) => text(`<video src="${J.Url.cdn}/img/help/${src}" loop autoplay class="full ${c || ''}" />`); -const img = (src: string, c?: string) => text(`<img src="${J.Url.cdn}/img/help/${src}" class="c70 ${c || ''}" />`); +const img = (src: string, c?: string) => text(`<img src="${J.Url.cdn}/img/help/${src}" class="full ${c || ''}" />`); const link = (url: string, t: string) => `<a href="${url}">${t}</a>`; export default [ - { type: I.BlockType.IconPage, icon: '👋' }, + //{ type: I.BlockType.IconPage, icon: '👋' }, + { type: I.BlockType.IconPage, icon: '🎄' }, + + title(`Desktop 0.44.0 Released!`), + text(`Before we say goodbye to the year, we're happy to share this final update packed with some nice improvements. In addition to bug fixes and reliability/performance enhancements, we would like to introduce two long-anticipated features. We hope you enjoy this update and wish you a joyful holiday season!`), + text(``), + + h2(`Highlights`), + + h3(`Date as an Object`), + text(`You can now open any date as a separate object to view the entire context related to that date, including mentions (@date), automatically created dates and custom date relations. Dates are accessible from relations, layouts, graph, calendar view and more.`), + text(`In addition, there are new Date and Time settings, and @date supports relative date formats such as @today or @tomorrow.`), + img(`44/1.png`), + text(``), + + h3(`Simple Formulas`), + text(`The long-requested functionality is now available for Sets and Collections. You can count objects in the Grid view and perform simple math and aggregation functions with all types of relations.`), + img(`44/2.png`), + text(``), + + h2(`Quality-of-Life`), + bullet(`Files and markups from the HTML and bookmarks are no longer created as objects when you place them in the app. This enhances the functionality of the Web Clipper and clipboard, resulting in a cleaner app without unnecessary file clutter.`), + bullet(`The Entry space can now be deleted if it’s no longer needed or if you prefer to use only shared spaces. You can export and import it into any other spaces (better to use the any-block format).`), + bullet(`Deeplinks to Objects include an invitation to the shared space when using the ${hl('Copy Link')} option, making collaboration and access more seamless.`), + bullet(`Added the ability to import data during the creation of a second space, simplifying the setup process.`), + bullet(`The Audio Player has been slightly restyled, with a background with a subtle shadow, slightly repositioned controls and updated icons.`), + bullet(`Some colour updates for both light and dark modes.`), + bullet(`The ${hl('Add Relation')} option has been removed from the File layout.`), + bullet(`Added an option to enable or disable custom CSS. Thanks, @${link('https://community.anytype.io/t/custom-css-in-anytype-directly/24498', 'Shampra')}!`), + text(``), + + h2(`Bug Fixes`), + bullet(`Resolved an issue where the Link option in the widget settings menu could not be selected.`), + bullet(`Fixed an issue in global search where the cursor would jump from its position when entering or deleting text, particularly after switching apps. Thanks, @${link('https://community.anytype.io/t/cursor-jumping-in-global-search/25012', 'dzlg')}!`), + bullet(`Re-fixed an issue where local links with "$" in their path would cause the super link text to appear in some source text.`), + bullet(`Search queries retain entered symbols, resolving an issue with missing characters.`), + bullet(`Search results remain consistent when reopening the search panel.`), + bullet(`Mermaid diagrams remain visible simultaneously. Thanks, @${link('https://community.anytype.io/t/only-one-mermaid-diagram-at-the-time-0-43-21-beta/25619', 'langtind')}!`), + bullet(`The checkbox ticks as expected when attempting to delete a Vault.`), + bullet(`The Brazilian currency symbols (R$) are displayed correctly. Thanks, @${link('https://community.anytype.io/t/bug-when-inserting-reference-to-values-in-brl-reais/24380', 'perereco')}!`), + bullet(`Inline LaTeX with multiple standalone backslash commands now renders correctly. Thanks, @${link('https://community.anytype.io/t/inline-latex-for-multiple-standalone-backslash-commands-not-rendering/25537', 'zewwo')}!`), + bullet(`Applying formatting via keyboard shortcuts affects all selected text across multiple levels of indentation. Thanks, @${link('https://community.anytype.io/t/formatting-shortcuts-only-apply-to-the-top-indent-level/24745', 'ferdzso')}!`), + bullet(`The ${hl('Local Only')} tooltip in Network Mode is now clearly visible when the operating system is set to dark mode. Thanks, @${link('https://community.anytype.io/t/tooltip-local-only-in-network-mode-barely-visible-when-the-os-is-in-dark-mode/25317', 'krst')}!`), + bullet(`${hl(`${cmd} + A`)} in an overlay now selects only the content within the overlay, without affecting the object underneath. Thanks, @${link('https://community.anytype.io/t/ctrl-cmd-a-in-an-overlay-also-selects-in-the-object-underneath/23642', 'krst')}!`), + bullet(`${hl('Click')} handlers in Mermaid embed blocks open external links in a browser. Unfortunately, internal resource links within Anytype can't be fixed as they're purified by Mermaid. Thanks, @${link('https://community.anytype.io/t/mermaid-embed-block-mermaid-click-not-working-as-expected/24948', 'francodgstn')}!`), + bullet(`Converting multiple blocks using shortcuts works correctly. Thanks, @${link('https://community.anytype.io/t/converting-multiple-blocks-with-shortcuts-not-working-anymore/25523', 'siousu')}!`), + bullet(`Fixed an issue where element outlines would become visible. Thanks, @${link('https://community.anytype.io/t/outlines-visible-when-zooming-in-and-out/25500', 'zma17')}!`), + bullet(`The right-click menu is correctly updated after an object or type is removed from the All Objects. Thanks, @${link('https://community.anytype.io/t/after-deleting-one-type-under-all-objects-the-right-click-menu-does-not-refresh-its-content-correctly/25295', 'Facility6384')} and @${link('https://community.anytype.io/t/after-deleting-one-type-under-all-objects-the-right-click-menu-does-not-refresh-its-content-correctly/25295', 'endlessblink')}!`), + bullet(`Frequently clicking on the ${hl('Get My Key')} button does not result in an error.`), + bullet(`Long space names no longer overlap with the sidebar button.`), + bullet(`Links starting with tel: are not prepended with https:// anymore. Thanks, @${link('https://community.anytype.io/t/tel-urls-work-incorrectly-on-desktop/25178', 'fieldnote')}!`), + bullet(`Formatting text as monospace using backticks ${hl('\`\`')} is applied without shifting the formatting of subsequent words in a paragraph. Thanks, @${link('https://community.anytype.io/t/monospace-formatting-with-backticks-shifts-following-formatting-left/24584', 'yuritem')}!`), + bullet(`Reordering views in Sets and Collections is consistent and the drop-down list remains open after reordering for additional adjustments.`), + bullet(`Resolved a crash caused by an ENOENT error.`), + bullet(`The search icon in Sets and Collections displays correctly.`), + bullet(`Fixed an issue where an Object name might not be saved when entering it for a new Object in a Set.`), + bullet(`Fixed unexpected behaviour of the context menu when it could follow the mouse cursor and lose the context of the element it refers to. Thanks, @${link('https://community.anytype.io/t/actions-popup-follows-mouse-and-loses-context-to-element/25705', 'krst')}!`), + text(``), + + callout(`Please be reminded that we no longer support macOS Catalina.`, '⚠️'), + + div(), + // --------------------------------------------// h1(`Desktop 0.43.7 Released!`), callout(`This follow-up update to the previous release brings improved performance and a few additional enhancements.`, '📃'), @@ -37,7 +99,7 @@ export default [ div(), // --------------------------------------------// - title(`Desktop 0.43.0 Released!`), + h1(`Desktop 0.43.0 Released!`), callout(`A big thank you to our amazing Community for the valuable suggestions and reports that continue to help us along the way!`, '💌'), h2(`Highlights on this release`), diff --git a/src/ts/hook/useElementMovement.ts b/src/ts/hook/useElementMovement.ts index 649c32a909..3e5f0632f0 100644 --- a/src/ts/hook/useElementMovement.ts +++ b/src/ts/hook/useElementMovement.ts @@ -11,13 +11,11 @@ class ElementMovementObserver { private movementObserver: MutationObserver; private resizeObserver: ResizeObserver; - private element: HTMLElement; private lastPosition: Position; - private onMove: (position: Position) => void; - constructor(element: HTMLElement, callback: (position: Position) => void) { + constructor (element: HTMLElement, callback: (position: Position) => void) { this.element = element; this.onMove = callback; this.lastPosition = this.getPosition(); @@ -86,18 +84,19 @@ class ElementMovementObserver { public disconnect (): void { this.movementObserver.disconnect(); this.resizeObserver.disconnect(); + window.removeEventListener('scroll', this.checkForMovement); }; }; -export default function useElementMovement (element: HTMLElement | null, callback: (position: Position) => void ) { +export default function useElementMovement (element: HTMLElement | null, callBack: (position: Position) => void ) { useEffect(() => { if (!element) { return; }; - const movementObserver = new ElementMovementObserver(element, callback); + const movementObserver = new ElementMovementObserver(element, callBack); return () => movementObserver.disconnect(); - }, [ element, callback ]); + }, [ element, callBack ]); }; \ No newline at end of file diff --git a/src/ts/interface/block/chat.ts b/src/ts/interface/block/chat.ts index cbb52ec358..2cced710b5 100644 --- a/src/ts/interface/block/chat.ts +++ b/src/ts/interface/block/chat.ts @@ -43,9 +43,9 @@ export interface ChatMessageAttachment { export interface ChatMessageComponent extends I.BlockComponent { blockId: string; id: string; - isThread: boolean; isNew: boolean; - onThread: (id: string) => void; + subId: string + scrollToBottom?: () => void; onContextMenu: (e: any) => void; onMore: (e: any) => void; onReplyEdit: (e: any) => void; diff --git a/src/ts/interface/block/dataview.ts b/src/ts/interface/block/dataview.ts index e3b57044db..b72fcedcb8 100644 --- a/src/ts/interface/block/dataview.ts +++ b/src/ts/interface/block/dataview.ts @@ -14,7 +14,8 @@ export enum DateFormat { ISO = 4, // 2020-07-30 Long = 5, // July 15, 2020 Nordic = 6, // 15. Jul 2020 - European = 7, // 15.07.2020 + European = 7, // 15.07.2020, + Default = 8, // Sat, Dec 14, 2024 }; export enum TimeFormat { @@ -87,17 +88,18 @@ export enum FilterQuickOption { export enum FormulaType { None = 0, Count = 1, - CountDistinct = 2, - CountEmpty = 3, - CountNotEmpty = 4, - PercentEmpty = 5, - PercentNotEmpty = 6, - MathSum = 7, - MathAverage = 8, - MathMedian = 9, - MathMin = 10, - MathMax = 11, - Range = 12, + CountValue = 2, + CountDistinct = 3, + CountEmpty = 4, + CountNotEmpty = 5, + PercentEmpty = 6, + PercentNotEmpty = 7, + MathSum = 8, + MathAverage = 9, + MathMedian = 10, + MathMin = 11, + MathMax = 12, + Range = 13, }; export enum FormulaSection { diff --git a/src/ts/interface/block/index.ts b/src/ts/interface/block/index.ts index c3eaa3c584..afcba60611 100644 --- a/src/ts/interface/block/index.ts +++ b/src/ts/interface/block/index.ts @@ -134,6 +134,7 @@ export interface Block { canTurnObject?(): boolean; canCreateBlock?(): boolean; canBecomeWidget?(): boolean; + canContextMenu?(): boolean; isIndentable?(): boolean; isFocusable?(): boolean; @@ -147,6 +148,7 @@ export interface Block { isRelation?(): boolean; isType?(): boolean; isChat?(): boolean; + isCover?(): boolean; isWidget?(): boolean; isWidgetLink?(): boolean; @@ -159,7 +161,6 @@ export interface Block { isLayoutColumn?(): boolean; isLayoutDiv?(): boolean; isLayoutHeader?(): boolean; - isLayoutFooter?(): boolean; isLayoutTableRows?(): boolean; isLayoutTableColumns?(): boolean; diff --git a/src/ts/interface/block/layout.ts b/src/ts/interface/block/layout.ts index a6e8d6cee8..a64336f206 100644 --- a/src/ts/interface/block/layout.ts +++ b/src/ts/interface/block/layout.ts @@ -1,14 +1,12 @@ import { I } from 'Lib'; export enum LayoutStyle { - Row = 0, - Column = 1, - Div = 2, - Header = 3, - TableRows = 4, - TableColumns = 5, - - Footer = 100, + Row = 0, + Column = 1, + Div = 2, + Header = 3, + TableRows = 4, + TableColumns = 5, }; export interface ContentLayout { diff --git a/src/ts/interface/block/widget.ts b/src/ts/interface/block/widget.ts index 269971824d..ba8ccd551a 100644 --- a/src/ts/interface/block/widget.ts +++ b/src/ts/interface/block/widget.ts @@ -8,7 +8,6 @@ export enum WidgetLayout { View = 4, Space = 100, - Buttons = 101, }; export interface WidgetComponent { diff --git a/src/ts/interface/common.ts b/src/ts/interface/common.ts index 80d21a2be9..dc973f5654 100644 --- a/src/ts/interface/common.ts +++ b/src/ts/interface/common.ts @@ -51,6 +51,7 @@ export interface Toast { count?: number; value?: boolean; ids?: string[]; + icon?: string; }; export enum ToastAction { @@ -122,15 +123,12 @@ export enum EdgeType { export enum Usecase { None = 0, GetStarted = 1, - Personal = 2, - Knowledge = 3, - Notes = 4, - Strategic = 5, - Empty = 6, + Empty = 2, }; export enum HomePredefinedId { Graph = 'graph', + Chat = 'chat', Last = 'lastOpened', Existing = 'existing', }; @@ -244,13 +242,6 @@ export enum NetworkMode { Custom = 2, }; -export enum NavigationMenuMode { - None = 0, - Context = 1, - Click = 2, - Hover = 3, -}; - export enum InterfaceStatus { Ok = 'ok', Error = 'error', @@ -301,6 +292,7 @@ export interface SearchSubscribeParam { ignoreHidden: boolean; ignoreDeleted: boolean; ignoreArchived: boolean; + skipLayoutFormat: I.ObjectLayout[]; noDeps: boolean; }; @@ -318,4 +310,13 @@ export enum SortId { export enum LoaderType { Loader = 'loader', Dots = 'dots', -}; \ No newline at end of file +}; + +export interface Error { + code: number; + description: string; +}; + +export interface PageRef { + resize: () => void; +}; diff --git a/src/ts/interface/index.ts b/src/ts/interface/index.ts index 2c8330238e..d7332033e1 100644 --- a/src/ts/interface/index.ts +++ b/src/ts/interface/index.ts @@ -25,4 +25,4 @@ export * from './block/relation'; export * from './block/embed'; export * from './block/table'; export * from './block/widget'; -export * from './block/chat'; +export * from './block/chat'; \ No newline at end of file diff --git a/src/ts/interface/menu.ts b/src/ts/interface/menu.ts index ab3d4395e3..14351434e6 100644 --- a/src/ts/interface/menu.ts +++ b/src/ts/interface/menu.ts @@ -53,7 +53,6 @@ export interface MenuParam { export interface Menu { id: string; param: MenuParam; - history?: any; setActive?(item?: any, scroll?: boolean): void; setHover?(item?: any, scroll?: boolean): void; onKeyDown?(e: any): void; @@ -66,6 +65,16 @@ export interface Menu { close? (callBack?: () => void): void; }; +export interface MenuRef { + rebind: () => void, + unbind: () => void, + getItems: () => any[]; + getIndex: () => number, + setIndex: (i: number) => void, + onClick?: (e: any, item: any) => void, + onOver?: (e: any, item: any) => void, +}; + export interface MenuItem { id?: string; icon?: string; @@ -86,6 +95,9 @@ export interface MenuItem { options?: I.Option[]; selectMenuParam?: any; isActive?: boolean; + isDiv?: boolean; + isSection?: boolean; + index?: number; withDescription?: boolean; withSwitch?: boolean; withSelect?: boolean; diff --git a/src/ts/lib/action.ts b/src/ts/lib/action.ts index 794552f847..c99175de48 100644 --- a/src/ts/lib/action.ts +++ b/src/ts/lib/action.ts @@ -174,8 +174,10 @@ class Action { const storageKey = 'openUrl'; const scheme = U.Common.getScheme(url); const cb = () => Renderer.send('openUrl', url); + const allowedSchemes = J.Constant.allowedSchemes.concat(J.Constant.protocol); + const isAllowed = scheme.match(new RegExp(`^(${allowedSchemes.join('|')})$`)); - if (!Storage.get(storageKey) && !scheme.match(new RegExp(`^(${J.Constant.allowedSchemes.join('|')})$`))) { + if (!Storage.get(storageKey) && !isAllowed) { S.Popup.open('confirm', { data: { icon: 'confirm', @@ -477,6 +479,8 @@ class Action { }; restore (ids: string[], route: string, callBack?: () => void) { + ids = ids || []; + C.ObjectListSetIsArchived(ids, false, (message: any) => { if (message.error.code) { return; @@ -653,24 +657,16 @@ class Action { onConfirm: () => { analytics.event(`Click${suffix}SpaceWarning`, { type: suffix, route }); - const cb = () => { - C.SpaceDelete(id, (message: any) => { - if (callBack) { - callBack(message); - }; - - if (!message.error.code) { - Preview.toastShow({ text: toast }); - analytics.event(`${suffix}Space`, { type: deleted.spaceAccessType, route }); - }; - }); - }; + C.SpaceDelete(id, (message: any) => { + if (callBack) { + callBack(message); + }; - if (space == id) { - U.Space.openFirstSpaceOrVoid(it => it.targetSpaceId != id, { replace: true, onRouteChange: cb }); - } else { - cb(); - }; + if (!message.error.code) { + Preview.toastShow({ text: toast }); + analytics.event(`${suffix}Space`, { type: deleted.spaceAccessType, route }); + }; + }); }, onCancel: () => { analytics.event(`Click${suffix}SpaceWarning`, { type: 'Cancel', route }); @@ -787,13 +783,20 @@ class Action { text: translate('popupConfirmRevokeLinkText'), textConfirm: translate('popupConfirmRevokeLinkConfirm'), colorConfirm: 'red', + noCloseOnConfirm: true, onConfirm: () => { - C.SpaceInviteRevoke(spaceId, () => { + C.SpaceInviteRevoke(spaceId, (message: any) => { + if (message.error.code) { + S.Popup.updateData('confirm', { error: message.error.description }); + return; + }; + if (callBack) { callBack(); }; Preview.toastShow({ text: translate('toastInviteRevoke') }); + S.Popup.close('confirm'); analytics.event('RevokeShareLink'); }); }, @@ -822,6 +825,21 @@ class Action { analytics.event('ThemeSet', { id }); }; + publish (objectId: string, url: string, callBack?: (message: any) => void) { + if (!url) { + return; + }; + + C.PublishingCreate(S.Common.space, objectId, url, (message: any) => { + if (!message.error.code) { + this.openUrl(message.url); + }; + + if (callBack) { + callBack(message); + }; + }); + }; }; export default new Action(); diff --git a/src/ts/lib/analytics.ts b/src/ts/lib/analytics.ts index d485ffee1e..3425ad09bc 100644 --- a/src/ts/lib/analytics.ts +++ b/src/ts/lib/analytics.ts @@ -17,7 +17,6 @@ class Analytics { public route = { block: 'Block', - navigation: 'Navigation', onboarding: 'Onboarding', collection: 'Collection', set: 'Set', @@ -28,6 +27,8 @@ class Analytics { deleted: 'Deleted', banner: 'Banner', widget: 'Widget', + addWidget: 'AddWidget', + inWidget: 'InWidget', graph: 'Graph', store: 'Library', type: 'Type', @@ -155,7 +156,7 @@ class Analytics { ret.push(config.channel); }; - C.InitialSetParameters(platform, ret.join('-'), userPath(), '', true); + C.InitialSetParameters(platform, ret.join('-'), userPath(), '', false, false); }; profile (id: string, networkId: string) { @@ -188,6 +189,10 @@ class Analytics { }; setProperty (props: any) { + if (!this.instance || !this.isAllowed()) { + return; + }; + this.instance.setUserProperties(props); this.log(`[Analytics].setProperty: ${JSON.stringify(props, null, 3)}`); }; @@ -419,11 +424,6 @@ class Analytics { break; }; - case 'ChangeShowQuickCapture': { - data.type = I.NavigationMenuMode[data.type]; - break; - }; - case 'SelectUsecase': { data.type = Number(data.type) || 0; data.type = I.Usecase[data.type]; @@ -514,6 +514,16 @@ class Analytics { break; }; + case 'ChangeDateFormat': { + data.type = I.DateFormat[Number(data.type)]; + break; + }; + + case 'ChangeTimeFormat': { + data.type = I.TimeFormat[Number(data.type)]; + break; + }; + case 'ObjectListSort': { data.type = I.SortType[Number(data.type)]; break; @@ -585,6 +595,7 @@ class Analytics { 'main/media': 'ScreenMedia', 'main/history': 'ScreenHistory', 'main/date': 'ScreenDate', + 'main/archive': 'ScreenBin', }; return map[key] || ''; @@ -594,6 +605,7 @@ class Analytics { const { id } = params; const map = { inviteRequest: 'ScreenInviteRequest', + spaceCreate: 'ScreenSettingsSpaceCreate', }; return map[id] || ''; diff --git a/src/ts/lib/api/command.ts b/src/ts/lib/api/command.ts index 953c484b84..ee63c30e88 100644 --- a/src/ts/lib/api/command.ts +++ b/src/ts/lib/api/command.ts @@ -4,7 +4,7 @@ import { I, S, U, J, Mark, Storage, dispatcher, Encode, Mapper, keyboard } from const { Rpc, Empty } = Commands; -export const InitialSetParameters = (platform: I.Platform, version: string, workDir: string, logLevel: string, doNotSendLogs: boolean, callBack?: (message: any) => void) => { +export const InitialSetParameters = (platform: I.Platform, version: string, workDir: string, logLevel: string, doNotSendLogs: boolean, doNotSaveLogs: boolean, callBack?: (message: any) => void) => { const request = new Rpc.Initial.SetParameters.Request(); request.setPlatform(platform); @@ -12,6 +12,7 @@ export const InitialSetParameters = (platform: I.Platform, version: string, work request.setWorkdir(workDir); request.setLoglevel(logLevel); request.setDonotsendlogs(doNotSendLogs); + request.setDonotsavelogs(doNotSaveLogs); dispatcher.request(InitialSetParameters.name, request, callBack); }; @@ -194,6 +195,23 @@ export const AccountSelect = (id: string, path: string, mode: I.NetworkMode, net dispatcher.request(AccountSelect.name, request, callBack); }; +export const AccountMigrate = (id: string, path: string, callBack?: (message: any) => void) => { + const request = new Rpc.Account.Migrate.Request(); + + request.setId(id); + request.setRootpath(path); + + dispatcher.request(AccountMigrate.name, request, callBack); +}; + +export const AccountMigrateCancel = (id: string, callBack?: (message: any) => void) => { + const request = new Rpc.Account.MigrateCancel.Request(); + + request.setId(id); + + dispatcher.request(AccountMigrateCancel.name, request, callBack); +}; + export const AccountStop = (removeData: boolean, callBack?: (message: any) => void) => { const request = new Rpc.Account.Stop.Request(); @@ -1363,6 +1381,19 @@ export const ObjectShow = (objectId: string, traceId: string, spaceId: string, c }); }; +export const PublishingCreate = (spaceId: string, objectId: string, uri: string, callBack?: (message: any) => void) => { + /* + const request = new Rpc.Publishing.Create.Request(); + + request.setObjectid(objectId); + request.setSpaceid(spaceId); + request.setUri(uri); + + dispatcher.request(PublishingCreate.name, request, callBack); + */ +}; + + export const ObjectClose = (objectId: string, spaceId: string, callBack?: (message: any) => void) => { const request = new Rpc.Object.Close.Request(); @@ -1883,11 +1914,12 @@ export const UnsplashDownload = (spaceId: string, id: string, callBack?: (messag // ---------------------- DEBUG ---------------------- // -export const DebugTree = (objectId: string, path: string, callBack?: (message: any) => void) => { +export const DebugTree = (objectId: string, path: string, unanonymized: boolean, callBack?: (message: any) => void) => { const request = new Rpc.Debug.Tree.Request(); request.setTreeid(objectId); request.setPath(path); + request.setUnanonymized(unanonymized); dispatcher.request(DebugTree.name, request, callBack); }; @@ -1931,6 +1963,14 @@ export const DebugNetCheck = (config: string, callBack?: (message: any) => void) dispatcher.request(DebugNetCheck.name, request, callBack); }; +export const DebugExportLog = (path: string, callBack?: (message: any) => void) => { + const request = new Rpc.Debug.ExportLog.Request(); + + request.setDir(path); + + dispatcher.request(DebugExportLog.name, request, callBack); +} + // ---------------------- NOTIFICATION ---------------------- // export const NotificationList = (includeRead: boolean, limit: number, callBack?: (message: any) => void) => { @@ -2208,11 +2248,12 @@ export const ChatDeleteMessage = (objectId: string, messageId: string, callBack? }; -export const ChatGetMessages = (objectId: string, beforeOrderId: string, limit: number, callBack?: (message: any) => void) => { +export const ChatGetMessages = (objectId: string, beforeOrderId: string, afterOrderId: string, limit: number, callBack?: (message: any) => void) => { const request = new Rpc.Chat.GetMessages.Request(); request.setChatobjectid(objectId); request.setBeforeorderid(beforeOrderId); + request.setAfterorderid(afterOrderId); request.setLimit(limit); dispatcher.request(ChatGetMessages.name, request, callBack); diff --git a/src/ts/lib/api/dispatcher.ts b/src/ts/lib/api/dispatcher.ts index c21111834b..5e41834bfb 100644 --- a/src/ts/lib/api/dispatcher.ts +++ b/src/ts/lib/api/dispatcher.ts @@ -110,8 +110,8 @@ class Dispatcher { const rootId = ctx.join('-'); const messages = event.getMessagesList() || []; - const log = (rootId: string, type: string, data: any, valueCase: any) => { - console.log(`%cEvent.${type}`, 'font-weight: bold; color: #ad139b;', rootId); + const log = (rootId: string, type: string, spaceId: string, data: any, valueCase: any) => { + console.log(`%cEvent.${type}`, 'font-weight: bold; color: #ad139b;', rootId, spaceId); if (!type) { console.error('Event not found for valueCase', valueCase); }; @@ -130,10 +130,14 @@ class Dispatcher { for (const message of messages) { const type = Mapper.Event.Type(message.getValueCase()); - const data = Mapper.Event.Data(message); - const mapped = Mapper.Event[type] ? Mapper.Event[type](data) : {}; + const { spaceId, data } = Mapper.Event.Data(message); + const mapped = Mapper.Event[type] ? Mapper.Event[type](data) : null; const needLog = this.checkLog(type) && !skipDebug; + if (!mapped) { + continue; + }; + switch (type) { case 'AccountShow': { @@ -948,7 +952,18 @@ class Dispatcher { S.Chat.add(rootId, idx, message); if (isMainWindow && !electron.isFocused() && (message.creator != account.id)) { - U.Common.notification({ title: author?.name, text: message.content.text }); + U.Common.notification({ title: author?.name, text: message.content.text }, () => { + const { space } = S.Common; + const open = () => { + U.Object.openAuto({ id: S.Block.workspace, layout: I.ObjectLayout.Chat }); + }; + + if (spaceId != space) { + U.Router.switchSpace(spaceId, '', false, { onRouteChange: open }); + } else { + open(); + }; + }); }; $(window).trigger('messageAdd', [ message ]); @@ -957,6 +972,8 @@ class Dispatcher { case 'ChatUpdate': { S.Chat.update(rootId, mapped.message); + + $(window).trigger('messageUpdate', [ mapped.message ]); break; }; @@ -971,7 +988,7 @@ class Dispatcher { set(message, { reactions: mapped.reactions }); }; - $(window).trigger('updateReactions', [ message ]); + $(window).trigger('reactionUpdate', [ message ]); break; }; @@ -1018,7 +1035,7 @@ class Dispatcher { }; if (needLog) { - log(rootId, type, data, message.getValueCase()); + log(rootId, type, spaceId, data, message.getValueCase()); }; }; @@ -1043,16 +1060,21 @@ class Dispatcher { subIds = this.getUniqueSubIds(subIds); subIds.forEach(subId => S.Detail.update(subId, { id, details }, clear)); + const { space } = S.Common; const keys = Object.keys(details); const check = [ 'creator', 'spaceDashboardId', 'spaceAccountStatus' ]; const intersection = check.filter(k => keys.includes(k)); - if (intersection.length && subIds.length && subIds.includes(J.Constant.subId.space)) { - const object = S.Detail.get(J.Constant.subId.space, id, [ 'layout', 'targetSpaceId' ], true); + if (subIds.length && subIds.includes(J.Constant.subId.space)) { + const object = U.Space.getSpaceview(id); - if (U.Object.isSpaceViewLayout(object.layout) && object.targetSpaceId) { + if (intersection.length && object.targetSpaceId) { U.Data.createSubSpaceSubscriptions([ object.targetSpaceId ]); }; + + if (object.isAccountDeleted && (object.targetSpaceId == space)) { + U.Space.openFirstSpaceOrVoid(null, { replace: true }); + }; }; if (!rootId) { diff --git a/src/ts/lib/api/mapper.ts b/src/ts/lib/api/mapper.ts index 5d2730285a..d317d9f70c 100644 --- a/src/ts/lib/api/mapper.ts +++ b/src/ts/lib/api/mapper.ts @@ -1163,8 +1163,12 @@ export const Mapper = { Data (e: any) { const type = Mapper.Event.Type(e.getValueCase()); const fn = `get${U.Common.ucFirst(type)}`; + const data = e[fn] ? e[fn]() : {}; - return e[fn] ? e[fn]() : {}; + return { + spaceId: e.getSpaceid(), + data, + }; }, AccountShow: (obj: Events.Event.Account.Show) => { diff --git a/src/ts/lib/api/response.ts b/src/ts/lib/api/response.ts index deae5de905..17824e79fc 100644 --- a/src/ts/lib/api/response.ts +++ b/src/ts/lib/api/response.ts @@ -198,6 +198,14 @@ export const ObjectShow = (response: Rpc.Object.Show.Response) => { }; }; +/* +export const PublishingCreate = (response: Rpc.Publishing.Create.Response) => { + return { + url: response.getUri(), + }; +}; +*/ + export const ObjectSearch = (response: Rpc.Object.Search.Response) => { return { records: (response.getRecordsList() || []).map(Decode.struct), @@ -422,10 +430,11 @@ export const HistoryDiffVersions = (response: Rpc.History.DiffVersions.Response) return { events: (response.getHistoryeventsList() || []).map(it => { const type = Mapper.Event.Type(it.getValueCase()); - const data = Mapper.Event[type](Mapper.Event.Data(it)); + const { spaceId, data } = Mapper.Event.Data(it); + const mapped = Mapper.Event[type] ? Mapper.Event[type](data) : null; - return { type, data }; - }), + return mapped ? { spaceId, type, data: mapped } : null; + }).filter(it => it), }; }; diff --git a/src/ts/lib/dataview.ts b/src/ts/lib/dataview.ts index a98833a557..5c7d788865 100644 --- a/src/ts/lib/dataview.ts +++ b/src/ts/lib/dataview.ts @@ -459,22 +459,23 @@ class Dataview { const relations = Relation.getSetOfObjects(rootId, objectId, I.ObjectLayout.Relation); const isAllowedDefaultType = this.isCollection(rootId, blockId) || !!relations.length; - if (view && view.defaultTypeId && isAllowedDefaultType) { - return view.defaultTypeId; - }; - let typeId = ''; + if (view && view.defaultTypeId && isAllowedDefaultType) { + typeId = view.defaultTypeId; + } else if (types.length) { typeId = types[0].id; } else if (relations.length) { for (const item of relations) { - if (!item.objectTypes.length) { + const objectTypes = Relation.getArrayValue(item.objectTypes); + + if (!objectTypes.length) { continue; }; - const first = S.Record.getTypeById(item.objectTypes[0]); + const first = S.Record.getTypeById(objectTypes[0]); if (first && !U.Object.isInFileOrSystemLayouts(first.recommendedLayout)) { typeId = first.id; break; @@ -482,7 +483,13 @@ class Dataview { }; }; - return typeId || S.Common.type; + const type = S.Record.getTypeById(typeId); + + if (!type) { + typeId = S.Common.type; + }; + + return typeId; }; getCreateTooltip (rootId: string, blockId: string, objectId: string, viewId: string): string { @@ -551,6 +558,7 @@ class Dataview { return null; }; + const { showRelativeDates } = S.Common; const { formulaType, includeTime, relationKey } = viewRelation; const relation = S.Record.getRelationByKey(relationKey); @@ -561,29 +569,39 @@ class Dataview { const { total } = S.Record.getMeta(subId, ''); const isDate = relation.format == I.RelationType.Date; - let records = []; - let needRecords = false; - - if (![ I.FormulaType.None, I.FormulaType.Count ].includes(formulaType)) { - needRecords = true; - }; - - if (needRecords) { - records = S.Record.getRecords(subId, [ relationKey ], true); - }; + const isArray = Relation.isArrayType(relation.format); + const needRecords = ![ I.FormulaType.None, I.FormulaType.Count ].includes(formulaType); + const records = needRecords ? S.Record.getRecords(subId, [ relationKey ], true) : []; const date = (t: number) => { - const date = U.Date.dateWithFormat(S.Common.dateFormat, t); + const day = showRelativeDates ? U.Date.dayString(t) : null; + const date = day ? day : U.Date.dateWithFormat(S.Common.dateFormat, t); const time = U.Date.timeWithFormat(S.Common.timeFormat, t); return includeTime ? [ date, time ].join(' ') : date; }; const min = () => { - return Math.min(...records.map(it => Number(it[relationKey] || 0))); + const map = records.map(it => it[relationKey]).filter(it => !Relation.isEmpty(it)); + return map.length ? Math.min(...map.map(it => Number(it || 0))) : null; }; const max = () => { - return Math.max(...records.map(it => Number(it[relationKey] || 0))); + const map = records.map(it => it[relationKey]).filter(it => !Relation.isEmpty(it)); + return map.length ? Math.max(...map.map(it => Number(it || 0))) : null; + }; + const float = (v: any): string => { + return (v === null) ? null : U.Common.formatNumber(U.Common.sprintf('%0.3f', v)).replace(/\.0+?$/, ''); + }; + const filtered = (filterEmpty: boolean) => { + return records.filter(it => { + let isEmpty = false; + if (relationKey == 'name') { + isEmpty = Relation.isEmpty(it[relationKey]) || (it[relationKey] == translate('defaultNamePage')); + } else { + isEmpty = relation.format == I.RelationType.Checkbox ? !it[relationKey] : Relation.isEmpty(it[relationKey]); + }; + return filterEmpty == isEmpty; + }); }; let ret = null; @@ -594,52 +612,85 @@ class Dataview { }; case I.FormulaType.Count: { - ret = total; + ret = float(total); + break; + }; + + case I.FormulaType.CountValue: { + const items = filtered(false); + + if (isArray || isDate) { + const values = new Set(); + + items.forEach(it => { + values.add(isDate ? + date(it[relationKey]) : + Relation.getArrayValue(it[relationKey]).sort().join(', ') + ); + }); + + ret = values.size; + } else { + ret = U.Common.arrayUniqueObjects(items, relationKey).length; + }; + ret = float(ret); break; }; case I.FormulaType.CountDistinct: { - ret = U.Common.arrayUniqueObjects(records, relationKey).length; + const items = filtered(false); + + if (isArray || isDate) { + const values = new Set(); + + items.forEach(it => { + if (isDate) { + values.add(date(it[relationKey])); + } else { + Relation.getArrayValue(it[relationKey]).forEach(v => values.add(v)); + }; + }); + + ret = values.size; + } else { + ret = U.Common.arrayUniqueObjects(items, relationKey).length; + }; + ret = float(ret); break; }; case I.FormulaType.CountEmpty: { - ret = records.filter(it => Relation.isEmpty(it[relationKey])).length; - ret = records.filter(it => { - return relation.format == I.RelationType.Checkbox ? !it[relationKey] : Relation.isEmpty(it[relationKey]); - }).length; + ret = float(filtered(true).length); break; }; case I.FormulaType.CountNotEmpty: { - ret = records.filter(it => { - return relation.format == I.RelationType.Checkbox ? !!it[relationKey] : !Relation.isEmpty(it[relationKey]); - }).length; + ret = float(filtered(false).length); break; }; case I.FormulaType.PercentEmpty: { - ret = U.Common.sprintf('%0.2f%', records.filter(it => Relation.isEmpty(it[relationKey])).length / total * 100); + ret = float(filtered(true).length / total * 100) + '%'; break; }; case I.FormulaType.PercentNotEmpty: { - ret = U.Common.sprintf('%0.2f%', records.filter(it => !Relation.isEmpty(it[relationKey])).length / total * 100); + ret = float(filtered(false).length / total * 100) + '%'; break; }; case I.FormulaType.MathSum: { - ret = records.reduce((acc, it) => acc + Number(it[relationKey] || 0), 0); + ret = float(records.reduce((acc, it) => acc + (Number(it[relationKey]) || 0), 0)); break; }; case I.FormulaType.MathAverage: { - ret = U.Common.sprintf('%0.4f%', records.reduce((acc, it) => acc + Number(it[relationKey] || 0), 0) / total); + ret = float(records.reduce((acc, it) => acc + (Number(it[relationKey]) || 0), 0) / total); break; }; case I.FormulaType.MathMedian: { - const data = records.map(it => Number(it[relationKey] || 0)); + const data = records.map(it => Number(it[relationKey]) || 0); const n = data.length; data.sort((a, b) => a - b); @@ -649,6 +700,8 @@ class Dataview { } else { ret = (data[n / 2 - 1] + data[n / 2]) / 2; }; + + ret = float(ret); break; }; @@ -656,6 +709,8 @@ class Dataview { ret = min(); if (isDate) { ret = ret ? date(ret) : ''; + } else { + ret = float(ret); }; break; }; @@ -664,6 +719,8 @@ class Dataview { ret = max(); if (isDate) { ret = ret ? date(ret) : ''; + } else { + ret = float(ret); }; break; }; @@ -672,7 +729,7 @@ class Dataview { if (isDate) { ret = U.Date.duration(max() - min()); } else { - ret = [ min(), max() ].join(' - '); + ret = float(max() - min()); }; break; }; diff --git a/src/ts/lib/keyboard.ts b/src/ts/lib/keyboard.ts index f96f71357e..9c17e833c7 100644 --- a/src/ts/lib/keyboard.ts +++ b/src/ts/lib/keyboard.ts @@ -32,6 +32,7 @@ class Keyboard { isSelectionClearDisabled = false; isComposition = false; isCommonDropDisabled = false; + isRtl = false; init () { this.unbind(); @@ -158,7 +159,7 @@ class Keyboard { canClose = false; } else if (selection) { - const ids = selection.get(I.SelectType.Block); + const ids = selection?.get(I.SelectType.Block) || []; if (ids.length) { canClose = false; }; @@ -180,7 +181,7 @@ class Keyboard { // Shortcuts this.shortcut('ctrl+space', e, () => { - S.Popup.open('shortcut', { preventResize: true }); + S.Popup.open('shortcut', {}); }); // Print @@ -241,6 +242,14 @@ class Keyboard { Action.themeSet(!theme ? 'dark' : ''); }); + // Lock the app + this.shortcut(`${cmd}+alt+l`, e, () => { + const pin = Storage.getPin(); + if (pin) { + Renderer.send('pinCheck'); + }; + }); + // Object id this.shortcut(`${cmd}+shift+\\`, e, () => { S.Popup.open('confirm', { @@ -263,12 +272,6 @@ class Keyboard { this.pageCreate({}, analytics.route.shortcut); }); - // Quick capture menu - this.shortcut(`${cmd}+alt+n`, e, () => { - e.preventDefault(); - this.onQuickCapture(true); - }); - // Lock/Unlock this.shortcut(`ctrl+shift+l`, e, () => { this.onToggleLock(); @@ -283,8 +286,9 @@ class Keyboard { checkSelection () { const range = U.Common.getSelectionRange(); const selection = S.Common.getRef('selectionProvider'); + const ids = selection?.get(I.SelectType.Block) || []; - if ((range && !range.collapsed) || (selection && selection.get(I.SelectType.Block).length)) { + if ((range && !range.collapsed) || ids.length) { return true; }; @@ -296,7 +300,7 @@ class Keyboard { return; }; - const flags = [ I.ObjectFlag.SelectTemplate, I.ObjectFlag.DeleteEmpty ]; + const flags = [ I.ObjectFlag.SelectType, I.ObjectFlag.SelectTemplate, I.ObjectFlag.DeleteEmpty ]; U.Object.create('', '', details, I.BlockPosition.Bottom, '', flags, route, message => { U.Object.openConfig(message.details); @@ -442,8 +446,9 @@ class Keyboard { }; const rootId = this.getRootId(); - const logPath = U.Common.getElectron().logPath(); - const tmpPath = U.Common.getElectron().tmpPath(); + const electron = U.Common.getElectron(); + const logPath = electron.logPath(); + const tmpPath = electron.tmpPath(); const route = analytics.route.menuSystem; switch (cmd) { @@ -561,7 +566,7 @@ class Keyboard { }; case 'debugTree': { - C.DebugTree(rootId, logPath, (message: any) => { + C.DebugTree(rootId, logPath, false, (message: any) => { if (!message.error.code) { Renderer.send('openPath', logPath); }; @@ -620,6 +625,15 @@ class Keyboard { break; }; + case 'debugLog': { + C.DebugExportLog(tmpPath, (message: any) => { + if (!message.error.code) { + Renderer.send('openPath', tmpPath); + }; + }); + break; + }; + case 'resetOnboarding': { Storage.delete('onboarding'); break; @@ -816,9 +830,9 @@ class Keyboard { let isDisabled = false; if (!isPopup) { - isDisabled = this.isMainSet() || this.isMainGraph(); + isDisabled = this.isMainSet() || this.isMainGraph() || this.isMainChat(); } else { - isDisabled = [ 'set', 'store', 'graph' ].includes(popupMatch.params.action); + isDisabled = [ 'set', 'store', 'graph', 'chat' ].includes(popupMatch.params.action); }; if (isDisabled) { @@ -849,47 +863,6 @@ class Keyboard { }); }; - menuFromNavigation (id: string, param: Partial<I.MenuParam>, data: any) { - const menuParam = Object.assign({ - element: '#navigationPanel', - className: 'fixed', - classNameWrap: 'fromNavigation', - type: I.MenuType.Horizontal, - horizontal: I.MenuDirection.Center, - vertical: I.MenuDirection.Top, - noFlipY: true, - offsetY: -12, - data, - }, param); - - if (S.Menu.isOpen(id)) { - S.Menu.open(id, menuParam); - } else { - S.Popup.close('search', () => { - S.Menu.closeAll(J.Menu.navigation, () => { - S.Menu.open(id, menuParam); - }); - }); - }; - }; - - onQuickCapture (shortcut: boolean, param?: Partial<I.MenuParam>) { - param = param || {}; - - if ((S.Common.navigationMenu != I.NavigationMenuMode.Hover) && S.Menu.isOpen('quickCapture')) { - S.Menu.close('quickCapture'); - return; - }; - - const button = $('#button-navigation-plus'); - - this.menuFromNavigation('quickCapture', { - ...param, - onOpen: () => button.addClass('active'), - onClose: () => button.removeClass('active'), - }, { isExpanded: shortcut }); - }; - onLock (rootId: string, v: boolean, route?: string) { const block = S.Block.getLeaf(rootId, rootId); if (!block) { @@ -972,6 +945,10 @@ class Keyboard { return this.isMain() && (this.match?.params?.action == 'graph'); }; + isMainChat () { + return this.isMain() && (this.match?.params?.action == 'chat'); + }; + isMainIndex () { return this.isMain() && (this.match?.params?.action == 'index'); }; @@ -1031,6 +1008,10 @@ class Keyboard { this.isComposition = v; }; + setRtl (v: boolean) { + this.isRtl = v; + }; + initPinCheck () { const { account } = S.Auth; const check = () => { @@ -1052,14 +1033,11 @@ class Keyboard { return; }; - this.setPinChecked(false); - if (this.isMain()) { S.Common.redirectSet(U.Router.getRoute()); }; - U.Router.go('/auth/pin-check', { replace: true, animate: true }); - Renderer.send('pin-check'); + Renderer.send('pinCheck'); }, S.Common.pinTime); }; diff --git a/src/ts/lib/mark.ts b/src/ts/lib/mark.ts index 47f5efa555..762058d502 100644 --- a/src/ts/lib/mark.ts +++ b/src/ts/lib/mark.ts @@ -1,5 +1,5 @@ import $ from 'jquery'; -import { I, U, J, analytics } from 'Lib'; +import { I, U, J } from 'Lib'; const Tags = {}; for (const i in I.MarkType) { @@ -139,8 +139,7 @@ class Mark { if (add) { map[type].push(mark); }; - - analytics.event('ChangeTextStyle', { type, count: 1 }); + return U.Common.unmap(map).sort(this.sort); }; @@ -304,11 +303,12 @@ class Mark { return; }; - const attr = this.paramToAttr(mark.type, param); + const fixedParam = param.replace(/([^\\])\$/gi, '$1\\$'); // Escape $ symbol for inline LaTeX + const attr = this.paramToAttr(mark.type, fixedParam); const data = []; if (param) { - data.push(`data-param="${param}"`); + data.push(`data-param="${fixedParam}"`); }; if ([ I.MarkType.Link, I.MarkType.Object, I.MarkType.Mention ].includes(mark.type)) { diff --git a/src/ts/lib/preview.ts b/src/ts/lib/preview.ts index c625d6c54f..10ccf023ea 100644 --- a/src/ts/lib/preview.ts +++ b/src/ts/lib/preview.ts @@ -1,6 +1,5 @@ import $ from 'jquery'; -import raf from 'raf'; -import { I, S, U, J, keyboard, sidebar, analytics } from 'Lib'; +import { I, S, U, J, keyboard } from 'Lib'; const BORDER = 12; const DELAY_TOOLTIP = 650; @@ -8,7 +7,7 @@ const DELAY_PREVIEW = 300; interface TooltipParam { text: string; - element: JQuery<HTMLElement>; + element: any; typeX: I.MenuDirection.Left | I.MenuDirection.Center | I.MenuDirection.Right; typeY: I.MenuDirection.Top | I.MenuDirection.Center | I.MenuDirection.Bottom; offsetX: number; @@ -164,7 +163,12 @@ class Preview { * Display a preview */ previewShow (param: I.Preview) { - if (keyboard.isPreviewDisabled) { + if ( + keyboard.isPreviewDisabled || + keyboard.isResizing || + keyboard.isDragging + ) { + window.clearTimeout(this.timeout.preview); return; }; @@ -202,9 +206,7 @@ class Preview { }); }; - obj.toggleClass('passThrough', passThrough); - obj.off('mouseleave.preview').on('mouseleave.preview', () => this.previewHide(true)); - + obj.toggleClass('passThrough', Boolean(passThrough)); this.previewHide(true); if (param.delay) { @@ -283,38 +285,6 @@ class Preview { }, force ? 0 : 250); }; - /** - * This method is used by toast to position itself on the screen - */ - toastPosition () { - const obj = $('#toast'); - const { ww } = U.Common.getWindowDimensions(); - const y = 32; - const sw = sidebar.getDummyWidth();; - const x = (ww - sw) / 2 - obj.outerWidth() / 2 + sw; - - obj.show().css({ opacity: 0, transform: 'scale3d(0.7,0.7,1)' }); - - raf(() => { - obj.css({ left: x, top: y, opacity: 1, transform: 'scale3d(1,1,1)' }); - }); - }; - - /** - * Show the share app tooltip - */ - shareTooltipShow () { - S.Common.shareTooltipSet(true); - analytics.event('OnboardingTooltip', { id: 'ShareApp' }); - }; - - /** - * Hide the share app tooltip - */ - shareTooltipHide () { - S.Common.shareTooltipSet(false); - }; - /** * Force hides all tooltips, previews, and toasts. */ @@ -322,7 +292,6 @@ class Preview { this.tooltipHide(true); this.previewHide(true); this.toastHide(true); - this.shareTooltipHide(); }; }; diff --git a/src/ts/lib/relation.ts b/src/ts/lib/relation.ts index c60b3f7648..3db37df76f 100644 --- a/src/ts/lib/relation.ts +++ b/src/ts/lib/relation.ts @@ -10,6 +10,8 @@ class Relation { }; public className (v: I.RelationType): string { + v = Number(v); + let c = ''; if ([ I.RelationType.Select, I.RelationType.MultiSelect ].includes(v)) { c = `select ${this.selectClassName(v)}`; @@ -109,9 +111,32 @@ class Relation { return ret; }; - public formulaByType (type: I.RelationType): { id: string, name: string, short?: string, section: I.FormulaSection }[] { + public formulaByType (relationKey: string, type: I.RelationType): { id: string, name: string, short?: string, section: I.FormulaSection }[] { + const relation = S.Record.getRelationByKey(relationKey); + if (!relation) { + return []; + }; + + const isArrayType = this.isArrayType(type); + const skipEmptyKeys = [ + 'type', + 'creator', + 'createdDate', + 'addedDate', + ]; + const skipEmpty = [ + I.FormulaType.CountEmpty, + I.FormulaType.CountNotEmpty, + I.FormulaType.PercentEmpty, + I.FormulaType.PercentNotEmpty, + ]; + const skipUnique = [ + I.FormulaType.CountValue, + ]; + const common = [ { id: I.FormulaType.Count, name: translate('formulaCount'), section: I.FormulaSection.Count }, + { id: I.FormulaType.CountValue, name: translate('formulaValue'), short: translate('formulaValueShort'), section: I.FormulaSection.Count }, { id: I.FormulaType.CountDistinct, name: translate('formulaDistinct'), short: translate('formulaDistinctShort'), section: I.FormulaSection.Count }, { id: I.FormulaType.CountEmpty, name: translate('formulaEmpty'), short: translate('formulaEmptyShort'), section: I.FormulaSection.Count }, { id: I.FormulaType.CountNotEmpty, name: translate('formulaNotEmpty'), short: translate('formulaNotEmptyShort'), section: I.FormulaSection.Count }, @@ -120,7 +145,7 @@ class Relation { ]; let ret: any[] = [ - { id: I.FormulaType.None, name: translate('formulaNone') }, + { id: I.FormulaType.None, name: translate('commonNone') }, ]; switch (type) { @@ -165,7 +190,17 @@ class Relation { }; - return ret.map(it => ({ ...it, id: String(it.id)})); + if (skipEmptyKeys.includes(relationKey)) { + ret = ret.filter(it => !skipEmpty.includes(it.id)); + }; + if (relation.maxCount == 1) { + ret = ret.filter(it => !skipUnique.includes(it.id)); + }; + if (!isArrayType) { + ret = ret.filter(it => ![ I.FormulaType.CountValue ].includes(it.id)); + }; + + return U.Menu.prepareForSelect(ret); }; public filterConditionsDictionary () { @@ -549,6 +584,10 @@ class Relation { return this.isUrl(type) || [ I.RelationType.Number, I.RelationType.ShortText ].includes(type); }; + public isDate (type: I.RelationType) { + return type == I.RelationType.Date; + }; + public getUrlScheme (type: I.RelationType, value: string): string { value = String(value || ''); diff --git a/src/ts/lib/sidebar.ts b/src/ts/lib/sidebar.ts index a12cb9180d..d75bfe645b 100644 --- a/src/ts/lib/sidebar.ts +++ b/src/ts/lib/sidebar.ts @@ -19,6 +19,7 @@ class Sidebar { loader: JQuery<HTMLElement> = null; dummy: JQuery<HTMLElement> = null; toggleButton: JQuery<HTMLElement> = null; + syncButton: JQuery<HTMLElement> = null; vault: JQuery<HTMLElement> = null; isAnimating = false; timeoutAnim = 0; @@ -60,6 +61,7 @@ class Sidebar { this.loader = this.page.find('#loader'); this.dummy = $('#sidebarDummy'); this.toggleButton = $('#sidebarToggle'); + this.syncButton = $('#sidebarSync'); if (vault) { this.vault = $(vault.node); @@ -223,11 +225,17 @@ class Sidebar { const vw = isClosed || !showVault || !keyboard.isMain() ? 0 : J.Size.vault.width; const pageWidth = ww - width - vw; const ho = keyboard.isMainHistory() ? J.Size.history.panel : 0; - const navigation = S.Common.getRef('navigation'); let toggleX = 16; + let syncX = 52; + if ((width && showVault) || (U.Common.isPlatformMac() && !isFullScreen)) { toggleX = 84; + syncX = 120; + + if (width) { + syncX = J.Size.vault.width + width - 40; + }; }; this.header.css({ width: '' }).removeClass('withSidebar'); @@ -239,8 +247,8 @@ class Sidebar { this.page.toggleClass('sidebarAnimation', animate); this.dummy.toggleClass('sidebarAnimation', animate); this.toggleButton.toggleClass('sidebarAnimation', animate); + this.syncButton.toggleClass('sidebarAnimation', animate); - navigation?.position(width + vw, animate); this.header.toggleClass('withSidebar', !!width); this.page.css({ width: pageWidth }); @@ -248,6 +256,7 @@ class Sidebar { this.header.css({ width: pageWidth - ho }); this.footer.css({ width: pageWidth - ho }); this.toggleButton.css({ left: toggleX }); + this.syncButton.css({ left: syncX }); $(window).trigger('sidebarResize'); }; diff --git a/src/ts/lib/storage.ts b/src/ts/lib/storage.ts index de4b62a4ab..8016168ced 100644 --- a/src/ts/lib/storage.ts +++ b/src/ts/lib/storage.ts @@ -1,5 +1,7 @@ import { I, S, U, J } from 'Lib'; +const electron = U.Common.getElectron(); + const ACCOUNT_KEYS = [ 'spaceId', 'spaceOrder', @@ -15,23 +17,54 @@ const SPACE_KEYS = [ 'popupSearch', 'focus', 'openUrl', + 'redirectInvite', ]; +const Api = { + get: (key: string) => { + if (electron.storeGet) { + return electron.storeGet(key); + } else { + localStorage.getItem(key); + }; + }, + + set: (key: string, obj: any) => { + if (electron.storeSet) { + electron.storeSet(key, obj); + } else { + localStorage.setItem(key, JSON.stringify(obj)); + }; + }, + + delete: (key: string) => { + if (electron.storeDelete) { + electron.storeDelete(key); + } else { + localStorage.removeItem(key); + }; + }, +}; + class Storage { storage: any = null; + store: any = null; constructor () { this.storage = localStorage; }; get (key: string): any { - const o = String(this.storage[key] || ''); + let o = Api.get(key); + if (!o) { + o = this.parse(String(this.storage[key] || '')); + }; if (this.isSpaceKey(key)) { if (o) { delete(this.storage[key]); - this.set(key, this.parse(o), true); + this.set(key, o, true); }; return this.getSpaceKey(key); @@ -39,16 +72,18 @@ class Storage { if (this.isAccountKey(key)) { if (o) { delete(this.storage[key]); - this.set(key, this.parse(o), true); + this.set(key, o, true); }; return this.getAccountKey(key); } else { - return this.parse(o); + return o; }; }; set (key: string, obj: any, del?: boolean): void { + obj = U.Common.objectCopy(obj); + if (!key) { console.log('[Storage].set: key not specified'); return; @@ -73,7 +108,8 @@ class Storage { if (this.isAccountKey(key)) { this.setAccountKey(key, o); } else { - this.storage[key] = JSON.stringify(o); + Api.set(key, o); + //delete(this.storage[key]); }; }; @@ -84,6 +120,8 @@ class Storage { if (this.isAccountKey(key)) { this.deleteAccountKey(key); } else { + U.Common.getElectron().storeDelete(key); + Api.delete(key); delete(this.storage[key]); }; }; @@ -92,31 +130,41 @@ class Storage { return SPACE_KEYS.includes(key); }; - setSpaceKey (key: string, value: any) { - const obj = this.getSpace(); + setSpaceKey (key: string, value: any, spaceId?: string) { + spaceId = spaceId || S.Common.space; + + const obj = this.getSpace(spaceId); - obj[S.Common.space][key] = value; + if (spaceId) { + obj[spaceId][key] = value; + }; this.setSpace(obj); }; - getSpaceKey (key: string) { - const obj = this.getSpace(); - return obj[S.Common.space][key]; + getSpaceKey (key: string, spaceId?: string) { + spaceId = spaceId || S.Common.space; + + const obj = this.getSpace(spaceId); + return obj[spaceId][key]; }; - deleteSpaceKey (key: string) { - const obj = this.getSpace(); + deleteSpaceKey (key: string, spaceId?: string) { + spaceId = spaceId || S.Common.space; + + const obj = this.getSpace(spaceId); - delete(obj[S.Common.space][key]); + delete(obj[spaceId][key]); this.setSpace(obj); }; - getSpace () { + getSpace (spaceId?: string) { + spaceId = spaceId || S.Common.space; + const obj = this.get('space') || {}; - obj[S.Common.space] = obj[S.Common.space] || {}; + obj[spaceId] = obj[spaceId] || {}; return obj; }; @@ -152,7 +200,9 @@ class Storage { const obj = this.getAccount(); const accountId = this.getAccountId(); - obj[accountId][key] = value; + if (accountId) { + obj[accountId][key] = value; + }; this.setAccount(obj); }; @@ -224,7 +274,7 @@ class Storage { this.deleteLastOpenedByWindowId(windowIds); }; - deleteLastOpenedByWindowId (windowIds: string[],) { + deleteLastOpenedByWindowId (windowIds: string[]) { windowIds = windowIds.filter(id => id != '1'); if (!windowIds.length) { @@ -437,6 +487,10 @@ class Storage { }; setChat (id: string, obj: any) { + if (!id) { + return; + }; + const map = this.get('chat') || {}; map[id] = Object.assign(map[id] || {}, obj); diff --git a/src/ts/lib/survey.ts b/src/ts/lib/survey.ts index d034e4ceb1..97eb07e0b8 100644 --- a/src/ts/lib/survey.ts +++ b/src/ts/lib/survey.ts @@ -40,6 +40,7 @@ class Survey { break; case I.SurveyType.Pmf: + param.complete = true; param.time = U.Date.now(); break; }; @@ -80,22 +81,23 @@ class Survey { const time = U.Date.now(); const obj = Storage.getSurvey(type); const timeRegister = this.getTimeRegister(); - const lastCompleted = Number(obj.time || Storage.get('lastSurveyTime')) || 0; - const lastCanceled = Number(obj.time || Storage.get('lastSurveyCanceled')) || 0; + const lastTime = Number(obj.time) || 0; const week = 86400 * 7; const month = 86400 * 30; - const registerTime = timeRegister <= time - week; - const completeTime = obj.complete && registerTime && (lastCompleted <= time - month); - const cancelTime = obj.cancel && registerTime && (lastCanceled <= time - month); + const cancelTime = obj.cancel && registerTime && (lastTime <= time - (month * 2)); + + if (obj.complete) { + return; + }; // Show this survey to 5% of users - if (this.checkRandSeed(5) && !completeTime) { + if (!this.checkRandSeed(5)) { Storage.setSurvey(type, { time }); return; }; - if (!S.Popup.isOpen() && (cancelTime || !lastCompleted) && !completeTime) { + if (!S.Popup.isOpen() && (cancelTime || !lastTime)) { this.show(type); }; }; @@ -119,6 +121,7 @@ class Survey { }; checkObject (type: I.SurveyType) { + const { space } = S.Common; const timeRegister = this.getTimeRegister(); const isComplete = this.isComplete(type); @@ -127,6 +130,7 @@ class Survey { }; U.Data.search({ + spaceId: space, filters: [ { relationKey: 'layout', condition: I.FilterCondition.In, value: U.Object.getPageLayouts() }, { relationKey: 'createdDate', condition: I.FilterCondition.Greater, value: timeRegister + 86400 * 3 } @@ -177,4 +181,4 @@ class Survey { }; -export default new Survey(); \ No newline at end of file +export default new Survey(); diff --git a/src/ts/lib/util/common.ts b/src/ts/lib/util/common.ts index 662e6d2f03..e2da6b8616 100644 --- a/src/ts/lib/util/common.ts +++ b/src/ts/lib/util/common.ts @@ -578,7 +578,7 @@ class UtilCommon { text: translate('popupConfirmObjectOpenErrorText'), textConfirm: translate('popupConfirmObjectOpenErrorButton'), onConfirm: () => { - C.DebugTree(rootId, logPath, (message: any) => { + C.DebugTree(rootId, logPath, false, (message: any) => { if (!message.error.code) { Renderer.send('openPath', logPath); }; @@ -627,11 +627,13 @@ class UtilCommon { }); }; - getScheme (url: string): string { - url = String(url || ''); - - const m = url.match(/^([a-z]+):/); - return m ? m[1] : ''; + getScheme(url: string): string { + try { + const u = new URL(String(url || '')); + return u.protocol.replace(/:$/, ''); + } catch { + return ''; + } }; intercept (obj: any, change: any) { @@ -665,7 +667,7 @@ class UtilCommon { }; searchParam (url: string): any { - const a = url.replace(/^\?/, '').split('&'); + const a = String(url || '').replace(/^\?/, '').split('&'); const param: any = {}; a.forEach((s) => { @@ -707,7 +709,7 @@ class UtilCommon { getUrlsFromText (text: string): any[] { const urls = []; - const words = text.split(/[\s\r?\n]+/); + const words = text.split(/[\s\r\n]+/); let offset = 0; @@ -805,8 +807,9 @@ class UtilCommon { return; }; - const ret: any[] = []; let n = 0; + + const ret: any[] = []; const cb = () => { n++; if (n == items.length) { @@ -816,19 +819,19 @@ class UtilCommon { for (const item of items) { if (item.path) { - ret.push({ name: item.name, path: item.path }); + ret.push(item); cb(); } else { const reader = new FileReader(); reader.onload = () => { - ret.push({ - name: item.name, + ret.push({ + ...item, path: this.getElectron().fileWrite(item.name, reader.result, { encoding: 'binary' }), }); cb(); }; reader.onerror = cb; - reader.readAsBinaryString(item); + reader.readAsBinaryString(item.file ? item.file : item); }; }; }; @@ -838,7 +841,7 @@ class UtilCommon { const ret: any = {}; for (const k in data) { - ret['data-' + k] = data[k]; + ret[`data-${k}`] = data[k]; }; return ret; }; @@ -1028,18 +1031,18 @@ class UtilCommon { return !!this.getElectron().version.app.match(/beta/); }; - isChatAllowed () { - const { config, space } = S.Common; - return config.experimental; - - //return config.experimental || (space == J.Constant.localLoversSpaceId); - //return this.isAlphaVersion() || this.isBetaVersion() || !this.getElectron().isPackaged; - }; - checkRtl (s: string): boolean { return /^[\u04c7-\u0591\u05D0-\u05EA\u05F0-\u05F4\u0600-\u06FF]/.test(s); }; + slug (s: string): string { + return String(s || '').toLowerCase().trim().normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .replace(/[^a-z0-9\s\-]/g, '') + .replace(/\s+/g, '-') + .replace(/-+/g, '-'); + }; + }; export default new UtilCommon(); diff --git a/src/ts/lib/util/data.ts b/src/ts/lib/util/data.ts index 486c6763f1..e676ab5122 100644 --- a/src/ts/lib/util/data.ts +++ b/src/ts/lib/util/data.ts @@ -201,11 +201,6 @@ class UtilData { Storage.set('bgColor', 'orange'); }; - [ - I.SurveyType.Register, - I.SurveyType.Object, - ].forEach(it => Survey.check(it)); - Storage.clearDeletedSpaces(); if (callBack) { @@ -289,6 +284,7 @@ class UtilData { createSubSpaceSubscriptions (ids: string[], callBack?: () => void) { const { account } = S.Auth; + const skipIds = U.Space.getSystemDashboardIds(); if (!account) { if (callBack) { @@ -317,7 +313,7 @@ class UtilData { U.Space.getParticipantId(space.targetSpaceId, account.id), ]; - if (![ I.HomePredefinedId.Graph, I.HomePredefinedId.Last ].includes(space.spaceDashboardId)) { + if (!skipIds.includes(space.spaceDashboardId)) { ids.push(space.spaceDashboardId); }; @@ -330,7 +326,8 @@ class UtilData { ], noDeps: true, ignoreDeleted: true, - ignoreHidden: false, + ignoreHidden: true, + ignoreArchived: true, }); }); @@ -482,7 +479,7 @@ class UtilData { }; chatRelationKeys () { - return J.Relation.default.concat([ 'source', 'picture' ]); + return J.Relation.default.concat([ 'source', 'picture', 'widthInPixels', 'heightInPixels' ]); }; createSession (phrase: string, key: string, callBack?: (message: any) => void) { @@ -839,7 +836,7 @@ class UtilData { console.error('[U.Data].searchSubscribe: subId is empty'); if (callBack) { - callBack({}); + callBack({ error: { code: 1, description: 'subId is empty' } }); }; return; }; @@ -848,7 +845,7 @@ class UtilData { console.error('[U.Data].searchSubscribe: spaceId is empty'); if (callBack) { - callBack({}); + callBack({ error: { code: 1, description: 'spaceId is empty' } }); }; return; }; @@ -885,7 +882,7 @@ class UtilData { console.error('[U.Data].subscribeIds: subId is empty'); if (callBack) { - callBack({}); + callBack({ error: { code: 1, description: 'subId is empty' } }); }; return; }; @@ -894,7 +891,7 @@ class UtilData { console.error('[U.Data].subscribeIds: spaceId is empty'); if (callBack) { - callBack({}); + callBack({ error: { code: 1, description: 'spaceId is empty' } }); }; return; }; @@ -903,7 +900,7 @@ class UtilData { console.error('[U.Data].subscribeIds: ids list is empty'); if (callBack) { - callBack({}); + callBack({ error: { code: 1, description: 'ids list is empty' } }); }; return; }; @@ -944,9 +941,10 @@ class UtilData { ignoreHidden: true, ignoreDeleted: true, ignoreArchived: true, + skipLayoutFormat: null, }, param); - const { spaceId, idField, sorts, offset, limit } = param; + const { spaceId, idField, sorts, offset, limit, skipLayoutFormat } = param; const keys: string[] = [ ...new Set(param.keys as string[]) ]; const filters = this.searchDefaultFilters(param); @@ -954,7 +952,7 @@ class UtilData { console.error('[U.Data].search: spaceId is empty'); if (callBack) { - callBack({}); + callBack({ error: { code: 1, description: 'spaceId is empty' } }); }; return; }; @@ -965,7 +963,7 @@ class UtilData { C.ObjectSearch(spaceId, filters, sorts.map(this.sortMapper), keys, param.fullText, offset, limit, (message: any) => { if (message.records) { - message.records = message.records.map(it => S.Detail.mapper(it)); + message.records = message.records.map(it => S.Detail.mapper(it, skipLayoutFormat)); }; if (callBack) { @@ -1143,54 +1141,52 @@ class UtilData { const yesterday = now - U.Date.timestamp(y, m, d - 1); const lastWeek = now - U.Date.timestamp(y, m, d - 7); const lastMonth = now - U.Date.timestamp(y, m - 1, d); - const groups = { - today: [], - yesterday: [], - lastWeek: [], - lastMonth: [], - older: [] - }; + const groups = {}; + const ids = [ 'today', 'yesterday', 'lastWeek', 'lastMonth', 'older' ]; - const groupNames = [ 'today', 'yesterday', 'lastWeek', 'lastMonth', 'older' ]; if (dir == I.SortType.Asc) { - groupNames.reverse(); + ids.reverse(); }; - let groupedRecords = []; - - if (!sectionTemplate) { - sectionTemplate = {}; - }; + ids.forEach(id => groups[id] = []); + let ret = []; records.forEach((record) => { const diff = now - record[key]; + + let id = ''; if (diff < today) { - groups.today.push(record); + id = 'today'; } else if (diff < yesterday) { - groups.yesterday.push(record); + id = 'yesterday'; } else if (diff < lastWeek) { - groups.lastWeek.push(record); + id = 'lastWeek'; } else if (diff < lastMonth) { - groups.lastMonth.push(record); + id = 'lastMonth'; } else { - groups.older.push(record); + id = 'older'; }; + groups[id].push(record); }); - groupNames.forEach((name) => { - if (groups[name].length) { - groupedRecords.push(Object.assign({ id: name, isSection: true }, sectionTemplate)); + ids.forEach(id => { + if (groups[id].length) { + ret.push(Object.assign({ + id, + name: translate(U.Common.toCamelCase([ 'common', id ].join('-'))), + isSection: true, + }, sectionTemplate || {})); + if (dir) { - groups[name] = groups[name].sort((c1, c2) => U.Data.sortByNumericKey(key, c1, c2, dir)); + groups[id] = groups[id].sort((c1, c2) => U.Data.sortByNumericKey(key, c1, c2, dir)); }; - groupedRecords = groupedRecords.concat(groups[name]); + ret = ret.concat(groups[id]); }; }); - - return groupedRecords; + return ret; }; getLinkBlockParam (id: string, layout: I.ObjectLayout, allowBookmark?: boolean) { diff --git a/src/ts/lib/util/date.ts b/src/ts/lib/util/date.ts index cd6c87d751..593fcb5ddc 100644 --- a/src/ts/lib/util/date.ts +++ b/src/ts/lib/util/date.ts @@ -163,6 +163,9 @@ class UtilDate { N: () => { return (f.w() + 6) % 7; }, + l: () => { + return translate(`day${f.N() + 1}`); + }, }; return format.replace(/[\\]?([a-zA-Z])/g, (t: string, s: string) => { let ret = null; @@ -189,6 +192,7 @@ class UtilDate { case I.DateFormat.Long: f = 'F j, Y'; break; case I.DateFormat.Nordic: f = 'j. M Y'; break; case I.DateFormat.European: f = 'j.m.Y'; break; + case I.DateFormat.Default: f = 'D, M d, Y'; break; }; return f; }; @@ -411,4 +415,4 @@ class UtilDate { }; -export default new UtilDate(); +export default new UtilDate(); \ No newline at end of file diff --git a/src/ts/lib/util/embed.ts b/src/ts/lib/util/embed.ts index 1f00b49614..ed9dd0c325 100644 --- a/src/ts/lib/util/embed.ts +++ b/src/ts/lib/util/embed.ts @@ -24,7 +24,14 @@ class UtilEmbed { }; getYoutubeHtml (content: string): string { - return `<iframe src="${content}" ${IFRAME_PARAM} title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>`; + let url = ''; + + try { + const a = new URL(content); + a.search += '&enablejsapi=1&rel=0'; + url = a.toString(); + } catch (e) {}; + return `<iframe id="player" src="${url.toString()}" ${IFRAME_PARAM} title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>`; }; getVimeoHtml (content: string): string { diff --git a/src/ts/lib/util/menu.ts b/src/ts/lib/util/menu.ts index 0f833d7368..d48921a2c1 100644 --- a/src/ts/lib/util/menu.ts +++ b/src/ts/lib/util/menu.ts @@ -112,7 +112,8 @@ class UtilMenu { const items = U.Data.getObjectTypesForNewObject({ withSet: true, withCollection: true }); const ret: any[] = [ { type: I.BlockType.Page, id: 'existingPage', icon: 'existing', lang: 'ExistingPage', arrow: true, aliases: [ 'link' ] }, - { type: I.BlockType.File, id: 'existingFile', icon: 'existing', lang: 'ExistingFile', arrow: true, aliases: [ 'file' ] } + { type: I.BlockType.File, id: 'existingFile', icon: 'existing', lang: 'ExistingFile', arrow: true, aliases: [ 'file' ] }, + { id: 'date', icon: 'date', lang: 'Date', arrow: true }, ]; items.sort((c1, c2) => U.Data.sortByNumericKey('lastUsedDate', c1, c2, I.SortType.Desc)); @@ -329,6 +330,7 @@ class UtilMenu { options, onSelect: (e, option) => { S.Menu.closeAll([ 'select' ]); + if (close) { close(); }; @@ -358,7 +360,7 @@ class UtilMenu { }; getRelationTypes () { - return [ + return this.prepareForSelect([ { id: I.RelationType.Object }, { id: I.RelationType.LongText }, { id: I.RelationType.Number }, @@ -374,7 +376,7 @@ class UtilMenu { it.name = translate(`relationName${it.id}`); it.icon = `relation ${Relation.className(it.id)}`; return it; - }); + })); }; getWidgetLimitOptions (layout: I.WidgetLayout) { @@ -390,7 +392,7 @@ class UtilMenu { break; }; }; - return options.map(id => ({ id: String(id), name: id })); + return this.prepareForSelect(options.map(id => ({ id, name: id }))); }; getWidgetLayoutOptions (id: string, layout: I.ObjectLayout) { @@ -594,9 +596,10 @@ class UtilMenu { data: { options: [ { id: I.HomePredefinedId.Graph, name: translate('commonGraph') }, + (U.Object.isAllowedChat() ? { id: I.HomePredefinedId.Chat, name: translate('commonChat') } : null), { id: I.HomePredefinedId.Last, name: translate('spaceLast') }, { id: I.HomePredefinedId.Existing, name: translate('spaceExisting'), arrow: true }, - ], + ].filter(it => it), onOver: (e: any, item: any) => { if (!menuContext) { return; @@ -639,6 +642,7 @@ class UtilMenu { switch (item.id) { case I.HomePredefinedId.Graph: + case I.HomePredefinedId.Chat: case I.HomePredefinedId.Last: { onSelect({ id: item.id }, false); @@ -710,10 +714,6 @@ class UtilMenu { }; spaceContext (space: any, param: any) { - if (space.isPersonal) { - return; - }; - const { targetSpaceId } = space; const isOwner = U.Space.isMyOwner(targetSpaceId); const isLocalNetwork = U.Data.isLocalNetwork(); @@ -851,7 +851,6 @@ class UtilMenu { }; getFixedWidgets () { - const { config } = S.Common; return [ { id: J.Constant.widgetId.favorite, name: translate('widgetFavorite'), iconEmoji: '⭐' }, { id: J.Constant.widgetId.set, name: translate('widgetSet'), iconEmoji: '🔍' }, @@ -1038,6 +1037,7 @@ class UtilMenu { dateFormatOptions () { return ([ + { id: I.DateFormat.Default }, { id: I.DateFormat.MonthAbbrBeforeDay }, { id: I.DateFormat.MonthAbbrAfterDay }, { id: I.DateFormat.Short }, @@ -1054,8 +1054,8 @@ class UtilMenu { timeFormatOptions () { return [ - { id: I.TimeFormat.H12, name: translate('menuDataviewDate12Hour') }, - { id: I.TimeFormat.H24, name: translate('menuDataviewDate24Hour') }, + { id: I.TimeFormat.H12, name: translate('timeFormat12') }, + { id: I.TimeFormat.H24, name: translate('timeFormat24') }, ]; }; @@ -1079,10 +1079,10 @@ class UtilMenu { getFormulaSections (relationKey: string) { const relation = S.Record.getRelationByKey(relationKey); - const options = Relation.formulaByType(relation.format); + const options = Relation.formulaByType(relationKey, relation.format); return [ - { id: I.FormulaSection.None, name: translate('formulaNone') }, + { id: I.FormulaSection.None, name: translate('commonNone') }, ].concat([ { id: I.FormulaSection.Count, name: translate('formulaCount'), arrow: true }, { id: I.FormulaSection.Percent, name: translate('formulaPercentage'), arrow: true }, @@ -1090,9 +1090,13 @@ class UtilMenu { { id: I.FormulaSection.Date, name: translate('formulaDate'), arrow: true }, ].filter(s => { return options.filter(it => it.section == s.id).length; - })).map(it => ({ ...it, id: String(it.id), checkbox: false })); + })).map(it => ({ ...it, checkbox: false })); + }; + + prepareForSelect (a: any[]) { + return a.map(it => ({ ...it, id: String(it.id) })); }; }; -export default new UtilMenu(); +export default new UtilMenu(); \ No newline at end of file diff --git a/src/ts/lib/util/object.ts b/src/ts/lib/util/object.ts index 4acf59328c..54c56e5a92 100644 --- a/src/ts/lib/util/object.ts +++ b/src/ts/lib/util/object.ts @@ -53,10 +53,6 @@ class UtilObject { }; universalRoute (object: any): string { - if (!object) { - return; - }; - return object ? `object?objectId=${object.id}&spaceId=${object.spaceId}` : ''; }; @@ -153,7 +149,6 @@ class UtilObject { }; param = param || {}; - param.preventResize = true; param.data = Object.assign(param.data || {}, { matchPopup: { params } }); if (object._routeParam_) { @@ -257,6 +252,10 @@ class UtilObject { C.ObjectListSetDetails([ rootId ], [ { key: 'defaultTemplateId', value: id } ], callBack); }; + setLastUsedDate (rootId: string, timestamp: number, callBack?: (message: any) => void) { + C.ObjectListSetDetails([ rootId ], [ { key: 'lastUsedDate', value: timestamp } ], callBack); + }; + name (object: any) { const { layout, snippet } = object; @@ -483,18 +482,25 @@ class UtilObject { return this.getPageLayouts().includes(layout); }; - openDateByTimestamp (t: number, method?: string) { + isAllowedChat () { + const { config, space } = S.Common; + return config.experimental || J.Constant.chatSpaceId.includes(space); + }; + + openDateByTimestamp (relationKey: string, t: number, method?: string) { method = method || 'auto'; - const fn = U.Common.toCamelCase(`open-${method}`); + let fn = U.Common.toCamelCase(`open-${method}`); + if (!this[fn]) { + fn = 'openAuto'; + }; C.ObjectDateByTimestamp(S.Common.space, t, (message: any) => { if (!message.error.code) { - if (U.Object[fn]) { - U.Object[fn](message.details); - } else { - U.Object.openConfig(message.details); - }; + const object = message.details; + + object._routeParam_ = { relationKey }; + this[fn](object); }; }); }; diff --git a/src/ts/lib/util/router.ts b/src/ts/lib/util/router.ts index 83909d5c3c..eee052e601 100644 --- a/src/ts/lib/util/router.ts +++ b/src/ts/lib/util/router.ts @@ -1,6 +1,15 @@ import $ from 'jquery'; import { I, C, S, U, J, Preview, analytics, Storage } from 'Lib'; +interface RouteParam { + page: string; + action: string; + id: string; + spaceId: string; + viewId: string; + relationKey: string; +}; + class UtilRouter { history: any = null; @@ -35,15 +44,17 @@ class UtilRouter { return param; }; - build (param: Partial<{ page: string; action: string; id: string; spaceId: string; viewId: string; }>): string { + build (param: Partial<RouteParam>): string { const { page, action } = param; const id = String(param.id || J.Constant.blankRouteId); const spaceId = String(param.spaceId || J.Constant.blankRouteId); const viewId = String(param.viewId || J.Constant.blankRouteId); + const relationKey = String(param.relationKey || J.Constant.blankRouteId); let route = [ page, action, id ]; route = route.concat([ 'spaceId', spaceId ]); route = route.concat([ 'viewId', viewId ]); + route = route.concat([ 'relationKey', relationKey ]); return route.join('/'); }; @@ -139,7 +150,7 @@ class UtilRouter { return; }; - const withChat = U.Common.isChatAllowed(); + const withChat = U.Object.isAllowedChat(); S.Menu.closeAllForced(); S.Progress.showSet(false); @@ -154,7 +165,13 @@ class UtilRouter { this.isOpening = false; if (message.error.code) { - U.Data.onAuthWithoutSpace(); + const spaces = U.Space.getList().filter(it => it.targetSpaceId != id); + + if (spaces.length) { + this.switchSpace(spaces[0].targetSpaceId, route, false, routeParam); + } else { + U.Router.go('/main/void', routeParam); + }; return; }; diff --git a/src/ts/lib/util/space.ts b/src/ts/lib/util/space.ts index 98fc1a3961..f6fda90c34 100644 --- a/src/ts/lib/util/space.ts +++ b/src/ts/lib/util/space.ts @@ -64,6 +64,9 @@ class UtilSpace { if (id == I.HomePredefinedId.Graph) { ret = this.getGraph(); } else + if (id == I.HomePredefinedId.Chat) { + ret = this.getChat(); + } else if (id == I.HomePredefinedId.Last) { ret = this.getLastOpened(); } else { @@ -76,11 +79,14 @@ class UtilSpace { return ret; }; + getSystemDashboardIds () { + return [ I.HomePredefinedId.Graph, I.HomePredefinedId.Chat, I.HomePredefinedId.Last ]; + }; + getGraph () { return { id: I.HomePredefinedId.Graph, name: translate('commonGraph'), - iconEmoji: ':earth_americas:', layout: I.ObjectLayout.Graph, }; }; @@ -92,6 +98,14 @@ class UtilSpace { }; }; + getChat () { + return { + id: S.Block.workspace, + name: translate('commonChat'), + layout: I.ObjectLayout.Chat, + }; + }; + getList () { return S.Record.getRecords(J.Constant.subId.space, U.Data.spaceRelationKeys()).filter(it => it.isAccountActive); }; @@ -173,12 +187,13 @@ class UtilSpace { return S.Common.isOnline && !U.Data.isLocalNetwork(); }; - isShareBanner () { + hasShareBanner () { const hasShared = !!this.getList().find(it => it.isShared && this.isMyOwner(it.targetSpaceId)); const space = this.getSpaceview(); const closed = Storage.get('shareBannerClosed'); - return !space.isPersonal && !space.isShared && !closed && this.isMyOwner() && !hasShared; + // return !space.isPersonal && !space.isShared && !closed && this.isMyOwner() && !hasShared; + return false; }; getReaderLimit () { @@ -233,7 +248,7 @@ class UtilSpace { return; }; - blocks.forEach(block => Storage.setToggle('widget', block.id, true)); + blocks.forEach(block => Storage.setToggle('widget', block.id, false)); const first = blocks[0]; const children = S.Block.getChildren(widgets, first.id); @@ -256,4 +271,4 @@ class UtilSpace { }; -export default new UtilSpace(); \ No newline at end of file +export default new UtilSpace(); diff --git a/src/ts/model/block.ts b/src/ts/model/block.ts index e20d38bbf2..0494fcfc3c 100644 --- a/src/ts/model/block.ts +++ b/src/ts/model/block.ts @@ -131,6 +131,10 @@ class Block implements I.Block { return this.isLink() || this.isBookmark() || this.isFile() || this.isText() || this.isDataview(); }; + canContextMenu(): boolean { + return !this.isSystem() && !this.isTable() && !this.isDataview() && !this.isCover() && !this.isChat(); + }; + isSystem () { return this.isPage() || this.isLayout(); }; @@ -171,6 +175,10 @@ class Block implements I.Block { return this.type == I.BlockType.Chat; }; + isCover (): boolean { + return this.type == I.BlockType.Cover; + }; + isRelation (): boolean { return this.type == I.BlockType.Relation; }; @@ -219,10 +227,6 @@ class Block implements I.Block { return this.isLayout() && (this.content.style == I.LayoutStyle.Header); }; - isLayoutFooter (): boolean { - return this.isLayout() && (this.content.style == I.LayoutStyle.Footer); - }; - isLayoutTableRows (): boolean { return this.isLayout() && (this.content.style == I.LayoutStyle.TableRows); }; diff --git a/src/ts/store/block.ts b/src/ts/store/block.ts index 3a685e0549..060a5481b6 100644 --- a/src/ts/store/block.ts +++ b/src/ts/store/block.ts @@ -533,9 +533,9 @@ class BlockStore { }; const { from, to } = mark.range; - const object = S.Detail.get(rootId, mark.param, [ 'name', 'layout', 'snippet', 'fileExt' ], true); + const object = S.Detail.get(rootId, mark.param, [ 'name', 'layout', 'snippet', 'fileExt', 'timestamp' ], true); - if (object._empty_ || U.Object.isDateLayout(object.layout)) { + if (object._empty_) { continue; }; @@ -551,7 +551,7 @@ class BlockStore { name = object.name; }; - name = U.Common.shorten(object.name.trim(), 30); + name = U.Common.shorten(name.trim(), 30); if (old != name) { const d = String(old || '').length - String(name || '').length; diff --git a/src/ts/store/chat.ts b/src/ts/store/chat.ts index a82ec476e0..e3649d40b4 100644 --- a/src/ts/store/chat.ts +++ b/src/ts/store/chat.ts @@ -1,5 +1,5 @@ import { observable, action, makeObservable, set } from 'mobx'; -import { I, M } from 'Lib'; +import { I, U, M } from 'Lib'; class ChatStore { @@ -17,14 +17,25 @@ class ChatStore { set (rootId: string, list: I.ChatMessage[]): void { list = list.map(it => new M.ChatMessage(it)); + list = U.Common.arrayUniqueObjects(list, 'id'); this.messageMap.set(rootId, observable.array(list)); }; prepend (rootId: string, add: I.ChatMessage[]): void { add = add.map(it => new M.ChatMessage(it)); - const list = this.getList(rootId); + let list = this.getList(rootId); list.unshift(...add); + list = U.Common.arrayUniqueObjects(list, 'id'); + this.set(rootId, list); + }; + + append (rootId: string, add: I.ChatMessage[]): void { + add = add.map(it => new M.ChatMessage(it)); + + let list = this.getList(rootId); + list.push(...add); + list = U.Common.arrayUniqueObjects(list, 'id'); this.set(rootId, list); }; diff --git a/src/ts/store/common.ts b/src/ts/store/common.ts index 77fa82b6bb..f0ae18ce8a 100644 --- a/src/ts/store/common.ts +++ b/src/ts/store/common.ts @@ -37,12 +37,10 @@ class CommonStore { public notionToken = ''; public showRelativeDatesValue = null; public fullscreenObjectValue = null; - public navigationMenuValue = null; public linkStyleValue = null; public dateFormatValue = null; public timeFormatValue = null; public isOnlineValue = false; - public shareTooltipValue = false; public showVaultValue = null; public hideSidebarValue = null; public showObjectValue = null; @@ -98,16 +96,16 @@ class CommonStore { defaultType: observable, isFullScreen: observable, fullscreenObjectValue: observable, - navigationMenuValue: observable, linkStyleValue: observable, isOnlineValue: observable, - shareTooltipValue: observable, showVaultValue: observable, hideSidebarValue: observable, showObjectValue: observable, spaceId: observable, membershipTiersList: observable, showRelativeDatesValue: observable, + dateFormatValue: observable, + timeFormatValue: observable, config: computed, preview: computed, toast: computed, @@ -118,9 +116,10 @@ class CommonStore { membershipTiers: computed, space: computed, isOnline: computed, - shareTooltip: computed, showVault: computed, showRelativeDates: computed, + dateFormat: computed, + timeFormat: computed, gatewaySet: action, filterSetFrom: action, filterSetText: action, @@ -132,12 +131,10 @@ class CommonStore { nativeThemeSet: action, spaceSet: action, spaceStorageSet: action, - navigationMenuSet: action, linkStyleSet: action, dateFormatSet: action, timeFormatSet: action, isOnlineSet: action, - shareTooltipSet: action, membershipTiersListSet: action, showVaultSet: action, showObjectSet: action, @@ -233,14 +230,6 @@ class CommonStore { return this.boolGet('showRelativeDates'); }; - get navigationMenu (): I.NavigationMenuMode { - let ret = this.navigationMenuValue; - if (ret === null) { - ret = Storage.get('navigationMenu'); - }; - return Number(ret) || I.NavigationMenuMode.Hover; - }; - get linkStyle (): I.LinkCardStyle { let ret = this.linkStyleValue; if (ret === null) { @@ -254,10 +243,16 @@ class CommonStore { get dateFormat (): I.DateFormat { let ret = this.dateFormatValue; + if (ret === null) { ret = Storage.get('dateFormat'); + + if (undefined === ret) { + ret = I.DateFormat.Long; + }; }; - return Number(ret) || I.DateFormat.Long; + + return Number(ret); }; get timeFormat (): I.TimeFormat { @@ -276,10 +271,6 @@ class CommonStore { return Boolean(this.isOnlineValue); }; - get shareTooltip (): boolean { - return Boolean(this.shareTooltipValue); - }; - get membershipTiers (): I.MembershipTier[] { return this.membershipTiersList || []; }; @@ -365,7 +356,7 @@ class CommonStore { }; previewClear () { - this.previewObj = { type: null, target: null, element: null, range: { from: 0, to: 0 }, marks: [] }; + this.previewObj = { type: I.PreviewType.None, target: null, element: null, range: { from: 0, to: 0 }, marks: [] }; }; toastClear () { @@ -478,8 +469,6 @@ class CommonStore { if (c) { head.append(`<link id="link-prism" rel="stylesheet" href="./css/theme/${c}/prism.css" />`); }; - - $(window).trigger('updateTheme'); }; getThemePath () { @@ -495,13 +484,7 @@ class CommonStore { this.languages = v; }; - navigationMenuSet (v: I.NavigationMenuMode) { - v = Number(v); - this.navigationMenuValue = v; - Storage.set('navigationMenu', v); - }; - - linkStyleSet (v: I.NavigationMenuMode) { + linkStyleSet (v: I.LinkCardStyle) { v = Number(v); this.linkStyleValue = v; Storage.set('linkStyle', v); @@ -524,10 +507,6 @@ class CommonStore { console.log('[Online status]:', v); }; - shareTooltipSet (v: boolean) { - this.shareTooltipValue = Boolean(v); - }; - configSet (config: any, force: boolean) { const html = $('html'); diff --git a/src/ts/store/detail.ts b/src/ts/store/detail.ts index d89c1feda7..8d37594956 100644 --- a/src/ts/store/detail.ts +++ b/src/ts/store/detail.ts @@ -147,7 +147,7 @@ class DetailStore { }; /** gets the object. if no keys are provided, all properties are returned. if force keys is set, J.Relation.default are included */ - public get (rootId: string, id: string, withKeys?: string[], forceKeys?: boolean): any { + public get (rootId: string, id: string, withKeys?: string[], forceKeys?: boolean, skipLayoutFormat?: I.ObjectLayout[]): any { let list = this.map.get(rootId)?.get(id) || []; if (!list.length) { return { id, _empty_: true }; @@ -164,21 +164,23 @@ class DetailStore { object[list[i].relationKey] = list[i].value; }; - return this.mapper(object); + return this.mapper(object, skipLayoutFormat); }; /** Mutates object provided and also returns a new object. Sets defaults. * This Function contains domain logic which should be encapsulated in a model */ - public mapper (object: any): any { + public mapper (object: any, skipLayoutFormat?: I.ObjectLayout[]): any { object = this.mapCommon(object || {}); - const fn = `map${I.ObjectLayout[object.layout]}`; - if (this[fn]) { - object = this[fn](object); - }; + if (!skipLayoutFormat || !skipLayoutFormat.includes(object.layout)) { + const fn = `map${I.ObjectLayout[object.layout]}`; + if (this[fn]) { + object = this[fn](object); + }; - if (U.Object.isInFileLayouts(object.layout)) { - object = this.mapFile(object); + if (U.Object.isInFileLayouts(object.layout)) { + object = this.mapFile(object); + }; }; return object; @@ -329,7 +331,9 @@ class DetailStore { private mapParticipant (object) { object.permissions = Number(object.permissions || object.participantPermissions) || I.ParticipantPermissions.Reader; object.status = Number(object.status || object.participantStatus) || I.ParticipantStatus.Joining; + object.identity = Relation.getStringValue(object.identity); object.globalName = Relation.getStringValue(object.globalName); + object.resolvedName = object.globalName || object.identity; delete(object.participantPermissions); delete(object.participantStatus); diff --git a/src/ts/store/extension.ts b/src/ts/store/extension.ts index 083c388de7..5625d0419d 100644 --- a/src/ts/store/extension.ts +++ b/src/ts/store/extension.ts @@ -3,7 +3,6 @@ import { makeObservable, observable, action } from 'mobx'; class ExtensionStore { public createdObject = null; - public challengeId = ''; public serverPort = ''; public gatewayPort = ''; public tabUrlValue = ''; diff --git a/src/ts/store/menu.ts b/src/ts/store/menu.ts index 596c02bd47..f598039384 100644 --- a/src/ts/store/menu.ts +++ b/src/ts/store/menu.ts @@ -167,7 +167,7 @@ class MenuStore { closeAll (ids?: string[], callBack?: () => void) { const items = this.getItems(ids); - const timeout = this.getTimeout(); + const timeout = this.getTimeout(ids); items.filter(it => !it.param.noClose).forEach(it => this.close(it.id)); this.onCloseAll(timeout, callBack); @@ -175,21 +175,27 @@ class MenuStore { closeAllForced (ids?: string[], callBack?: () => void) { const items = this.getItems(ids); - const timeout = this.getTimeout(); + const timeout = this.getTimeout(ids); items.forEach(it => this.close(it.id)); this.onCloseAll(timeout, callBack); }; onCloseAll (timeout: number, callBack?: () => void) { - if (callBack) { + if (!callBack) { + return; + }; + + if (timeout) { this.clearTimeout(); this.timeout = window.setTimeout(() => callBack(), timeout); + } else { + callBack(); }; }; - getTimeout (): number { - const items = this.getItems(); + getTimeout (ids?: string[]): number { + const items = this.getItems(ids); let t = 0; for (const item of items) { diff --git a/src/ts/store/popup.ts b/src/ts/store/popup.ts index 153bf44a6c..6e6eae33c0 100644 --- a/src/ts/store/popup.ts +++ b/src/ts/store/popup.ts @@ -72,10 +72,6 @@ class PopupStore { }; Preview.previewHide(true); - - if (this.checkShowDimmer(this.popupList)) { - $('#navigationPanel').addClass('hide'); - }; }; get (id: string): I.Popup { @@ -141,10 +137,6 @@ class PopupStore { const filtered = this.popupList.filter(it => it.id != id); - if (!this.checkShowDimmer(filtered)) { - $('#navigationPanel').removeClass('hide'); - }; - if (force) { this.popupList = filtered; diff --git a/src/ts/store/progress.ts b/src/ts/store/progress.ts index 4416ffc197..f52dd07846 100644 --- a/src/ts/store/progress.ts +++ b/src/ts/store/progress.ts @@ -49,32 +49,27 @@ class ProgressStore { this.showValue = Boolean(v); }; - getList () { + getList (filter?: (it: I.Progress) => boolean) { const { space } = S.Common; - const skip = [ I.ProgressState.Done, I.ProgressState.Canceled ]; - return this.list.filter(it => (!it.spaceId || (it.spaceId == space)) && !skip.includes(it.state)); - }; - - getItem (id: string): I.Progress { - return this.getList().find(it => it.id == id); - }; + return this.list.filter(it => { + let ret = true; - getField (field: string): number { - return this.getList().reduce((acc, it) => acc + (Number(it[field]) || 0), 0); - }; + if (filter) { + ret = filter(it); + }; - getCurrent (): number { - return this.getField('current'); + return ret && (!it.spaceId || (it.spaceId == space)); + }); }; - getTotal (): number { - return this.getField('total'); + getItem (id: string): I.Progress { + return this.getList().find(it => it.id == id); }; - getPercent (): number { - const current = this.getCurrent(); - const total = this.getTotal(); + getPercent (list: I.Progress[]): number { + const current = list.reduce((acc, it) => acc + (Number(it.current) || 0), 0); + const total = list.reduce((acc, it) => acc + (Number(it.total) || 0), 0); return total > 0 ? Math.min(100, Math.ceil(current / total * 100)) : 0; }; diff --git a/update.sh b/update.sh index 1dea52a68c..6182b5dda8 100755 --- a/update.sh +++ b/update.sh @@ -11,7 +11,7 @@ folder="build"; if [ "$platform" = "ubuntu-latest" ]; then arch="linux-$arch"; folder="$arch"; -elif [ "$platform" = "macos-13" ]; then +elif [ "$platform" = "macos-13" ] || [ "$platform" = "macos-latest" ]; then arch="darwin-$arch"; folder="$arch"; elif [ "$platform" = "windows-latest" ]; then