1
0
Fork 0
mirror of https://github.com/anyproto/anytype-ts.git synced 2025-06-11 18:20:27 +09:00
This commit is contained in:
Andrew Simachev 2024-12-06 12:33:32 +01:00
commit e579b1ebfa
No known key found for this signature in database
GPG key ID: 1DFE44B21443F0EF
163 changed files with 3956 additions and 4013 deletions

View file

@ -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 {

View file

@ -317,7 +317,6 @@ updateSettings = (param) => {
updateTheme = ({ theme, colors }) => {
data.colors = colors;
initTheme(theme);
redraw();
};

View file

@ -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');
@ -74,6 +75,21 @@ class MenuManager {
Separator,
{
label: Util.translate('electronMenuCustomCss'),
click: () => {
const fp = path.join(Util.userPath(), 'custom.css');
if (!fs.existsSync(fp)) {
fs.writeFileSync(fp, '');
};
shell.openPath(fp);
},
},
Separator,
{ role: 'close', label: Util.translate('electronMenuClose') },
]
},

View file

@ -1 +1 @@
0.37.3
0.38.1

64
package-lock.json generated
View file

@ -1,12 +1,12 @@
{
"name": "anytype",
"version": "0.43.7",
"version": "0.43.26-alpha",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "anytype",
"version": "0.43.7",
"version": "0.43.26-alpha",
"hasInstallScript": true,
"license": "SEE LICENSE IN LICENSE.md",
"dependencies": {
@ -22,6 +22,7 @@
"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",
@ -44,7 +45,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 +93,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",
@ -2358,9 +2359,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 +2417,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",
@ -2786,6 +2778,12 @@
"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",
@ -5890,6 +5888,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",
@ -6539,11 +6547,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",
@ -10824,15 +10831,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 +10846,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 +10857,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",

View file

@ -1,6 +1,6 @@
{
"name": "anytype",
"version": "0.43.7",
"version": "0.43.26-alpha",
"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,6 +102,7 @@
"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",
@ -124,7 +125,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",

View file

@ -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>

Before

Width:  |  Height:  |  Size: 208 B

After

Width:  |  Height:  |  Size: 208 B

Before After
Before After

View file

@ -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',
@ -40,7 +43,7 @@ export default {
graphDepth: 5,
chat: {
messages: 30,
messages: 50,
attachments: 10,
files: 10,
mentions: 10,

View file

@ -160,4 +160,8 @@ export default {
],
pageCover: 'pageCover',
key: {
mention: 'mentions',
}
};

View file

@ -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'

View file

@ -33,6 +33,7 @@
"commonUncategorized": "Uncategorized",
"commonDashboard": "Dashboard",
"commonGraph": "Graph",
"commonChat": "Chat",
"commonFlow": "Flow",
"commonLibrary": "Library",
"commonAllContent": "All Objects",
@ -175,6 +176,7 @@
"commonSignUp": "Sign Up",
"commonNotFound": "Not found",
"commonCalculate": "Calculate",
"commonRelations": "Relations",
"pluralDay": "day|days",
"pluralObject": "Object|Objects",
@ -221,6 +223,7 @@
"electronMenuQuit": "Quit",
"electronMenuFile": "File",
"electronMenuDirectory": "Show Directory",
"electronMenuCustomCss": "Apply custom CSS",
"electronMenuWorkDirectory": "Work",
"electronMenuDataDirectory": "Data",
"electronMenuConfigDirectory": "Config",
@ -482,6 +485,8 @@
"pageMainVoidText": "Looks like youve 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",
@ -1951,7 +1956,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",
@ -2257,6 +2261,8 @@
"formulaPercentage": "Percentage",
"formulaMath": "Math",
"formulaDate": "Date",
"formulaValue": "Count values",
"formulaValueShort": "Values",
"formulaDistinct": "Count unique values",
"formulaDistinctShort": "Unique",
"formulaEmpty": "Count empty",

View file

@ -48,6 +48,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 +74,8 @@
.attachment { width: 540px; }
}
.attachments.isSingle.isBookmark {
.attachment { width: 360px; }
.attachments.isSingle {
.attachment.isBookmark { width: 360px; }
}
/* Layouts */

View file

@ -34,10 +34,8 @@
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'); }

View file

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

View file

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

View file

@ -55,11 +55,11 @@
.cellFoot {
.flex { justify-content: flex-end; }
.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 +80,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; }

View file

@ -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'); }

View file

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

View file

@ -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; }
#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); }
.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; }
}

View file

@ -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 {

View file

@ -150,7 +150,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%; }
}

View file

@ -40,6 +40,11 @@
.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); }

View file

@ -32,7 +32,8 @@ html.bodyIndex, html.bodyAuth {
#sidebarToggle,
#notifications,
.sidebar,
.sidebarDummy { display: none !important; }
.sidebarDummy,
.shareTooltip { display: none !important; }
.popup {
.innerWrap { background: var(--color-popup) !important;; box-shadow: var(--shadow) !important; color: var(--color-text-primary) !important; }
@ -89,7 +90,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; }
}

View file

@ -1,9 +1,10 @@
@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; }
.headSimple { align-items: center; height: 32px; margin-bottom: 20px; }
.headSimple {
.side.right { gap: 0px; }
.side.right {
@ -18,11 +19,17 @@
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%); }
}

View file

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

View file

@ -13,7 +13,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'); }

View file

@ -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 {
@ -41,6 +41,8 @@
}
ul { padding-left: 1.25em; }
.error { margin-bottom: 0px; }
}
.popup.popupConfirm.isWide {

View file

@ -203,11 +203,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; }
.reply { color: var(--color-text-inversion); }
}
.form {

View file

@ -15,16 +15,19 @@
.name { @include text-paragraph; @include text-overflow-nw; font-weight: 600; }
}
.side.right { flex-shrink: 0; }
.side.right { flex-shrink: 0; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; }
.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;
text-align: center; font-weight: 500; height: 18px; line-height: 18px; display: none; padding: 0px 2px;
}
}
}
.body.withCnt {
.side.left { width: calc(100% - 30px); }
.side.right {
.cnt { display: block; }
}
}
}

View file

@ -2,6 +2,6 @@
.viewGraph { padding: 0px; }
.viewGraph {
#graphWrapper { height: 240px; }
.graphWrapper { height: 240px; }
canvas { width: 100%; height: 100%; }
}

View file

@ -173,6 +173,7 @@ class App extends React.Component<object, State> {
render () {
const { isLoading } = this.state;
const platform = U.Common.getPlatform();
const { shareTooltip } = S.Common
let drag = null;
if (platform == I.Platform.Mac) {
@ -204,7 +205,7 @@ class App extends React.Component<object, State> {
<Progress />
<Toast />
<ListNotification key="listNotification" />
<ShareTooltip showOnce={true} />
<ShareTooltip showOnce={true} route={analytics.route.onboarding} />
<Vault ref={ref => S.Common.refSet('vault', ref)} />
<Switch>

View file

@ -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();
@ -76,8 +76,7 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon
rootId={rootId}
blockId={blockId}
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)}
@ -133,7 +132,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();
@ -205,37 +204,88 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon
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();
const list = this.getMessages();
if (!rootId) {
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();
if (!rootId || this.isLoading) {
return;
};
if (!clear && (dir > 0) && this.isLoaded) {
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;
};
this.scrollToMessage(first.id);
C.ChatGetMessages(rootId, before, after, J.Constant.limit.chat.messages, (message: any) => {
this.isLoading = false;
if (message.error.code) {
this.isLoaded = true;
if (callBack) {
callBack();
};
return;
};
const messages = message.messages || [];
if ((dir > 0) && !messages.length) {
this.isLoaded = true;
this.subscribeMessages(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 +389,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 () {
@ -447,13 +497,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 +528,15 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon
});
};
this.isBottom = false;
if (st <= 0) {
this.loadMessages(false);
this.loadMessages(-1, false);
};
if (st - fh >= scrollWrapper.outerHeight() - ch) {
this.isBottom = true;
//this.loadMessages(1, false);
};
dates.each((i, item: any) => {
@ -512,28 +572,30 @@ 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;
};
container.get(0).scrollTo({ top });
}, 50);
const container = U.Common.getScrollContainer(this.props.isPopup);
const top = this.getMessageScrollOffset(id);
container.get(0).scrollTo({ top });
};
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) {
@ -545,18 +607,29 @@ const BlockChat = observer(class BlockChat extends React.Component<I.BlockCompon
return;
};
this.isLoaded = 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.ChatGetMessages(rootId, reply.orderId, limit, (message: any) => {
S.Chat.clear(rootId);
C.ChatGetMessages(rootId, reply.orderId, '', limit, (message: any) => {
if (!message.error.code && message.messages.length) {
messages = messages.concat(message.messages);
};
S.Chat.set(rootId, messages);
C.ChatGetMessages(rootId, '', reply.orderId, limit, (message: any) => {
if (!message.error.code && message.messages.length) {
messages = messages.concat(message.messages);
};
S.Chat.set(rootId, messages);
this.scrollToMessage(reply.id);
});
});
};

View file

@ -1,21 +1,29 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { IconObject, Icon, ObjectName, ObjectDescription, ObjectType, MediaVideo, MediaAudio } from 'Component';
import { IconObject, Icon, ObjectName, ObjectDescription, ObjectType, MediaVideo, MediaAudio, Loader } from 'Component';
import { I, U, S, J, Action } from 'Lib';
interface Props {
object: any;
showAsFile?: boolean;
bookmarkAsDefault?: boolean;
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);
@ -176,13 +184,14 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro
};
renderImage () {
const { object } = this.props;
const { object, scrollToBottom } = this.props;
const { isLoaded } = this.state;
this.previewItem = { type: I.FileType.Image, object };
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 });
@ -195,15 +204,23 @@ const ChatAttachment = observer(class ChatAttachment extends React.Component<Pro
this.previewItem.src = this.src;
return (
if (!isLoaded) {
const img = new Image();
img.onload = () => this.setState({ isLoaded: true });
img.src = this.src;
};
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 () {
@ -230,6 +247,14 @@ 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;

View file

@ -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, 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,7 @@ const ChatMessage = observer(class ChatMessage extends React.Component<I.ChatMes
ref={ref => this.attachmentRefs[item.id] = ref}
key={i}
object={item}
scrollToBottom={scrollToBottom}
onRemove={() => this.onAttachmentRemove(item.id)}
onPreview={(preview) => this.onPreview(preview)}
showAsFile={!attachmentsLayout}
@ -170,10 +174,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 ? (

View file

@ -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 {

View file

@ -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 });
};

View file

@ -1,7 +1,6 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Icon } from 'Component';
import { I, S, C, U, keyboard, Relation, Dataview, analytics, translate } 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;
};
@ -19,16 +17,13 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> {
menuContext = 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);
@ -37,24 +32,18 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> {
};
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) {
return <div />;
};
const cn = [ 'cellFoot', `cell-key-${relationKey}` ];
const option = Relation.formulaByType(relation.format).find(it => it.id == String(viewRelation.formulaType));
const name = option.short || option.name;
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,22 +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) ? (
<div className="select" onClick={this.onSelect}>
<div className="name">{viewRelation.formulaType ? name : translate('commonCalculate')}</div>
<Icon className="arrow light" />
</div>
) : ''}
{!isEditing && option && (result !== null) ? (
{formulaType != I.FormulaType.None ? (
<div className="result">
<span className="name">{name}</span>
{result}
<span className="value">{result}</span>
</div>
) : ''}
</div>
@ -107,6 +89,10 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> {
};
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);
@ -116,38 +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();
onClick (e: any) {
this.setState({ isEditing: true });
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));
};
onSelect (e: any) {
const { relationKey } = this.props;
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} .select`,
element: `#${id}`,
horizontal: I.MenuDirection.Center,
onOpen: this.onOpen,
onClose: this.onClose,
subIds: [ 'select2' ],
data: {
options,
options: U.Menu.prepareForSelect(options),
noScroll: true,
noVirtualisation: true,
onOver: this.onOver,
onSelect: (e: any, item: any) => {
this.onChange(item.id);
this.setEditing(false);
},
}
});
});
Preview.tooltipHide();
};
onOpen (context: any): void {
@ -161,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;
@ -177,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', {
@ -193,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);
},
}
});
@ -216,13 +212,49 @@ const FootCell = observer(class FootCell extends React.Component<Props, State> {
};
onMouseEnter (): void {
if (!keyboard.isDragging) {
$(this.node).addClass('hover');
if (keyboard.isDragging) {
return;
};
const formulaType = this.getFormulaType();
if (formulaType == I.FormulaType.None) {
return;
};
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 () {
$(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;
};
});

View file

@ -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) {

View file

@ -661,7 +661,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props
};
if (!this.canEdit(relation)) {
U.Object.openDateByTimestamp(value, 'config');
U.Object.openDateByTimestamp(relationKey, value, 'config');
ret = true;
break;
};

View file

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

View file

@ -1,63 +1,50 @@
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,
const [ isLoading, setIsLoading ] = useState(false);
const onSelect = () => {
setIsLoading(true);
U.Object.setIcon(rootId, '', '', () => setIsLoading(false));
};
constructor (props: I.BlockComponent) {
super(props);
this.onSelect = this.onSelect.bind(this);
this.onUpload = this.onUpload.bind(this);
const onUpload = (objectId: string) => {
setIsLoading(true);
U.Object.setIcon(rootId, '', objectId, () => setIsLoading(false));
};
render (): any {
const { isLoading } = this.state;
const { rootId, readonly } = this.props;
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={this.onSelect}
onUpload={this.onUpload}
size={128}
iconSize={128}
menuParam={{
horizontal: I.MenuDirection.Center,
}}
/>
</div>
);
};
onSelect () {
this.setLoading(true);
U.Object.setIcon(this.props.rootId, '', '', () => this.setLoading(false));
};
onUpload (objectId: string) {
this.setLoading(true);
U.Object.setIcon(this.props.rootId, '', objectId, () => this.setLoading(false));
};
setLoading (v: boolean) {
this.setState({ isLoading: 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}
iconSize={128}
menuParam={{
horizontal: I.MenuDirection.Center,
}}
/>
</div>
);
}));
export default BlockIconUser;

View file

@ -22,7 +22,7 @@ const BlockLink = observer(class BlockLink extends React.Component<I.BlockCompon
render () {
const { rootId, block } = this.props;
const object = S.Detail.get(rootId, block.getTargetObjectId(), J.Relation.cover);
const object = S.Detail.get(rootId, block.getTargetObjectId(), J.Relation.cover, false, [ I.ObjectLayout.Date ]);
const { _empty_, isArchived, isDeleted, done, layout, coverId, coverType, coverX, coverY, coverScale } = object;
const content = U.Data.checkLinkSettings(block.content, layout);
const readonly = this.props.readonly || !S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Details ]);

View file

@ -305,15 +305,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 +339,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;
};

View file

@ -189,7 +189,7 @@ const Cell = observer(class Cell extends React.Component<Props> {
};
if (relation.format == I.RelationType.Date) {
U.Object.openDateByTimestamp(value, 'config');
U.Object.openDateByTimestamp(relation.relationKey, value, 'config');
return;
}
@ -277,6 +277,7 @@ const Cell = observer(class Cell extends React.Component<Props> {
blockId: block.id,
value,
relation: observable.box(relation),
relationKey: relation.relationKey,
record,
placeholder,
canEdit,

View file

@ -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 ];
if (isTargetTop) {
cn.push('targetTop');
key.push('top');
};
if (isTargetBottom) {
cn.push('targetBot');
key.push('bot');
};
if (isTargetColumn) {
cn.push('targetCol');
key.push('col');
};
constructor (props: Props) {
super(props);
this.onClick = this.onClick.bind(this);
};
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>
);
};
onClick (e: any) {
const { onClick } = this.props;
if (onClick) {
onClick(e);
};
};
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;

View file

@ -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}
@ -274,7 +273,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) {
@ -560,6 +559,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();
@ -620,15 +620,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;
@ -653,18 +645,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, () => {

View file

@ -38,18 +38,13 @@ const DragVertical = forwardRef<DragVerticalRefProps, Props>(({
$(trackRef.current).css({ height: `${Math.round(v * 72)}px` });
};
const setValue = (v: number) => {
inputRef.current.setValue(v);
setHeight(v);
};
const handleChange = (e: ChangeEvent<HTMLInputElement>, value: string) => {
const v = 1 - Number(value) || 0;
e.preventDefault();
e.stopPropagation();
setValue(v);
setHeight(v);
if (onChange) {
onChange(e, v);
@ -57,8 +52,8 @@ const DragVertical = forwardRef<DragVerticalRefProps, Props>(({
};
useImperativeHandle(ref, () => ({
getValue: () => inputRef?.current.getValue(),
setValue,
getValue: () => inputRef.current?.getValue(),
setValue: (v: number) => inputRef.current?.setValue(v),
}));
useEffect(() => {

View file

@ -0,0 +1,293 @@
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 nodeRef = useRef(null);
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;

View file

@ -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> {
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 ];
public static defaultProps = {
className: '',
inputClassName: '',
tooltipY: I.MenuDirection.Bottom,
if (isFocused) {
cn.push('isFocused');
};
state = {
isActive: false,
if (isActive) {
cn.push('isActive');
};
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);
};
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();
const focus = () => {
inputRef.current.focus();
};
componentDidUpdate () {
this.checkButton();
this.resize();
const blur = () => {
inputRef.current.blur();
};
focus () {
this.ref.focus();
this.checkButton();
};
blur () {
this.ref.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,158 @@ class Filter extends React.Component<Props, State> {
};
};
onInput () {
this.placeholderCheck();
const onSelectHandler = (e: any) => {
if (onSelect) {
onSelect(e);
};
};
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 });
};
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);
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);
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();
};
placeholderSet (v: string) {
this.placeholder.text(v);
const placeholderSet = (v: string) => {
$(placeholderRef.current).text(v);
};
placeholderHide () {
this.placeholder.hide();
const placeholderHide = () => {
$(placeholderRef.current).hide();
};
placeholderShow () {
this.placeholder.show();
const placeholderShow = () => {
$(placeholderRef.current).show();
};
resize () {
this.placeholder.css({ lineHeight: this.placeholder.height() + 'px' });
const resize = () => {
const ref = $(placeholderRef.current);
ref.css({ lineHeight: ref.height() + 'px' });
};
};
const init = () => {
buttonCheck();
resize();
};
useEffect(() => init(), []);
useEffect(() => init(), [ value ]);
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;

View file

@ -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';
@ -103,81 +105,33 @@ const Input = forwardRef<InputRef, Props>(({
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>) => {
const handleKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
if ($(inputRef.current).hasClass('disabled')) return;
handleEvent(onKeyUp, e);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
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>) => {
isFocused.current = true;
addClass('isFocused');
keyboard.setFocus(true);
@ -185,7 +139,7 @@ const Input = forwardRef<InputRef, Props>(({
handleEvent(onFocus, e);
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
const handleBlur = (e: FocusEvent<HTMLInputElement>) => {
isFocused.current = false;
removeClass('isFocused');
keyboard.setFocus(false);
@ -193,7 +147,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 +155,7 @@ const Input = forwardRef<InputRef, Props>(({
});
};
const handleCut = (e: React.ClipboardEvent<HTMLInputElement>) => {
const handleCut = (e: ClipboardEvent<HTMLInputElement>) => {
e.persist();
callWithTimeout(() => {
updateRange(e);
@ -209,7 +163,7 @@ const Input = forwardRef<InputRef, Props>(({
});
};
const handleSelect = (e: React.SyntheticEvent<HTMLInputElement>) => {
const handleSelect = (e: SyntheticEvent<HTMLInputElement>) => {
updateRange(e);
handleEvent(onSelect, e);
};
@ -277,6 +231,44 @@ const Input = forwardRef<InputRef, Props>(({
return rect;
};
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}

View file

@ -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,122 @@ 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> {
const InputWithFile: FC<Props> = ({
icon = '',
textUrl = translate('inputWithFileTextUrl'),
textFile = '',
withFile = true,
accept,
block,
readonly = false,
canResize = true,
onChangeUrl,
onChangeFile,
}) => {
public static defaultProps = {
withFile: true,
canResize: true,
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');
};
if (isSmall) {
cn.push('isSmall');
};
_isMounted = false;
node: any = null;
state = {
focused: false,
size: Size.Full,
if (readonly) {
cn.push('isReadonly');
};
if (isIcon) {
cn.push('isIcon');
onClick = e => onClickFile(e);
};
if (isFocused) {
cn.push('isFocused');
};
if (withFile && isFocused) {
placeholder += or + (!isSmall ? textFile : '');
};
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);
};
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>
{withFile ? (
<span className="fileWrap" onMouseDown={this.onClickFile}>
{!isSmall ? <span>&nbsp;{translate('commonOr')}&nbsp;</span> : ''}
<span className="border">{textFile}</span>
</span>
) : ''}
</div>
</div>
);
};
componentDidMount () {
this._isMounted = true;
this.resize();
this.rebind();
};
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 });
const rebind = () => {
if (canResize) {
$(nodeRef.current).off('resizeMove').on('resizeMove', () => resize());
};
};
componentWillUnmount () {
const { focused } = focus.state;
const { block } = this.props;
this._isMounted = false;
this.unbind();
if (focused == block.id) {
keyboard.setFocus(false);
const unbind = () => {
if (canResize) {
$(nodeRef.current).off('resizeMove');
};
};
rebind () {
const { canResize } = this.props;
if (!this._isMounted || !canResize) {
return;
};
$(this.node).off('resizeMove').on('resizeMove', (e: any) => this.resize());
};
unbind () {
const { canResize } = this.props;
if (!this._isMounted || !canResize) {
return;
};
$(this.node).off('resizeMove');
};
resize () {
const { canResize } = this.props;
const resize = () => {
if (!canResize) {
return;
};
raf(() => {
if (!this._isMounted) {
return;
};
const node = $(this.node);
const node = $(nodeRef.current);
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 +143,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 +158,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>&nbsp;{translate('commonOr')}&nbsp;</span> : ''}
<span className="border">{textFile}</span>
</span>
) : ''}
</div>
</div>
);
};
export default InputWithFile;

View file

@ -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;
interface InputWithLabelRefProps {
focus(): void;
blur(): void;
setValue(v: string): void;
getValue(): string;
setRange(range: I.TextRange): void;
isFocused(): boolean;
};
constructor (props: Props) {
super(props);
const InputWithLabel = forwardRef<InputWithLabelRefProps, Props>((props, ref) => {
const {
label = '',
value = '',
readonly = false,
onFocus,
onBlur,
} = props;
this.onFocus = this.onFocus.bind(this);
this.onBlur = this.onBlur.bind(this);
};
render () {
const { label } = this.props;
const nodeRef = useRef(null);
const inputRef = useRef(null);
const [ isFocused, setIsFocused ] = useState(false);
const cn = [ 'inputWithLabel' ];
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>
);
if (isFocused) {
cn.push('isFocused');
};
componentDidMount() {
const node = $(this.node);
this.ref.setValue(this.props.value);
const focus = () => {
inputRef.current?.focus();
};
focus () {
this.ref.focus();
const blur = () => {
inputRef.current?.blur();
};
blur () {
this.ref.blur();
const setValue = (v: string) => {
inputRef.current?.setValue(v);
};
setValue (v: string) {
this.ref.setValue(v);
const getValue = () => {
return inputRef.current?.getValue();
};
getValue () {
return this.ref.getValue();
const setRange = (range: I.TextRange) => {
inputRef.current?.setRange(range);
};
setRange (range: I.TextRange) {
this.ref.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), []);
removeFocusedClass () {
const node = $(this.node);
node.removeClass('isFocused');
};
useImperativeHandle(ref, () => ({
focus,
blur,
setValue,
getValue,
setRange,
isFocused: () => isFocused,
}));
};
return (
<div
ref={nodeRef}
onClick={focus}
className={cn.join(' ')}
>
<div className="inner">
<Label text={label} />
export default InputWithLabel;
<Input
ref={inputRef}
{...props}
onFocus={onFocusHandler}
onBlur={onBlurHandler}
/>
</div>
</div>
);
});
export default InputWithLabel;

View file

@ -1,4 +1,4 @@
import * as React from 'react';
import React, { forwardRef, useState, useEffect, useImperativeHandle, MouseEvent } from 'react';
import $ from 'jquery';
import { I, S, U, Relation } from 'Lib';
import { Icon, MenuItemVertical } from 'Component';
@ -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);
};
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[] = [];
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);
};
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>
);
};
componentDidMount () {
this._isMounted = true;
const options = this.getOptions();
let value = Relation.getArrayValue(this.props.value);
if (!value.length && options.length) {
value = [ options[0].id ];
};
this.setState({ value, options });
if (className) {
cn.push(className);
};
componentWillUnmount () {
this._isMounted = false;
if (readonly) {
cn.push('isReadonly');
};
getOptions () {
const { initial } = this.props;
const options = [];
let val = Relation.getArrayValue(value);
val.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]);
};
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;
return ret;
};
setOptions (options: any[]) {
this.setState({ options });
const getValue = (val: any): any => {
return isMultiple ? val : (val.length ? val[0] : '');
};
getValue (): any {
const { isMultiple } = this.props;
const value = Relation.getArrayValue(this.state.value);
return isMultiple ? value : value[0];
const setValueHandler = (v: any) => {
setValue(Relation.getArrayValue(v));
};
setValue (v: any) {
const value = Relation.getArrayValue(v);
if (this._isMounted) {
this.state.value = value;
this.setState({ value });
};
};
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: 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;

View file

@ -46,7 +46,7 @@ const Switch = forwardRef<SwitchRefProps, Props>(({
};
};
useEffect(() => setValue(initialValue));
useEffect(() => setValue(initialValue), []);
useImperativeHandle(ref, () => ({
getValue: () => value,

View file

@ -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> {
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' ];
public static defaultProps = {
value: ''
if (className) {
cn.push(className);
};
_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);
};
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 onChangeHandler = (e: any) => {
setValue(e.target.value);
if (onChange) {
onChange(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 onKeyDownHandler = (e: any) => {
setValue(e.target.value);
if (onKeyDown) {
onKeyDown(e, e.target.value);
};
};
onInput (e: any) {
if (this.props.onInput) {
this.props.onInput(e, e.target.value);
const onKeyUpHandler = (e: any) => {
setValue(e.target.value);
if (onKeyUp) {
onKeyUp(e, e.target.value);
};
};
onFocus (e: any) {
if (this.props.onFocus) {
this.props.onFocus(e, this.state.value);
const onInputHandler = (e: any) => {
if (onInput) {
onInput(e, e.target.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);
};
};
onPaste (e: any) {
if (this.props.onPaste) {
this.props.onPaste(e);
const onCopyHandler = (e: any) => {
if (onCopy) {
onCopy(e, value);
};
};
focus () {
window.setTimeout(() => {
if (!this._isMounted) {
return;
};
this.textAreaElement.focus({ preventScroll: true });
});
};
select () {
window.setTimeout(() => {
if (!this._isMounted) {
return;
};
this.textAreaElement.select();
});
};
setValue (v: string) {
this.setState({ value: v });
};
getValue () {
return this.state.value;
};
setError (v: boolean) {
$(this.node).toggleClass('withError', v);
};
addClass (v: string) {
if (this._isMounted) {
$(this.node).addClass(v);
const onPasteHandler = (e: any) => {
if (onPaste) {
onPaste(e);
};
};
removeClass (v: string) {
if (this._isMounted) {
$(this.node).removeClass(v);
};
const focus = () => {
window.setTimeout(() => nodeRef.current.focus({ preventScroll: true }));
};
};
const select = () => {
window.setTimeout(() => nodeRef.current.select());
};
const setError = (v: boolean) => {
$(nodeRef.current).toggleClass('withError', v);
};
const addClass = (v: string) => {
$(nodeRef.current).addClass(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;

View file

@ -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,82 @@ interface Props {
storageKey: string;
};
const Graph = observer(class Graph extends React.Component<Props> {
interface GraphRefProps {
init: () => void;
resize: () => void;
};
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 = '';
const Graph = observer(forwardRef<GraphRefProps, Props>(({
id = '',
isPopup = false,
rootId = '',
data = {},
storageKey = '',
}, ref) => {
constructor (props: Props) {
super(props);
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);
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();
};
componentWillUnmount () {
if (this.worker) {
this.worker.terminate();
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 +97,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 +122,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 +163,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 +187,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 +212,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,22 +259,21 @@ 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);
if (!node || !node.length) {
return;
@ -316,8 +281,8 @@ const Graph = observer(class Graph extends React.Component<Props> {
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 +293,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 +325,90 @@ 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;
};
};
};
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: [] });
});
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 });
});
});
if (!length) {
return;
};
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 +418,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 +441,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 +451,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 +498,59 @@ 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,
}));
return (
<div
ref={nodeRef}
className="graphWrapper"
>
<div id={elementId} />
</div>
);
}));
export default Graph;

View file

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

View file

@ -81,7 +81,7 @@ class Header extends React.Component<Props> {
componentDidUpdate () {
sidebar.resizePage(null, null, false);
this.refChild.forceUpdate();
this.refChild?.forceUpdate();
};
renderLeftIcons (onOpen?: () => void) {

View file

@ -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> {
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);
state = {
version: null,
};
useImperativeHandle(ref, () => ({
setVersion,
forceUpdate: () => setDummyState(dummyState + 1),
}));
constructor (props: I.HeaderComponent) {
super(props);
return (
<React.Fragment>
<div className="side left">{renderLeftIcons()}</div>
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>
<div className="side center">
<div className="txt">
{version ? U.Date.date('M d, Y g:i:s A', version.time) : ''}
</div>
</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>
);
};
<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>
);
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 });
};
});
}));
export default HeaderMainHistory;

View file

@ -98,8 +98,24 @@ const HeaderMainObject = observer(class HeaderMainObject extends React.Component
</div>
<div className="side right">
{showRelations ? <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} /> : ''}
{showRelations ? (
<Icon
id="button-header-relation"
tooltip={translate('commonRelations')}
tooltipCaption={`${cmd} + Shift + R`}
className="relation withBackground"
onClick={this.onRelation}
/>
) : ''}
{showMenu ? (
<Icon
id="button-header-more"
tooltip={translate('commonMenu')}
className="more withBackground"
onClick={this.onMore}
/>
) : ''}
</div>
</React.Fragment>
);

View file

@ -19,6 +19,7 @@ import ListNotification from './list/notification';
import ListChildren from './list/children';
import ListObject from './list/object';
import ListObjectManager from './list/objectManager';
import ListObjectPreview from './list/objectPreview';
import Header from './header';
import Footer from './footer';
@ -60,6 +61,9 @@ import Pin from './form/pin';
import Filter from './form/filter';
import Phrase from './form/phrase';
import TabSwitch from './form/tabSwitch';
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';
@ -96,7 +100,7 @@ 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';
export {
Page,
@ -118,6 +122,7 @@ export {
ListChildren,
ListObject,
ListObjectManager,
ListObjectPreview,
ListNotification,
Header,
@ -194,5 +199,8 @@ export {
ShareBanner,
FooterAuthDisclaimer,
EmailCollectionForm,
EmailCollection,
Floater,
HeadSimple,
EditorControls,
};

View file

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

View file

@ -45,7 +45,7 @@ const ListObjectManager = observer(class ListObjectManager extends React.Compone
offset = 0;
cache: any = null;
refList: List = null;
refFilter: Filter = null;
refFilter = null;
refCheckbox: Map<string, any> = new Map();
selected: string[] = [];
timeout = 0;

View file

@ -53,7 +53,7 @@ class ListObjectPreview extends React.Component<Props> {
<div className="scroller">
<div className="heading">
<div className="name">Blank</div>
<div className="name">{translate('commonBlank')}</div>
</div>
</div>
<div className="border" />

View file

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

View file

@ -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(' ')}
/>
);

View file

@ -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>
);
};
@ -281,7 +265,6 @@ const MenuBlockMention = observer(class MenuBlockMention extends React.Component
fullText: filter,
offset: this.offset,
limit: J.Constant.limit.menuRecords,
skipLayoutFormat: [ I.ObjectLayout.Date ],
}, (message: any) => {
if (message.error.code) {
this.setState({ isLoading: false });
@ -370,6 +353,7 @@ const MenuBlockMention = observer(class MenuBlockMention extends React.Component
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) {

View file

@ -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);
const getItems = () => {
return props.param.data.options || [];
};
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;
};
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;

View file

@ -1,18 +1,27 @@
import * as React from 'react';
import { I, S, U, translate } from 'Lib';
import { I, S, U, J, translate } 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>;
};
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(),
};
render () {
const { param } = this.props;
const { data, classNameWrap } = param;
const { value, isEmpty, canEdit, canClear = true } = data;
const { dotMap } = this.state;
const items = this.getData();
const { m, y } = U.Date.getCalendarDateParam(value);
const todayParam = U.Date.getCalendarDateParam(this.originalValue);
@ -91,15 +100,20 @@ 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');
};
const check = dotMap.get([ item.d, item.m, item.y ].join('-'));
return (
<div
key={i}
key={i}
id={[ 'day', item.d, item.m, item.y ].join('-')}
className={cn.join(' ')}
onClick={e => this.onClick(e, item)}
onContextMenu={e => this.onContextMenu(e, item)}
>
{item.d}
<div className="inner">
{item.d}
{check ? <div className="bullet" /> : ''}
</div>
</div>
);
})}
@ -130,6 +144,7 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu>
const { value } = data;
this.originalValue = value;
this.initDotMap();
this.forceUpdate();
};
@ -145,17 +160,35 @@ 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();
const { param } = this.props;
const { data } = param;
const { canEdit } = data;
const { canEdit, relationKey } = data;
if (canEdit) {
this.setValue(U.Date.timestamp(item.y, item.m, item.d), true, true);
} else {
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));
};
};
@ -163,7 +196,8 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu>
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('-')}`,
@ -176,7 +210,7 @@ const MenuCalendar = observer(class MenuCalendar extends React.Component<I.Menu>
{ 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));
}
}
});

View file

@ -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, readonly, onCreate } = data;
const timestamp = U.Date.timestamp(y, m, d);
const items = this.getItems();
const cn = [ 'wrap' ];
const menuId = getId();
@ -19,7 +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;
const w = Number(U.Date.date('N', timestamp)) + 1;
label = `${translate(`day${w}`)} ${d}`;
size = 18;
};
@ -60,7 +61,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">

View file

@ -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>
);

View file

@ -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>
);
};

View file

@ -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);
},

View file

@ -341,7 +341,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', {

View file

@ -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(() => (

View file

@ -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) : '' },

View file

@ -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 });
@ -134,6 +80,40 @@ class MenuHelp extends React.Component<I.Menu> {
};
};
const items = getItems();
export default MenuHelp;
useEffect(() => {
rebind();
S.Common.shareTooltipSet(true);
Highlight.showAll();
return () => unbind();
}, []);
useImperativeHandle(ref, () => ({
rebind,
unbind,
getItems,
getIndex: () => n.current,
setIndex: (i: number) => n.current = i,
}), []);
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;

View file

@ -745,6 +745,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 +775,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 +810,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 +818,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,25 +850,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;
@ -899,7 +921,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,18 +939,21 @@ 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) {
@ -937,36 +962,39 @@ const Menu = observer(class Menu extends React.Component<I.Menu, State> {
};
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();
index = 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 || next.isEmpty) {
this.ref.n++;
if (items[this.ref.n]) {
this.setActive(items[this.ref.n], scroll);
index++;
if (items[index]) {
this.setActive(items[index], scroll);
};
} else {
this.setHover(next, scroll);
};
this.setIndex(index);
};
setHover (item?: any, scroll?: boolean) {

View file

@ -9,13 +9,16 @@ 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,
} = this.props;
const cn = [ 'item' ];
const id = this.props.id || '';
const cn = [];
const withArrow = arrow || subComponent;
isDiv ? cn.push('separator') : cn.push('item');
let hasClick = true;
let iconMainElement = null;
let iconSideElement = null;
@ -93,6 +96,9 @@ class MenuItemVertical extends React.Component<I.MenuItem> {
};
let content = null;
if (isDiv) {
content = <div className="inner" />;
} else
if (withDescription) {
content = (
<React.Fragment>
@ -158,7 +164,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}

View file

@ -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(' ')}>

View file

@ -1,50 +1,44 @@
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>
);
const getObject = () => {
return props.param.data.object;
};
componentDidMount () {
this.load();
const setObject = (object: any) => {
props.param.data.object = object;
};
getObject () {
return this.props.param.data.object;
};
load () {
const object = this.getObject();
const load = () => {
const object = getObject();
if (!object) {
return;
};
U.Object.getById(object.id, { keys: U.Data.participantRelationKeys() }, (object: any) => {
if (object) {
this.props.param.data.object = object;
setObject(object);
};
});
};
});
const object = getObject();
const relationKey = object.globalName ? 'globalName': 'identity';
return object ? (
<>
<IconObject object={object} size={96} />
<ObjectName object={object} />
<Label text={U.Common.shorten(object[relationKey], 150)} />
<ObjectDescription object={object} />
</>
) : <EmptySearch text={translate('commonNotFound')} />;
}));
export default MenuParticipant;

View file

@ -68,13 +68,6 @@ const MenuRelationSuggest = observer(class MenuRelationSuggest extends React.Com
</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 {

View file

@ -70,13 +70,6 @@ const MenuSearchObject = observer(class MenuSearchObject extends React.Component
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 {
const props = {
...item,

View file

@ -55,13 +55,6 @@ const MenuSelect = observer(class MenuSelect extends React.Component<I.Menu> {
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

View file

@ -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);

View file

@ -68,13 +68,6 @@ const MenuTypeSuggest = observer(class MenuTypeSuggest extends React.Component<I
</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 {

View file

@ -282,7 +282,7 @@ const MenuWidget = observer(class MenuWidget extends React.Component<I.Menu> {
menuId = 'select';
menuParam.width = 320;
menuParam.data = Object.assign(menuParam.data, {
options: U.Menu.getWidgetLayoutOptions(this.target?.id, this.target?.layout),
options: U.Menu.prepareForSelect(U.Menu.getWidgetLayoutOptions(this.target?.id, this.target?.layout)),
value: this.layout,
onSelect: (e, option) => {
this.layout = Number(option.id);

View file

@ -117,6 +117,7 @@ const PageAuthLogin = observer(class PageAuthLogin extends React.Component<I.Pag
const { mode, path } = networkConfig;
const account = accounts[0];
const { shareTooltip } = S.Common;
S.Auth.accountSet(account);
Renderer.send('keytarSet', account.id, this.refPhrase.getValue());
@ -131,12 +132,12 @@ 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();
S.Common.shareTooltipSet(false);
analytics.event('OnboardingTooltip', { id: 'ShareApp' });
};
},
};

View file

@ -211,8 +211,8 @@ const PageAuthOnboard = observer(class PageAuthOnboard extends React.Component<I
if (stage == Stage.Vault) {
const cb = () => {
Animation.from(() => {
this.setState({ stage: stage + 1 });
this.refNext?.setLoading(false);
this.setState({ stage: stage + 1 });
});
};

View file

@ -9,8 +9,10 @@ interface Props {
isContextMenuDisabled?: boolean;
readonly?: boolean;
noIcon?: boolean;
relationKey?: string;
onCreate?: () => void;
onEdit?: () => void;
getDotMap?: (start: number, end: number, callback: (res: Map<string, boolean>) => void) => void;
};
const EDITORS = [
@ -305,7 +307,7 @@ const HeadSimple = observer(class Controls extends React.Component<Props> {
};
onCalendar = () => {
const { rootId } = this.props;
const { rootId, getDotMap, relationKey } = this.props;
const object = S.Detail.get(rootId, rootId);
S.Menu.open('dataviewCalendar', {
@ -315,7 +317,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,
},
});
@ -323,10 +327,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');
};

View file

@ -105,7 +105,8 @@ const PageMainChat = observer(class PageMainChat extends React.Component<I.PageC
};
unbind () {
const namespace = this.getNamespace();
const { isPopup } = this.props;
const namespace = U.Common.getEventNamespace(isPopup);
const events = [ 'keydown', 'scroll' ];
$(window).off(events.map(it => `${it}.set${namespace}`).join(' '));
@ -114,7 +115,7 @@ const PageMainChat = observer(class PageMainChat extends React.Component<I.PageC
rebind () {
const { isPopup } = this.props;
const win = $(window);
const namespace = this.getNamespace();
const namespace = U.Common.getEventNamespace(isPopup);
const container = U.Common.getScrollContainer(isPopup);
this.unbind();
@ -137,10 +138,6 @@ const PageMainChat = observer(class PageMainChat extends React.Component<I.PageC
};
};
getNamespace () {
return this.props.isPopup ? '-popup' : '';
};
open () {
const rootId = this.getRootId();
@ -177,11 +174,8 @@ const PageMainChat = observer(class PageMainChat 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);
};
@ -189,7 +183,7 @@ const PageMainChat = observer(class PageMainChat extends React.Component<I.PageC
getRootId () {
const { rootId, match } = this.props;
return rootId ? rootId : match.params.id;
return rootId ? rootId : match?.params?.id;
};
onScroll () {

View file

@ -1,11 +1,12 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Header, Footer, Deleted, ListObject, Button } from 'Component';
import { Header, Footer, Deleted, ListObject, Button, Icon, Label, Loader, HeadSimple } from 'Component';
import { I, C, S, U, J, Action, translate, analytics } from 'Lib';
import HeadSimple from 'Component/page/elements/head/simple';
import { eachDayOfInterval, isEqual, format, fromUnixTime } from 'date-fns';
interface State {
isDeleted: boolean;
isLoading: boolean;
relations: any[];
relationKey: string;
};
@ -22,18 +23,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: [],
relationKey: RELATION_KEY_MENTION,
};
render () {
const { space } = S.Common;
const { isDeleted, relations, relationKey } = this.state;
const { isLoading, isDeleted, relations, relationKey } = this.state;
const rootId = this.getRootId();
const object = S.Detail.get(rootId, rootId, []);
@ -42,87 +43,99 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC
};
const relation = S.Record.getRelationByKey(relationKey);
if (!relation) {
return null;
};
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: 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: relationKey, 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 },
];
if ([ 'createdDate' ].includes(relationKey)) {
const map = {
createdDate: 'creator',
const keys = relations.map(it => it.relationKey);
const filters: I.Filter[] = [];
if (relation.format == I.RelationType.Object) {
filters.push({ relationKey: RELATION_KEY_MENTION, condition: I.FilterCondition.In, value: [ object.id ] });
} else {
filters.push({ relationKey: relationKey, condition: I.FilterCondition.Equal, value: object.timestamp, format: I.RelationType.Date });
};
filters.push({ relationKey: map[relationKey], condition: I.FilterCondition.NotEqual, value: J.Constant.anytypeProfileId });
keys.push(map[relationKey]);
};
if ([ 'createdDate' ].includes(relationKey)) {
const map = {
createdDate: 'creator',
};
return (
<div ref={node => this.node = node}>
<Header
{...this.props}
component="mainObject"
ref={ref => this.refHeader = ref}
rootId={rootId}
/>
<div className="blocks wrapper">
<HeadSimple
{...this.props}
noIcon={true}
ref={ref => this.refHead = ref}
rootId={rootId}
readonly={true}
/>
filters.push({ relationKey: map[relationKey], condition: I.FilterCondition.NotEqual, value: J.Constant.anytypeProfileId });
keys.push(map[relationKey]);
};
content = (
<React.Fragment>
<div className="categories">
{relations.map((item) => {
const isMention = item.relationKey == 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={relationKey == 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}
relationKeys={keys}
/>
</div>
<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="mainObject"
ref={ref => this.refHeader = ref}
rootId={rootId}
/>
<div className="blocks wrapper">
<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} />
@ -130,9 +143,69 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC
);
};
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);
});
};
componentDidMount () {
const { match } = this.props;
const { relationKey } = match?.params || {};
this._isMounted = true;
this.open();
if (relationKey) {
this.setState({ relationKey }, () => this.open());
} else {
this.open();
};
};
componentDidUpdate () {
@ -168,7 +241,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)) {
@ -194,11 +267,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);
};
@ -209,6 +279,8 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC
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)) {
@ -231,9 +303,9 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC
return 0;
});
if (relations.length) {
this.setState({ relations });
this.setState({ relations, isLoading: false });
if (relations.length) {
if (!relationKey || !relations.find(it => it.relationKey == relationKey)) {
this.onCategory(relations[0].relationKey);
} else {
@ -247,6 +319,10 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC
onCategory (relationKey: string) {
this.setState({ relationKey }, () => this.reload());
};
onCategoryClick (relationKey: string) {
this.onCategory(relationKey);
analytics.event('SwitchRelationDate', { relationKey });
};
@ -256,9 +332,9 @@ const PageMainDate = observer(class PageMainDate extends React.Component<I.PageC
getRootId () {
const { rootId, match } = this.props;
return rootId ? rootId : match.params.id;
return rootId ? rootId : match?.params?.id;
};
});
export default PageMainDate;
export default PageMainDate;

View file

@ -62,7 +62,7 @@ class PageMainEdit extends React.Component<I.PageComponent> {
getRootId () {
const { rootId, match } = this.props;
return rootId ? rootId : match.params.id;
return rootId ? rootId : match?.params?.id;
};
};

View file

@ -171,7 +171,7 @@ const PageMainGraph = observer(class PageMainGraph extends React.Component<I.Pag
getRootId () {
const { rootId, match } = this.props;
return this.rootId || (rootId ? rootId : match.params.id);
return this.rootId || (rootId ? rootId : match?.params?.id);
};
onTab (id: string) {

View file

@ -445,7 +445,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 {

View file

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

View file

@ -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;
@ -237,11 +236,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);
@ -321,7 +316,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 () {

View file

@ -399,8 +399,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 +467,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;
};

View file

@ -1,8 +1,7 @@
import * as React from 'react';
import { observer } from 'mobx-react';
import { Header, Footer, Loader, ListObject, Deleted, Icon } from 'Component';
import { Header, Footer, Loader, ListObject, Deleted, Icon, 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;
@ -183,11 +182,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);
};
@ -195,7 +191,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;
};
getObject () {

View file

@ -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;
@ -60,7 +58,7 @@ const PageMainSet = observer(class PageMainSet extends React.Component<I.PageCom
{check.withCover ? <Block {...this.props} key={cover.id} rootId={rootId} block={cover} /> : ''}
<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} />
<HeadSimple
{...this.props}
ref={ref => this.refHead = ref}
@ -195,11 +193,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 +202,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 () {

Some files were not shown because too many files have changed in this diff Show more