mirror of
https://github.com/anyproto/anytype-ts.git
synced 2025-06-07 21:47:02 +09:00
JS-7164: updates
This commit is contained in:
commit
b81cea9ec3
71 changed files with 1502 additions and 322 deletions
305
AGENTS.md
Normal file
305
AGENTS.md
Normal file
|
@ -0,0 +1,305 @@
|
|||
|
||||
# agents.md
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the architecture and implementation details of the Electron application built with React, TypeScript, and MobX. It covers the project's structure, state management, inter-process communication, and other essential aspects to facilitate understanding and contribution.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Project Structure](#project-structure)
|
||||
2. [State Management with MobX](#state-management-with-mobx)
|
||||
3. [Electron Integration](#electron-integration)
|
||||
4. [Inter-Process Communication (IPC)](#inter-process-communication-ipc)
|
||||
5. [Routing](#routing)
|
||||
6. [Internationalization (i18n)](#internationalization-i18n)
|
||||
7. [Testing](#testing)
|
||||
8. [Build and Packaging](#build-and-packaging)
|
||||
9. [Development Workflow](#development-workflow)
|
||||
10. [References](#references)
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
The project follows a modular structure to separate concerns and enhance maintainability:
|
||||
|
||||
```
|
||||
project-root/
|
||||
├── public/
|
||||
├── src/
|
||||
│ ├── main/ # Electron main process
|
||||
│ │ └── main.ts
|
||||
│ ├── renderer/ # React application
|
||||
│ │ ├── components/ # Reusable UI components
|
||||
│ │ ├── pages/ # Page components
|
||||
│ │ ├── stores/ # MobX stores
|
||||
│ │ ├── utils/ # Utility functions
|
||||
│ │ ├── App.tsx # Root component
|
||||
│ │ └── index.tsx # Entry point
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── webpack.config.js
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## State Management with MobX
|
||||
|
||||
MobX is utilized for state management, providing a simple and scalable solution.
|
||||
|
||||
- **Store Initialization**: Each domain has its own store class, decorated with `makeAutoObservable` to enable reactivity.
|
||||
|
||||
```typescript
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
class TodoStore {
|
||||
todos = [];
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
addTodo(todo) {
|
||||
this.todos.push(todo);
|
||||
}
|
||||
}
|
||||
|
||||
export const todoStore = new TodoStore();
|
||||
```
|
||||
|
||||
- **Context Provider**: Stores are provided to React components via Context API.
|
||||
|
||||
```typescript
|
||||
import React from 'react';
|
||||
import { todoStore } from './stores/TodoStore';
|
||||
|
||||
export const StoreContext = React.createContext({
|
||||
todoStore,
|
||||
});
|
||||
```
|
||||
|
||||
- **Usage in Components**: Components consume stores using the `useContext` hook and are wrapped with `observer`.
|
||||
|
||||
```typescript
|
||||
import React, { useContext } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { StoreContext } from '../StoreContext';
|
||||
|
||||
const TodoList = observer(() => {
|
||||
const { todoStore } = useContext(StoreContext);
|
||||
|
||||
return (
|
||||
<ul>
|
||||
{todoStore.todos.map(todo => (
|
||||
<li key={todo.id}>{todo.title}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
});
|
||||
|
||||
export default TodoList;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Electron Integration
|
||||
|
||||
Electron enables the creation of cross-platform desktop applications using web technologies.
|
||||
|
||||
- **Main Process**:
|
||||
|
||||
```typescript
|
||||
import { app, BrowserWindow } from 'electron';
|
||||
|
||||
function createWindow() {
|
||||
const win = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
},
|
||||
});
|
||||
|
||||
win.loadURL('http://localhost:3000');
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow);
|
||||
```
|
||||
|
||||
- **Preload Script**:
|
||||
|
||||
```typescript
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
contextBridge.exposeInMainWorld('api', {
|
||||
send: (channel, data) => ipcRenderer.send(channel, data),
|
||||
receive: (channel, func) => ipcRenderer.on(channel, (event, ...args) => func(...args)),
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Inter-Process Communication (IPC)
|
||||
|
||||
IPC facilitates communication between the main and renderer processes.
|
||||
|
||||
- **Renderer Process**:
|
||||
|
||||
```typescript
|
||||
window.api.send('channel-name', data);
|
||||
```
|
||||
|
||||
- **Main Process**:
|
||||
|
||||
```typescript
|
||||
ipcMain.on('channel-name', (event, data) => {
|
||||
event.reply('channel-name-response', responseData);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Routing
|
||||
|
||||
React Router is employed for client-side routing within the renderer process.
|
||||
|
||||
```typescript
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
|
||||
const App = () => (
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/about" component={AboutPage} />
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Internationalization (i18n)
|
||||
|
||||
The application supports multiple languages using `react-intl`.
|
||||
|
||||
- **Provider Setup**:
|
||||
|
||||
```typescript
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import messages_en from './translations/en.json';
|
||||
import messages_de from './translations/de.json';
|
||||
|
||||
const messages = {
|
||||
en: messages_en,
|
||||
de: messages_de,
|
||||
};
|
||||
|
||||
const language = navigator.language.split(/[-_]/)[0];
|
||||
|
||||
const App = () => (
|
||||
<IntlProvider locale={language} messages={messages[language]}>
|
||||
{/* Application components */}
|
||||
</IntlProvider>
|
||||
);
|
||||
```
|
||||
|
||||
- **Usage in Components**:
|
||||
|
||||
```typescript
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
const Greeting = () => (
|
||||
<p>
|
||||
<FormattedMessage id="app.greeting" defaultMessage="Hello, World!" />
|
||||
</p>
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
Testing ensures the reliability of the application.
|
||||
|
||||
- **Unit Testing**:
|
||||
|
||||
```typescript
|
||||
test('adds two numbers', () => {
|
||||
expect(add(2, 3)).toBe(5);
|
||||
});
|
||||
```
|
||||
|
||||
- **Component Testing**:
|
||||
|
||||
```typescript
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import TodoList from './TodoList';
|
||||
|
||||
test('renders todo items', () => {
|
||||
render(<TodoList />);
|
||||
expect(screen.getByText(/Sample Todo/i)).toBeInTheDocument();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Build and Packaging
|
||||
|
||||
- **Development Build**:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
- **Production Build**:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
- **Packaging**:
|
||||
|
||||
```bash
|
||||
npm run dist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow
|
||||
|
||||
1. **Install Dependencies**:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Start Development Server**:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
3. **Start Electron**:
|
||||
|
||||
```bash
|
||||
npm run electron
|
||||
```
|
||||
|
||||
4. **Run Tests**:
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- [Electron Documentation](https://www.electronjs.org/docs)
|
||||
- [React Documentation](https://reactjs.org/docs/getting-started.html)
|
||||
- [MobX Documentation](https://mobx.js.org/README.html)
|
||||
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||
- [React Router Documentation](https://reactrouter.com/)
|
||||
- [react-intl Documentation](https://formatjs.io/docs/react-intl/)
|
41
dist/extension/manifest.chrome.json
vendored
41
dist/extension/manifest.chrome.json
vendored
|
@ -1,41 +0,0 @@
|
|||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Anytype Web Clipper",
|
||||
"description": "Save web content to the Anytype — open, encrypted, and local-first application that connects everything as objects.",
|
||||
"version": "0.0.8",
|
||||
"icons": {
|
||||
"16": "img/icon16x16.png",
|
||||
"128": "img/icon128x128.png"
|
||||
},
|
||||
"options_page": "settings/index.html",
|
||||
"action": {
|
||||
"default_title": "Anytype Web Clipper",
|
||||
"default_popup": "popup/index.html"
|
||||
},
|
||||
"permissions": [
|
||||
"contextMenus",
|
||||
"nativeMessaging",
|
||||
"tabs",
|
||||
"scripting",
|
||||
"activeTab"
|
||||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/browser-polyfill.min.js"
|
||||
],
|
||||
"service_worker": "js/background.js"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"js": [ "js/foreground.js" ],
|
||||
"css": [ "css/foreground.css" ],
|
||||
"matches": [ "<all_urls>" ]
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
{
|
||||
"resources": [ "iframe/index.html" ],
|
||||
"matches": [ "<all_urls>" ]
|
||||
}
|
||||
]
|
||||
}
|
2
dist/extension/manifest.json
vendored
2
dist/extension/manifest.json
vendored
|
@ -29,7 +29,7 @@
|
|||
{
|
||||
"js": [
|
||||
"js/browser-polyfill.min.js",
|
||||
"js/foreground.min.js"
|
||||
"js/foreground.js"
|
||||
],
|
||||
"css": [ "css/foreground.css" ],
|
||||
"matches": [ "<all_urls>" ]
|
||||
|
|
12
electron.js
12
electron.js
|
@ -30,6 +30,10 @@ const Util = require('./electron/js/util.js');
|
|||
const Cors = require('./electron/json/cors.json');
|
||||
const csp = [];
|
||||
|
||||
let deeplinkingUrl = '';
|
||||
let waitLibraryPromise = null;
|
||||
let mainWindow = null;
|
||||
|
||||
MenuManager.store = store;
|
||||
|
||||
for (let i in Cors) {
|
||||
|
@ -42,6 +46,10 @@ app.removeAsDefaultProtocolClient(protocol);
|
|||
if (process.defaultApp) {
|
||||
if (process.argv.length >= 2) {
|
||||
app.setAsDefaultProtocolClient(protocol, process.execPath, [ path.resolve(process.argv[1]) ]);
|
||||
|
||||
if (!is.macos) {
|
||||
deeplinkingUrl = argv.find(arg => arg.startsWith(`${protocol}://`));
|
||||
};
|
||||
};
|
||||
} else {
|
||||
app.setAsDefaultProtocolClient(protocol);
|
||||
|
@ -66,10 +74,6 @@ ipcMain.on('storeDelete', (e, key) => {
|
|||
e.returnValue = store.delete(key);
|
||||
});
|
||||
|
||||
let deeplinkingUrl = '';
|
||||
let waitLibraryPromise = null;
|
||||
let mainWindow = null;
|
||||
|
||||
if (is.development && !port) {
|
||||
console.error('ERROR: Please define SERVER_PORT env var');
|
||||
Api.exit(mainWindow, '', false);
|
||||
|
|
|
@ -276,6 +276,17 @@ class Api {
|
|||
return data;
|
||||
};
|
||||
|
||||
focusWindow (win) {
|
||||
if (!win || win.isDestroyed()) {
|
||||
return;
|
||||
};
|
||||
|
||||
win.show();
|
||||
win.focus();
|
||||
win.setAlwaysOnTop(true);
|
||||
win.setAlwaysOnTop(false);
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
module.exports = new Api();
|
|
@ -18,7 +18,7 @@ const { fixPathForAsarUnpack, is } = require('electron-util');
|
|||
const APP_NAME = 'com.anytype.desktop';
|
||||
const MANIFEST_FILENAME = `${APP_NAME}.json`;
|
||||
const EXTENSION_IDS_CHROME = [ 'jbnammhjiplhpjfncnlejjjejghimdkf', 'jkmhmgghdjjbafmkgjmplhemjjnkligf', 'lcamkcmpcofgmbmloefimnelnjpcdpfn' ];
|
||||
const EXTENSION_IDS_FIREFOX = [ '6f3d2083562159a9e0a7635ee6008b1b6326202b@temporary-addon' ]
|
||||
const EXTENSION_IDS_FIREFOX = [ 'a46f8b5233f8efc536c649bd6db78cfd31312600@temporary-addon' ]
|
||||
const USER_PATH = app.getPath('userData');
|
||||
const EXE_PATH = app.getPath('exe');
|
||||
|
||||
|
@ -70,11 +70,11 @@ const installNativeMessagingHost = () => {
|
|||
const installToMacOS = (manifest) => {
|
||||
const dirs = getDarwinDirectory();
|
||||
|
||||
for (const [ key, value ] of Object.entries(dirs)) {
|
||||
if (fs.existsSync(value)) {
|
||||
writeManifest(path.join(value, 'NativeMessagingHosts', MANIFEST_FILENAME), manifest);
|
||||
for (const [ key, dir ] of Object.entries(dirs)) {
|
||||
if (fs.existsSync(dir)) {
|
||||
writeManifest(path.join(dir, 'NativeMessagingHosts', MANIFEST_FILENAME), manifest);
|
||||
} else {
|
||||
console.log('[InstallNativeMessaging] Manifest skipped:', key);
|
||||
console.log('[InstallNativeMessaging] Manifest skipped:', dir);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@ -159,12 +159,14 @@ const getLinuxDirectory = () => {
|
|||
};
|
||||
|
||||
const getDarwinDirectory = () => {
|
||||
const home = path.join(getHomeDir(), 'Library', 'Application Support');
|
||||
const lib = path.join('Library', 'Application Support');
|
||||
const home = path.join(getHomeDir(), lib);
|
||||
|
||||
/* eslint-disable no-useless-escape */
|
||||
|
||||
return {
|
||||
'Firefox': path.join(home, 'Mozilla'),
|
||||
'Firefox1': path.join(home, 'Mozilla'),
|
||||
'Firefox2': path.join('/', lib, 'Mozilla'),
|
||||
'Chrome': path.join(home, 'Google', 'Chrome'),
|
||||
'Chrome Beta': path.join(home, 'Google', 'Chrome Beta'),
|
||||
'Chrome Dev': path.join(home, 'Google', 'Chrome Dev'),
|
||||
|
|
|
@ -105,6 +105,7 @@ class WindowManager {
|
|||
if (!isChild) {
|
||||
try {
|
||||
state = windowStateKeeper({ defaultWidth: DEFAULT_WIDTH, defaultHeight: DEFAULT_HEIGHT });
|
||||
|
||||
param = Object.assign(param, {
|
||||
x: state.x,
|
||||
y: state.y,
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.41.0-rc14
|
||||
0.41.0-rc16
|
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "anytype",
|
||||
"version": "0.46.32-beta",
|
||||
"version": "0.46.34-alpha",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "anytype",
|
||||
"version": "0.46.32-beta",
|
||||
"version": "0.46.34-alpha",
|
||||
"hasInstallScript": true,
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "anytype",
|
||||
"version": "0.46.32-beta",
|
||||
"version": "0.46.34-alpha",
|
||||
"description": "Anytype",
|
||||
"main": "electron.js",
|
||||
"scripts": {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<svg width="21" height="21" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.5 8.5L10.5 12.5L6.5 8.5" stroke="#252525" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<svg width="16" height="20" viewBox="0 0 16 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 8L8 12L4 8" stroke="#B6B6B6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 182 B After Width: | Height: | Size: 170 B |
3
src/img/icon/widget/button/create.svg
Normal file
3
src/img/icon/widget/button/create.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.3818 4.95117H6.20801C5.8859 4.95117 5.63964 5.03434 5.46875 5.2002C5.29785 5.36621 5.21191 5.61816 5.21191 5.95508V13.9893C5.21191 14.3262 5.29785 14.5781 5.46875 14.7441C5.63963 14.905 5.88606 14.9854 6.20801 14.9854H14.4189C14.6579 14.9852 14.8556 14.905 15.0117 14.7441C15.168 14.5781 15.2461 14.3262 15.2461 13.9893V9.25391L16.8799 7.62012V14.1504C16.8799 14.9559 16.6746 15.5664 16.2646 15.9814C15.8595 16.4013 15.2878 16.6112 14.5508 16.6113H6.06934C5.25879 16.6113 4.64062 16.4014 4.21582 15.9814C3.7959 15.5664 3.58594 14.9561 3.58594 14.1504V5.77832C3.58602 4.97778 3.79598 4.3671 4.21582 3.94727C4.64062 3.5275 5.25894 3.31738 6.06934 3.31738H13.0156L11.3818 4.95117ZM17.0186 4.40918L10.4492 10.9795L8.91113 11.6162C8.81855 11.6503 8.73032 11.6285 8.64746 11.5508C8.56452 11.4727 8.54505 11.3847 8.58887 11.2871L9.24805 9.78516L15.8174 3.21484L17.0186 4.40918ZM17.3994 1.9043C17.5996 1.88477 17.7656 1.93848 17.8975 2.06543L18.1104 2.28516C18.2615 2.43158 18.335 2.60265 18.3301 2.79785C18.3252 2.99306 18.244 3.17096 18.0879 3.33203L17.5898 3.84473L16.3887 2.63672L16.8799 2.14551C17.0311 1.99933 17.2043 1.91893 17.3994 1.9043Z" fill="#252525"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
5
src/img/icon/widget/emptyMore.svg
Normal file
5
src/img/icon/widget/emptyMore.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 11C10.5523 11 11 10.5523 11 10C11 9.44772 10.5523 9 10 9C9.44772 9 9 9.44772 9 10C9 10.5523 9.44772 11 10 11Z" fill="#828282"/>
|
||||
<path d="M5 11C5.55228 11 6 10.5523 6 10C6 9.44772 5.55228 9 5 9C4.44772 9 4 9.44772 4 10C4 10.5523 4.44772 11 5 11Z" fill="#828282"/>
|
||||
<path d="M15 11C15.5523 11 16 10.5523 16 10C16 9.44772 15.5523 9 15 9C14.4477 9 14 9.44772 14 10C14 10.5523 14.4477 11 15 11Z" fill="#828282"/>
|
||||
</svg>
|
After Width: | Height: | Size: 523 B |
3
src/img/icon/widget/system/pin.svg
Normal file
3
src/img/icon/widget/system/pin.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.0099 8.24704C4.36293 7.89401 4.79768 7.63578 5.31415 7.47234C5.83389 7.31217 6.40103 7.25823 7.01556 7.31053C7.63336 7.35956 8.25607 7.52791 8.88367 7.81556L11.3206 5.70229C11.2127 5.31984 11.1277 4.95373 11.0656 4.60397C11.0068 4.25748 10.9659 3.94204 10.943 3.65766C10.9267 3.37327 10.9316 3.13301 10.9577 2.93689C10.9969 2.63616 11.0999 2.40244 11.2666 2.23573C11.4301 2.07229 11.6197 1.99384 11.8354 2.00038C12.0577 2.00691 12.2603 2.10171 12.4434 2.28476L17.7192 7.5606C17.899 7.74038 17.9922 7.94141 17.9987 8.16369C18.0085 8.3827 17.9317 8.57392 17.7683 8.73736C17.6015 8.90407 17.3662 9.0054 17.0622 9.04136C16.8693 9.07078 16.6291 9.07568 16.3414 9.05607C16.0603 9.03646 15.7449 8.9956 15.3951 8.93349C15.0486 8.86811 14.6825 8.78312 14.2968 8.67852L12.1835 11.1154C12.4744 11.7463 12.6444 12.3706 12.6935 12.9884C12.7458 13.603 12.6902 14.1685 12.5267 14.6849C12.3666 15.2047 12.11 15.6411 11.7569 15.9941C11.5347 16.2164 11.2846 16.3259 11.0068 16.3226C10.7354 16.3193 10.4821 16.2 10.2468 15.9647L7.62846 13.3464L4.90424 16.0706C4.37721 16.5976 3.67319 16.9148 3.27639 16.9826C3.14563 17.0087 3.06391 17.0054 3.03123 16.9728C2.99527 16.9368 2.99037 16.8534 3.01652 16.7227C3.13752 16.1177 3.49973 15.5334 3.93341 15.0997L6.65763 12.3755L4.03442 9.75232C3.80233 9.52024 3.68302 9.26691 3.67649 8.99233C3.67649 8.71775 3.78762 8.46932 4.0099 8.24704Z" fill="#252525"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
3
src/img/theme/dark/icon/widget/system/pin.svg
Normal file
3
src/img/theme/dark/icon/widget/system/pin.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.0099 8.24704C4.36293 7.89401 4.79768 7.63578 5.31415 7.47234C5.83389 7.31217 6.40103 7.25823 7.01556 7.31053C7.63336 7.35956 8.25607 7.52791 8.88367 7.81556L11.3206 5.70229C11.2127 5.31984 11.1277 4.95373 11.0656 4.60397C11.0068 4.25748 10.9659 3.94204 10.943 3.65766C10.9267 3.37327 10.9316 3.13301 10.9577 2.93689C10.9969 2.63616 11.0999 2.40244 11.2666 2.23573C11.4301 2.07229 11.6197 1.99384 11.8354 2.00038C12.0577 2.00691 12.2603 2.10171 12.4434 2.28476L17.7192 7.5606C17.899 7.74038 17.9922 7.94141 17.9987 8.16369C18.0085 8.3827 17.9317 8.57392 17.7683 8.73736C17.6015 8.90407 17.3662 9.0054 17.0622 9.04136C16.8693 9.07078 16.6291 9.07568 16.3414 9.05607C16.0603 9.03646 15.7449 8.9956 15.3951 8.93349C15.0486 8.86811 14.6825 8.78312 14.2968 8.67852L12.1835 11.1154C12.4744 11.7463 12.6444 12.3706 12.6935 12.9884C12.7458 13.603 12.6902 14.1685 12.5267 14.6849C12.3666 15.2047 12.11 15.6411 11.7569 15.9941C11.5347 16.2164 11.2846 16.3259 11.0068 16.3226C10.7354 16.3193 10.4821 16.2 10.2468 15.9647L7.62846 13.3464L4.90424 16.0706C4.37721 16.5976 3.67319 16.9148 3.27639 16.9826C3.14563 17.0087 3.06391 17.0054 3.03123 16.9728C2.99527 16.9368 2.99037 16.8534 3.01652 16.7227C3.13752 16.1177 3.49973 15.5334 3.93341 15.0997L6.65763 12.3755L4.03442 9.75232C3.80233 9.52024 3.68302 9.26691 3.67649 8.99233C3.67649 8.71775 3.78762 8.46932 4.0099 8.24704Z" fill="#f8f8f8"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -70,4 +70,5 @@ export default {
|
|||
table: [ 'select2', 'blockColor', 'blockBackground' ],
|
||||
dataviewNew: [ 'searchObject', 'typeSuggest', 'dataviewTemplateList' ],
|
||||
graphSettings: [ 'select' ],
|
||||
syncStatus: [ 'syncStatusInfo' ],
|
||||
};
|
||||
|
|
|
@ -95,8 +95,9 @@
|
|||
"commonDates": "Dates",
|
||||
"commonPreferences": "Preferences",
|
||||
"commonDuplicate": "Duplicate",
|
||||
"commonRemoveFromFavorites": "Remove from Favorites",
|
||||
"commonAddToFavorites": "Add to Favorites",
|
||||
"commonRemoveFromFavorites": "Unpin",
|
||||
"commonPin": "Pin",
|
||||
"commonUnpin": "Unpin",
|
||||
"commonAddToCollection": "Add to Collection",
|
||||
"commonRestore": "Restore",
|
||||
"commonRestoreFromBin": "Restore from Bin",
|
||||
|
@ -185,8 +186,6 @@
|
|||
"commonCalculate": "Calculate",
|
||||
"commonRelations": "Properties",
|
||||
"commonSelectType": "Select Type",
|
||||
"commonPin": "Pin on top",
|
||||
"commonUnpin": "Unpin",
|
||||
"commonOpenType": "Open Type",
|
||||
"commonManage": "Manage",
|
||||
"commonSize": "Size",
|
||||
|
@ -901,7 +900,7 @@
|
|||
"popupSettingsImportNotionTitle": "Notion Import",
|
||||
"popupSettingsImportCsvTitle": "CSV Import",
|
||||
"popupSettingsImportCsvText": "Select a <b>Zip archive or CSV file<\/b> (.csv) to complete the import",
|
||||
"popupSettingsImportFavouriteTitle": "Selected objects will appear in your Favorites widget",
|
||||
"popupSettingsImportFavouriteTitle": "Selected objects will appear in your Pinned widget",
|
||||
"popupSettingsImportHtmlTitle": "HTML Import",
|
||||
"popupSettingsImportTextTitle": "Text Import",
|
||||
"popupSettingsImportProtobufTitle": "Any-Block Import",
|
||||
|
@ -1832,6 +1831,7 @@
|
|||
|
||||
"toastChatAttachmentsLimitReached": "You can upload only %s %s at a time",
|
||||
"toastWidget": "Widget <b>%s</b> has been added",
|
||||
"toastJoinSpace": "You have joined the <b>%s</b>",
|
||||
|
||||
"textColor-grey": "Grey",
|
||||
"textColor-yellow": "Yellow",
|
||||
|
@ -1931,7 +1931,7 @@
|
|||
"widget4Name": "View",
|
||||
"widget4Description": "Same as in object view",
|
||||
|
||||
"widgetFavorite": "Favorites",
|
||||
"widgetFavorite": "Pinned",
|
||||
"widgetRecent": "Recently edited",
|
||||
"widgetRecentOpen": "Recently opened",
|
||||
"widgetSet": "Queries",
|
||||
|
@ -1941,6 +1941,7 @@
|
|||
"widgetLibrary": "Library",
|
||||
"widgetOptions": "Options",
|
||||
"widgetEmptyLabel": "There are no objects here",
|
||||
"widgetEmptyFavoriteLabel": "Add objects here using the <div class='icon more'></div> menu",
|
||||
"widgetShowAll": "See all Objects",
|
||||
"widgetItemClipboard": "Create from clipboard",
|
||||
|
||||
|
|
|
@ -183,7 +183,7 @@
|
|||
}
|
||||
|
||||
.content {
|
||||
.scroll { overflow-x: auto; overflow-y: visible; transform: translate3d(0px,0px,0px); padding-bottom: 14px; }
|
||||
.scroll { overflow-x: auto; overflow-y: visible; }
|
||||
}
|
||||
|
||||
.viewContent {
|
||||
|
|
|
@ -41,34 +41,39 @@
|
|||
.number { color: var(--color-text-tertiary); }
|
||||
}
|
||||
|
||||
.day {
|
||||
.head { display: flex; flex-direction: row; gap: 0px 8px; align-items: center; justify-content: space-between; }
|
||||
.head {
|
||||
.icon { flex-shrink: 0; width: 24px !important; height: 24px !important; opacity: 0; transition: opacity $transitionCommon; }
|
||||
.dropTarget { width: 100%; height: 100%; }
|
||||
.dropTarget.targetBot { width: calc(100% - 8px); margin: 0px auto 0px auto; }
|
||||
|
||||
.number { @include text-paragraph; text-align: right; width: 100%; }
|
||||
.number {
|
||||
.inner { display: inline-block; }
|
||||
}
|
||||
}
|
||||
.head { display: flex; flex-direction: row; gap: 0px 8px; align-items: center; justify-content: space-between; }
|
||||
.head {
|
||||
.icon { flex-shrink: 0; width: 24px !important; height: 24px !important; opacity: 0; transition: opacity $transitionCommon; }
|
||||
|
||||
.item {
|
||||
display: flex; flex-direction: row; align-items: center; gap: 0px 4px; @include text-small; @include text-overflow-nw;
|
||||
margin: 0px 0px 2px 0px; position: relative; padding: 0px 8px; border-radius: 4px;
|
||||
.number { @include text-paragraph; text-align: right; width: 100%; }
|
||||
.number {
|
||||
.inner { display: inline-block; }
|
||||
}
|
||||
.item {
|
||||
.iconObject { flex-shrink: 0; }
|
||||
.name { @include text-overflow-nw; }
|
||||
}
|
||||
.item::before {
|
||||
content: ""; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: rgba(79,79,79,0); z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.item:hover::before { background: var(--color-shape-highlight-medium); }
|
||||
|
||||
.item.more { display: block; color: var(--color-text-secondary); }
|
||||
}
|
||||
|
||||
.items { flex-grow: 1; }
|
||||
|
||||
.record {
|
||||
display: flex; flex-direction: row; align-items: center; gap: 0px 4px; @include text-small; margin: 0px 0px 2px 0px;
|
||||
position: relative; padding: 0px 4px; border-radius: 4px;
|
||||
}
|
||||
.record {
|
||||
.dropTarget { display: flex; flex-direction: row; align-items: center; gap: 0px 4px; }
|
||||
|
||||
.iconObject { flex-shrink: 0; }
|
||||
.name { @include text-overflow-nw; width: 100%; }
|
||||
}
|
||||
.record::before {
|
||||
content: ""; position: absolute; left: 0px; top: 0px; width: 100%; height: 100%; background: rgba(79,79,79,0); z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.record:hover::before { background: var(--color-shape-highlight-medium); }
|
||||
|
||||
.record.more { display: block; color: var(--color-text-secondary); }
|
||||
|
||||
.day:hover {
|
||||
.head {
|
||||
.icon { opacity: 1; }
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
.icon.plus { background-image: url('~img/icon/plus/menu1.svg'); }
|
||||
}
|
||||
|
||||
.viewContent.viewGrid { width: 100%; position: relative; padding: 0px 0px 16px 0px; }
|
||||
.viewContent.viewGrid { width: 100%; position: relative; padding: 0px; }
|
||||
.viewContent.viewGrid {
|
||||
.loadMore { padding: 10px 2px; box-shadow: 0px 1px var(--color-shape-secondary) inset; }
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
.icon.checkbox { width: 20px; height: 20px; vertical-align: middle; margin-top: -4px; background-image: url('~img/icon/dataview/checkbox0.svg'); }
|
||||
.icon.checkbox.active { background-image: url('~img/icon/dataview/checkbox1.svg'); }
|
||||
|
||||
.listColumn { padding-top: 36px; }
|
||||
.listColumn {
|
||||
.wrapMenu { display: none; }
|
||||
}
|
||||
|
@ -17,7 +16,7 @@
|
|||
.listInline {
|
||||
.bullet { width: 4px; height: 4px; border-radius: 50%; background: var(--color-text-secondary); }
|
||||
|
||||
.cell { white-space: nowrap; display: inline-flex; flex-direction: row; align-items: center; gap: 0px 2px; height: 28px; }
|
||||
.cell { white-space: nowrap; display: inline-flex; flex-direction: row; align-items: center; gap: 0px 2px; min-height: 28px; }
|
||||
.cell.canEdit {
|
||||
.cellContent {
|
||||
.empty { display: inline; }
|
||||
|
@ -45,12 +44,15 @@
|
|||
}
|
||||
|
||||
.cellContent.c-select {
|
||||
.over { display: flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 0px 6px; }
|
||||
.over {
|
||||
display: inline-flex; flex-direction: row; align-items: center; flex-wrap: wrap; gap: 0px 6px; vertical-align: middle;
|
||||
margin-top: -2px;
|
||||
}
|
||||
.tagItem { margin: 0px; max-width: 400px; }
|
||||
}
|
||||
|
||||
.cellContent.c-file {
|
||||
.over { display: flex; flex-direction: row; align-items: center; gap: 0px 2px; height: 28px; }
|
||||
.over { display: inline-flex; flex-direction: row; align-items: center; gap: 0px 2px; height: 28px; }
|
||||
.element { flex-shrink: 0; display: flex; }
|
||||
.iconObject { display: block; }
|
||||
.name { display: none; }
|
||||
|
@ -61,7 +63,7 @@
|
|||
.iconObject { flex-shrink: 0; margin: 0px 6px 0px 0px !important; }
|
||||
}
|
||||
|
||||
.cellContent.c-checkbox { display: flex; flex-direction: row; align-items: center; height: 28px; gap: 0px 4px; }
|
||||
.cellContent.c-checkbox { display: inline-flex; flex-direction: row; align-items: center; height: 28px; gap: 0px 4px; }
|
||||
|
||||
.cellContent.c-longText {
|
||||
.name { display: inline-block; vertical-align: top; max-width: 220px; @include text-overflow-nw; }
|
||||
|
|
|
@ -59,7 +59,7 @@
|
|||
}
|
||||
|
||||
.cell.isEditing.c-select {
|
||||
.cellContent, .placeholder { padding: 6px 8px 4px 8px; }
|
||||
.cellContent, .placeholder { padding: 6px 8px; }
|
||||
}
|
||||
.cell.isEditing.c-select.isSelect {
|
||||
.over { width: calc(100% - 26px); }
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
.icon.ghost { background-image: url('~img/icon/ghost.svg'); }
|
||||
|
||||
.icon.widget-star { background-image: url('~img/icon/widget/system/star.svg'); }
|
||||
.icon.widget-pin { background-image: url('~img/icon/widget/system/pin.svg'); }
|
||||
.icon.widget-chat { background-image: url('~img/icon/widget/system/chat.svg'); }
|
||||
.icon.widget-pencil { background-image: url('~img/icon/widget/system/pencil.svg'); }
|
||||
.icon.widget-eye { background-image: url('~img/icon/widget/system/eye.svg'); }
|
||||
|
|
|
@ -67,7 +67,7 @@
|
|||
|
||||
> .body { flex-grow: 1; overflow: hidden; }
|
||||
> .body {
|
||||
.ReactVirtualized__List { padding: 0px 8px 8px 8px; overscroll-behavior: none; }
|
||||
.ReactVirtualized__List { padding: 0px 8px 8px 8px; }
|
||||
|
||||
.emptySearch { height: auto; padding: 5px 16px 0px 16px; }
|
||||
.emptySearch {
|
||||
|
|
|
@ -7,9 +7,11 @@
|
|||
.menus {
|
||||
.menu.menuSyncStatus { width: var(--menu-width-large); height: var(--menu-width-large); }
|
||||
.menu.menuSyncStatus {
|
||||
.content { padding: 0px; height: 100%; max-height: unset; }
|
||||
.syncMenuWrapper { padding: 16px 0px 0px 0px; height: 100%; display: flex; flex-direction: column; }
|
||||
.syncPanel { height: 28px; padding: 0px 16px; margin-bottom: 4px; display: flex; justify-content: space-between; align-items: center; }
|
||||
.content {
|
||||
padding: 16px 0px 0px 0px; height: 100%; max-height: unset; display: flex; flex-direction: column; gap: 4px 0px;
|
||||
overflow: visible;
|
||||
}
|
||||
.syncPanel { height: 28px; padding: 0px 16px; display: flex; justify-content: space-between; align-items: center; }
|
||||
|
||||
.title { @include text-paragraph; font-weight: 600; padding: 0; color: var(--color-text-primary); }
|
||||
.ReactVirtualized__List { padding: 8px; }
|
||||
|
@ -48,7 +50,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.items { height: 100%; padding: 0px; }
|
||||
.items { height: 100%; }
|
||||
.items {
|
||||
.sectionName { padding: 4px 8px; }
|
||||
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
.pageMainRelation,
|
||||
.settingsPage.pageSettingsRelation {
|
||||
#loader { position: fixed; top: 0px; width: 100%; height: 100%; background: var(--color-bg-primary); z-index: 5; }
|
||||
|
||||
.wrapper { width: calc(100% - 96px); margin: 0px auto; padding: 60px 0px 0px 0px; user-select: none; }
|
||||
.wrapper.withIcon {
|
||||
.editorControls {
|
||||
|
@ -80,6 +82,4 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pageMainType > div > #loader { position: fixed; top: 0px; width: 100%; height: 100%; background: var(--color-bg-primary); z-index: 5; }
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
.popups {
|
||||
.popup { position: fixed; left: 0px; top: 0px; width: 100%; height: 100%; z-index: 101; }
|
||||
.popup.show {
|
||||
.innerWrap { opacity: 1; transform: scale3d(1,1,1); }
|
||||
.innerWrap { opacity: 1; transform: none; }
|
||||
.dimmer { opacity: 1; -webkit-app-region: no-drag; }
|
||||
}
|
||||
.popup.showDimmer {
|
||||
|
@ -15,7 +15,7 @@
|
|||
.popup {
|
||||
.innerWrap {
|
||||
position: absolute; left: 0px; top: 50%; z-index: 1; background: var(--color-bg-primary); border-radius: 12px; box-shadow: 0px 2px 28px rgba(0, 0, 0, 0.2);
|
||||
opacity: 0; transform: scale3d(0.95,0.95,1); transition-duration: 0.15s; transition-property: transform, opacity; transition-timing-function: $easeInQuint;
|
||||
opacity: 0; transform: scale(0.95,0.95); transition-duration: 0.15s; transition-property: transform, opacity; transition-timing-function: $easeInQuint;
|
||||
overflow-x: hidden; overflow-y: auto; overscroll-behavior: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -118,6 +118,7 @@ html.themeDark {
|
|||
.icon.import-roam { background-image: url('#{$themePath}/icon/import/roam.svg'); }
|
||||
|
||||
.icon.widget-star { background-image: url('#{$themePath}/icon/widget/system/star.svg'); }
|
||||
.icon.widget-pin { background-image: url('#{$themePath}/icon/widget/system/pin.svg'); }
|
||||
.icon.widget-chat { background-image: url('#{$themePath}/icon/widget/system/chat.svg'); }
|
||||
.icon.widget-pencil { background-image: url('#{$themePath}/icon/widget/system/pencil.svg'); }
|
||||
.icon.widget-eye { background-image: url('#{$themePath}/icon/widget/system/eye.svg'); }
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
.widget { background: var(--color-bg-primary); border-radius: 12px; transform: translate(0px, 0px); position: relative; }
|
||||
.widget:last-child { margin: 0px; }
|
||||
.widget {
|
||||
.ReactVirtualized__List { overscroll-behavior: none; }
|
||||
|
||||
.head { padding: 8px; }
|
||||
.head {
|
||||
.sides { width: 100%; position: relative; display: flex; flex-direction: row; align-items: center; gap: 0px 8px; padding: 2px 4px 2px 8px; }
|
||||
|
@ -72,7 +70,8 @@
|
|||
|
||||
.emptyWrap { padding: 0px 16px; display: flex; align-items: center; justify-content: center; }
|
||||
.emptyWrap {
|
||||
.label { text-align: center; @include text-common; white-space: nowrap; }
|
||||
.label { text-align: center; @include text-common; white-space: nowrap; color: var(--color-text-secondary); }
|
||||
.icon.more { width: 20px; height: 20px; background-image: url('~img/icon/widget/emptyMore.svg'); }
|
||||
}
|
||||
|
||||
.icon.remove { position: absolute; top: -15px; left: -15px; height: 40px; width: 40px; display: none; z-index: 2; cursor: default !important; }
|
||||
|
|
|
@ -24,17 +24,16 @@
|
|||
|
||||
.side.right { flex-shrink: 0; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 0px 4px; }
|
||||
.side.right {
|
||||
.icon { width: 28px !important; height: 28px !important; flex-shrink: 0; }
|
||||
.icon.search { background-image: url('~img/icon/widget/button/search.svg'); }
|
||||
|
||||
.plusWrapper { box-shadow: 0px 0px 0px 1px var(--color-shape-secondary) inset; border-radius: 6px; }
|
||||
.plusWrapper {
|
||||
.icon:first-child { border-radius: 6px 0px 0px 6px; }
|
||||
.icon:last-child { border-radius: 0px 6px 6px 0px; }
|
||||
}
|
||||
|
||||
.icon.plus { background-image: url('~img/icon/widget/button/plus.svg'); border-right: 1px solid var(--color-shape-secondary); }
|
||||
.icon.arrow { width: 23px !important; background-image: url('~img/icon/widget/button/arrow.svg'); }
|
||||
.icon { width: 28px !important; height: 28px !important; flex-shrink: 0; }
|
||||
.icon.search { background-image: url('~img/icon/widget/button/search.svg'); }
|
||||
.icon.plus { background-image: url('~img/icon/widget/button/create.svg'); border-right: 1px solid var(--color-shape-secondary); }
|
||||
.icon.arrow { width: 22px !important; background-size: 16px 20px !important; background-image: url('~img/icon/widget/button/arrow.svg'); }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -971,6 +971,10 @@ const BlockDataview = observer(class BlockDataview extends React.Component<Props
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (e.ctrlKey) {
|
||||
return;
|
||||
};
|
||||
|
||||
subId = subId || this.getSubId();
|
||||
|
||||
const { block } = this.props;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as React from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { IconObject, ObjectName, Icon } from 'Component';
|
||||
import { I, S, U, translate, Preview } from 'Lib';
|
||||
import { IconObject, ObjectName, Icon, DropTarget } from 'Component';
|
||||
import { I, S, U, C, translate, Preview } from 'Lib';
|
||||
|
||||
interface Props extends I.ViewComponent {
|
||||
d: number;
|
||||
|
@ -27,16 +27,20 @@ const Item = observer(class Item extends React.Component<Props> {
|
|||
this.onContext = this.onContext.bind(this);
|
||||
this.canCreate = this.canCreate.bind(this);
|
||||
this.onCreate = this.onCreate.bind(this);
|
||||
this.onDragStart = this.onDragStart.bind(this);
|
||||
this.onRecordDrop = this.onRecordDrop.bind(this);
|
||||
};
|
||||
|
||||
render () {
|
||||
const { items, className, d, m, y, getView, onContext } = this.props;
|
||||
const { rootId, items, className, d, m, y, getView, onContext } = this.props;
|
||||
const view = getView();
|
||||
const { hideIcon } = view;
|
||||
const slice = items.slice(0, LIMIT);
|
||||
const length = items.length;
|
||||
const cn = [ 'day' ];
|
||||
const canCreate = this.canCreate();
|
||||
const relation = S.Record.getRelationByKey(view.groupRelationKey);
|
||||
const canDrag = relation && !relation.isReadonlyValue;
|
||||
|
||||
if (className) {
|
||||
cn.push(className);
|
||||
|
@ -45,7 +49,7 @@ const Item = observer(class Item extends React.Component<Props> {
|
|||
let more = null;
|
||||
if (length > LIMIT) {
|
||||
more = (
|
||||
<div className="item more" onClick={this.onMore}>
|
||||
<div className="record more" onClick={this.onMore}>
|
||||
+{length - LIMIT} {translate('commonMore')} {U.Common.plural(length, translate('pluralObject')).toLowerCase()}
|
||||
</div>
|
||||
);
|
||||
|
@ -55,16 +59,33 @@ const Item = observer(class Item extends React.Component<Props> {
|
|||
const canEdit = !item.isReadonly && S.Block.isAllowed(item.restrictions, [ I.RestrictionObject.Details ]) && U.Object.isTaskLayout(item.layout);
|
||||
const icon = hideIcon ? null : <IconObject id={`item-${item.id}-icon`} object={item} size={16} canEdit={canEdit} />;
|
||||
|
||||
let content = (
|
||||
<>
|
||||
{icon}
|
||||
<ObjectName object={item} onClick={() => this.onOpen(item)} />
|
||||
</>
|
||||
);
|
||||
|
||||
if (canDrag) {
|
||||
content = (
|
||||
<DropTarget {...this.props} rootId={rootId} id={item.id} dropType={I.DropType.Record} viewType={view.type}>
|
||||
{content}
|
||||
</DropTarget>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div
|
||||
id={`item-${item.id}`}
|
||||
className="item"
|
||||
id={`record-${item.id}`}
|
||||
className="record"
|
||||
draggable={canDrag}
|
||||
onContextMenu={e => onContext(e, item.id)}
|
||||
onMouseEnter={e => this.onMouseEnter(e, item)}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
onDragStart={e => this.onDragStart(e, item)}
|
||||
>
|
||||
{icon}
|
||||
<ObjectName object={item} onClick={() => this.onOpen(item)} />
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -96,6 +117,8 @@ const Item = observer(class Item extends React.Component<Props> {
|
|||
))}
|
||||
|
||||
{more}
|
||||
|
||||
<DropTarget {...this.props} rootId={rootId} id={[ 'empty', y, m, d ].join('-')} isTargetBottom={true} dropType={I.DropType.Record} viewType={view.type} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -107,7 +130,7 @@ const Item = observer(class Item extends React.Component<Props> {
|
|||
|
||||
onMouseEnter (e: any, item: any) {
|
||||
const node = $(this.node);
|
||||
const element = node.find(`#item-${item.id}`);
|
||||
const element = node.find(`#record-${item.id}`);
|
||||
const name = U.Common.shorten(item.name, 50);
|
||||
|
||||
Preview.tooltipShow({ text: name, element });
|
||||
|
@ -207,6 +230,38 @@ const Item = observer(class Item extends React.Component<Props> {
|
|||
return groupRelation && (!groupRelation.isReadonlyValue || isToday) && isAllowedObject();
|
||||
};
|
||||
|
||||
onDragStart (e: any, item: any) {
|
||||
const dragProvider = S.Common.getRef('dragProvider');
|
||||
|
||||
dragProvider?.onDragStart(e, I.DropType.Record, [ item.id ], this);
|
||||
};
|
||||
|
||||
onRecordDrop (targetId: string, ids: [], position: I.BlockPosition) {
|
||||
const { getSubId, getView } = this.props;
|
||||
const subId = getSubId();
|
||||
const view = getView();
|
||||
|
||||
let value = 0;
|
||||
|
||||
if (targetId.match(/^empty-/)) {
|
||||
const [ , y, m, d ] = targetId.split('-').map(Number);
|
||||
|
||||
value = U.Date.timestamp(y, m, d, 0, 0, 0);
|
||||
} else {
|
||||
const records = S.Record.getRecords(subId);
|
||||
const target = records.find(r => r.id == targetId);
|
||||
if (!target) {
|
||||
return;
|
||||
};
|
||||
|
||||
value = target[view.groupRelationKey] + (position == I.BlockPosition.Bottom ? 1 : 0);
|
||||
};
|
||||
|
||||
if (value) {
|
||||
C.ObjectListSetDetails(ids, [ { key: view.groupRelationKey, value } ]);
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
export default Item;
|
||||
|
|
|
@ -43,7 +43,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props
|
|||
};
|
||||
|
||||
render () {
|
||||
const { rootId, block, size, iconSize, isPopup } = this.props;
|
||||
const { rootId, block, size, iconSize, isPopup, readonly } = this.props;
|
||||
const allowedValue = S.Block.checkFlags(rootId, rootId, [ I.RestrictionObject.Details ]);
|
||||
const items = this.getItems();
|
||||
const object = this.getObject();
|
||||
|
@ -60,24 +60,29 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component<Props
|
|||
>
|
||||
{headerRelationsLayout == I.FeaturedRelationLayout.Column ? (
|
||||
<div className="listColumn">
|
||||
{items.map((relation: any) => (
|
||||
<Block
|
||||
{...this.props}
|
||||
key={relation.id}
|
||||
rootId={rootId}
|
||||
block={new M.Block({ id: relation.id, type: I.BlockType.Relation, content: { key: relation.relationKey } })}
|
||||
readonly={!allowedValue}
|
||||
isSelectionDisabled={true}
|
||||
isContextMenuDisabled={true}
|
||||
/>
|
||||
))}
|
||||
{items.map((relation: any) => {
|
||||
const value = object[relation.relationKey];
|
||||
const canEdit = !readonly && allowedValue && !relation.isReadonlyValue;
|
||||
|
||||
return (
|
||||
<Block
|
||||
{...this.props}
|
||||
key={relation.id}
|
||||
rootId={rootId}
|
||||
block={new M.Block({ id: relation.id, type: I.BlockType.Relation, content: { key: relation.relationKey } })}
|
||||
readonly={!canEdit}
|
||||
isSelectionDisabled={true}
|
||||
isContextMenuDisabled={true}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="listInline">
|
||||
{items.map((relation: any, i: any) => {
|
||||
const id = Relation.cellId(PREFIX, relation.relationKey, object.id);
|
||||
const value = object[relation.relationKey];
|
||||
const canEdit = allowedValue && !relation.isReadonlyValue;
|
||||
const canEdit = !readonly && allowedValue && !relation.isReadonlyValue;
|
||||
const cn = [ 'cell', (canEdit ? 'canEdit' : '') ];
|
||||
|
||||
if (i == items.length - 1) {
|
||||
|
|
|
@ -544,6 +544,10 @@ const Block = observer(class Block extends React.Component<Props> {
|
|||
const { rootId, block, readonly, isContextMenuDisabled } = this.props;
|
||||
const selection = S.Common.getRef('selectionProvider');
|
||||
|
||||
if (e.ctrlKey) {
|
||||
return;
|
||||
};
|
||||
|
||||
if (
|
||||
isContextMenuDisabled ||
|
||||
readonly ||
|
||||
|
|
|
@ -960,13 +960,21 @@ const BlockText = observer(class BlockText extends React.Component<Props> {
|
|||
e.persist();
|
||||
|
||||
this.placeholderCheck();
|
||||
this.setValue(block.getText());
|
||||
|
||||
keyboard.setFocus(true);
|
||||
|
||||
if (onFocus) {
|
||||
onFocus(e);
|
||||
};
|
||||
|
||||
// Workaround for focus issue and Latex rendering
|
||||
window.setTimeout(() => {
|
||||
const range = this.getRange();
|
||||
|
||||
this.setValue(block.getText());
|
||||
|
||||
focus.set(block.id, range);
|
||||
focus.apply();
|
||||
}, 0);
|
||||
};
|
||||
|
||||
onBlur (e: any) {
|
||||
|
|
|
@ -54,6 +54,7 @@ const DragProvider = observer(forwardRef<DragProviderRefProps, Props>((props, re
|
|||
type: item.attr('data-type'),
|
||||
style: item.attr('data-style'),
|
||||
targetContextId: item.attr('data-target-context-id'),
|
||||
viewType: item.attr('data-view-type'),
|
||||
};
|
||||
const offset = item.offset();
|
||||
const rect = el.getBoundingClientRect() as DOMRect;
|
||||
|
@ -428,7 +429,7 @@ const DragProvider = observer(forwardRef<DragProviderRefProps, Props>((props, re
|
|||
const { onRecordDrop } = origin.current;
|
||||
|
||||
if (onRecordDrop) {
|
||||
onRecordDrop(targetId, ids);
|
||||
onRecordDrop(targetId, ids, position);
|
||||
};
|
||||
break;
|
||||
};
|
||||
|
|
|
@ -9,6 +9,7 @@ interface Props {
|
|||
style?: number;
|
||||
type?: I.BlockType;
|
||||
dropType: I.DropType;
|
||||
viewType?: I.ViewType;
|
||||
className?: string;
|
||||
canDropMiddle?: boolean;
|
||||
isTargetTop?: boolean;
|
||||
|
@ -26,6 +27,7 @@ const DropTarget: FC<Props> = ({
|
|||
cacheKey = '',
|
||||
targetContextId = '',
|
||||
dropType = I.DropType.None,
|
||||
viewType = null,
|
||||
type,
|
||||
style = 0,
|
||||
className = '',
|
||||
|
@ -71,6 +73,7 @@ const DropTarget: FC<Props> = ({
|
|||
'drop-type': dropType,
|
||||
'context-id': targetContextId,
|
||||
'drop-middle': Number(canDropMiddle) || 0,
|
||||
'view-type': Number(viewType) || 0,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -25,7 +25,7 @@ const SNAP = 0.025;
|
|||
const DragHorizontal = forwardRef<DragHorizontalRefProps, Props>(({
|
||||
id = '',
|
||||
className = '',
|
||||
value: initalValue = 0,
|
||||
value: initialValue = 0,
|
||||
snaps = [],
|
||||
strictSnap = false,
|
||||
iconIsOutside = false,
|
||||
|
@ -34,7 +34,7 @@ const DragHorizontal = forwardRef<DragHorizontalRefProps, Props>(({
|
|||
onMove,
|
||||
onEnd,
|
||||
}, ref) => {
|
||||
let value = initalValue;
|
||||
let value = initialValue;
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
const iconRef = useRef(null);
|
||||
|
@ -154,7 +154,7 @@ const DragHorizontal = forwardRef<DragHorizontalRefProps, Props>(({
|
|||
$(nodeRef.current).removeClass('isDragging');
|
||||
};
|
||||
|
||||
useEffect(() => setValue(initalValue), []);
|
||||
useEffect(() => setValue(initialValue), []);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getValue,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React, { forwardRef, useRef, useEffect, useImperativeHandle } from 'react';
|
||||
import { I, S, U, J, Renderer, keyboard, sidebar, Preview, translate } from 'Lib';
|
||||
import $ from 'jquery';
|
||||
import raf from 'raf';
|
||||
import { I, S, U, Renderer, keyboard, sidebar, Preview, translate } from 'Lib';
|
||||
import { Icon } from 'Component';
|
||||
|
||||
import HeaderAuthIndex from './auth';
|
||||
|
@ -41,9 +43,13 @@ const Header = forwardRef<{}, Props>((props, ref) => {
|
|||
onTab,
|
||||
} = props;
|
||||
|
||||
const nodeRef = useRef(null);
|
||||
const childRef = useRef(null);
|
||||
const Component = Components[component] || null;
|
||||
const cn = [ 'header', component, className ];
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
raf(() => resize());
|
||||
});
|
||||
|
||||
if (![ 'authIndex' ].includes(component)) {
|
||||
cn.push('isCommon');
|
||||
|
@ -159,6 +165,27 @@ const Header = forwardRef<{}, Props>((props, ref) => {
|
|||
return (isPopup ? '.popup' : '') + ' .header';
|
||||
};
|
||||
|
||||
const resize = () => {
|
||||
const node = $(nodeRef.current);
|
||||
const path = node.find('.path');
|
||||
|
||||
node.toggleClass('isSmall', path.outerWidth() < 180);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (nodeRef.current) {
|
||||
resizeObserver.observe(nodeRef.current);
|
||||
};
|
||||
|
||||
resize();
|
||||
|
||||
return () => {
|
||||
if (nodeRef.current) {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
sidebar.resizePage(null, null, false);
|
||||
});
|
||||
|
@ -180,6 +207,7 @@ const Header = forwardRef<{}, Props>((props, ref) => {
|
|||
return (
|
||||
<div
|
||||
id="header"
|
||||
ref={nodeRef}
|
||||
className={cn.join(' ')}
|
||||
onDoubleClick={onDoubleClick}
|
||||
>
|
||||
|
|
|
@ -39,7 +39,7 @@ const HeaderMainObject = observer(forwardRef<{}, I.HeaderComponent>((props, ref)
|
|||
if (isLocked) {
|
||||
label = translate('headerObjectLocked');
|
||||
} else
|
||||
if (U.Object.isTypeOrRelationLayout(object.layout) && !S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Delete ])) {
|
||||
if (U.Object.isTypeOrRelationLayout(object.layout) && !S.Block.isAllowed(object.restrictions, [ I.RestrictionObject.Delete ], true)) {
|
||||
label = translate('commonSystem');
|
||||
};
|
||||
|
||||
|
|
|
@ -52,6 +52,7 @@ const MenuTemplateList = observer(class MenuTemplateList extends React.Component
|
|||
} else {
|
||||
content = (
|
||||
<PreviewObject
|
||||
key={`preview-${item.id}`}
|
||||
rootId={item.id}
|
||||
size={previewSize}
|
||||
onMore={onMore}
|
||||
|
|
|
@ -106,7 +106,7 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
|
||||
let archive = null;
|
||||
let remove = null;
|
||||
let fav = null;
|
||||
let pin = null;
|
||||
let pageLock = null;
|
||||
let template = null;
|
||||
let setDefaultTemplate = null;
|
||||
|
@ -137,9 +137,9 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
};
|
||||
|
||||
if (object.isFavorite) {
|
||||
fav = { id: 'unfav', name: translate('commonRemoveFromFavorites') };
|
||||
pin = { id: 'unpin', icon: 'pin', name: translate('commonUnpin') };
|
||||
} else {
|
||||
fav = { id: 'fav', name: translate('commonAddToFavorites') };
|
||||
pin = { id: 'pin', name: translate('commonPin') };
|
||||
};
|
||||
|
||||
if (block) {
|
||||
|
@ -170,7 +170,7 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
const allowedArchive = canWrite && canDelete;
|
||||
const allowedSearch = !isFilePreview && !isInSetLayouts;
|
||||
const allowedHistory = !object.isArchived && !isInFileOrSystemLayouts && !isParticipant && !isDate && !object.templateIsBundled;
|
||||
const allowedFav = canWrite && !object.isArchived && !object.templateIsBundled;
|
||||
const allowedPin = canWrite && !object.isArchived && !object.templateIsBundled;
|
||||
const allowedLock = canWrite && !object.isArchived && S.Block.checkFlags(rootId, rootId, [ I.RestrictionObject.Details ]) && !isInFileOrSystemLayouts;
|
||||
const allowedLinkTo = canWrite && !object.isArchived;
|
||||
const allowedAddCollection = canWrite && !object.isArchived && !isTemplate;
|
||||
|
@ -196,7 +196,7 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
if (!allowedReload) pageReload = null;
|
||||
if (!allowedSearch) search = null;
|
||||
if (!allowedHistory) history = null;
|
||||
if (!allowedFav) fav = null;
|
||||
if (!allowedPin) pin = null;
|
||||
if (!isTemplate && !allowedTemplate) template = null;
|
||||
if (!allowedWidget) createWidget = null;
|
||||
if (!allowedLinkTo) linkTo = null;
|
||||
|
@ -229,7 +229,7 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
|
||||
sections = sections.concat([
|
||||
{ children: [ openObject ] },
|
||||
{ children: [ createWidget, fav, pageLock, history ] },
|
||||
{ children: [ createWidget, pin, pageLock, history ] },
|
||||
{ children: [ linkTo, addCollection, template, pageLink ] },
|
||||
{ children: [ search, pageCopy, archive, remove ] },
|
||||
{ children: [ print ] },
|
||||
|
@ -252,7 +252,7 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
} else {
|
||||
sections = sections.concat([
|
||||
{ children: [ openObject ] },
|
||||
{ children: [ createWidget, fav, pageLock ] },
|
||||
{ children: [ createWidget, pin, pageLock ] },
|
||||
{ children: [ linkTo, addCollection, template, pageLink ] },
|
||||
{ children: [ search, history, pageCopy, archive ] },
|
||||
{ children: [ pageReload ] },
|
||||
|
@ -527,13 +527,9 @@ class MenuObject extends React.Component<I.Menu> {
|
|||
break;
|
||||
};
|
||||
|
||||
case 'fav': {
|
||||
Action.setIsFavorite([ rootId ], true, route);
|
||||
break;
|
||||
};
|
||||
|
||||
case 'unfav': {
|
||||
Action.setIsFavorite([ rootId ], false, route);
|
||||
case 'pin':
|
||||
case 'unpin': {
|
||||
Action.setIsFavorite([ rootId ], item.id == 'pin', route);
|
||||
break;
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import $ from 'jquery';
|
|||
import { observer } from 'mobx-react';
|
||||
import { AutoSizer, CellMeasurer, InfiniteLoader, List, CellMeasurerCache } from 'react-virtualized';
|
||||
import { Title, Icon, IconObject, ObjectName, EmptySearch } from 'Component';
|
||||
import { I, C, S, U, J, Action, translate, analytics, Onboarding } from 'Lib';
|
||||
import { I, S, U, J, Action, translate, analytics, Onboarding } from 'Lib';
|
||||
|
||||
interface State {
|
||||
isLoading: boolean;
|
||||
|
@ -11,15 +11,11 @@ interface State {
|
|||
|
||||
const HEIGHT_SECTION = 26;
|
||||
const HEIGHT_ITEM = 28;
|
||||
const LIMIT_HEIGHT = 12;
|
||||
const SUB_ID = 'syncStatusObjectsList';
|
||||
|
||||
const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.Menu, State> {
|
||||
|
||||
_isMounted = false;
|
||||
node = null;
|
||||
cache: any = {};
|
||||
items: any[] = [];
|
||||
currentInfo = '';
|
||||
state = {
|
||||
isLoading: false,
|
||||
|
@ -121,7 +117,7 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
};
|
||||
|
||||
return (
|
||||
<div ref={ref => this.node = ref} className="syncMenuWrapper" onClick={this.onCloseInfo}>
|
||||
<>
|
||||
<div className="syncPanel">
|
||||
<Title text={translate('menuSyncStatusTitle')} />
|
||||
|
||||
|
@ -139,7 +135,7 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
<InfiniteLoader
|
||||
rowCount={items.length}
|
||||
isRowLoaded={({ index }) => !!items[index]}
|
||||
threshold={LIMIT_HEIGHT}
|
||||
threshold={20}
|
||||
loadMoreRows={() => {}}
|
||||
>
|
||||
{({ onRowsRendered }) => (
|
||||
|
@ -162,32 +158,45 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
</InfiniteLoader>
|
||||
</div>
|
||||
) : ''}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
componentDidMount () {
|
||||
this._isMounted = true;
|
||||
this.load();
|
||||
this.rebind();
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
this._isMounted = false;
|
||||
this.onCloseInfo();
|
||||
this.unbind();
|
||||
|
||||
U.Subscription.destroyList([ SUB_ID ]);
|
||||
};
|
||||
|
||||
rebind () {
|
||||
const { getId } = this.props;
|
||||
|
||||
this.unbind();
|
||||
$(window).on('keydown.menu', e => this.props.onKeyDown(e));
|
||||
$(`#${getId()}`).on('click', () => this.onCloseInfo());
|
||||
};
|
||||
|
||||
unbind () {
|
||||
const { getId } = this.props;
|
||||
|
||||
$(window).off('keydown.menu');
|
||||
$(`#${getId()}`).off('click');
|
||||
};
|
||||
|
||||
onContextMenu (e, item) {
|
||||
e.stopPropagation();
|
||||
|
||||
const { param } = this.props;
|
||||
const { getId, param } = this.props;
|
||||
const { classNameWrap } = param;
|
||||
const canWrite = U.Space.canMyParticipantWrite();
|
||||
const canDelete = S.Block.isAllowed(item.restrictions, [ I.RestrictionObject.Delete ]);
|
||||
const element = $(e.currentTarget);
|
||||
const node = $(this.node);
|
||||
const itemElement = node.find(`#item-${item.id}`);
|
||||
const itemElement = $(`#${getId()} #item-${item.id}`);
|
||||
const options: any[] = [
|
||||
{ id: 'open', name: translate('commonOpen') }
|
||||
];
|
||||
|
@ -278,7 +287,7 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
sorts,
|
||||
keys: U.Subscription.syncStatusRelationKeys(),
|
||||
offset: 0,
|
||||
limit: 30,
|
||||
limit: 11,
|
||||
}, () => {
|
||||
this.setState({ isLoading: false });
|
||||
|
||||
|
@ -339,13 +348,9 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
};
|
||||
|
||||
getIconNetwork (syncStatus) {
|
||||
let { network, error, syncingCounter, status } = syncStatus;
|
||||
const { network, syncingCounter, error, status } = syncStatus;
|
||||
const buttons: any[] = [];
|
||||
|
||||
error = 1;
|
||||
status = 2;
|
||||
|
||||
|
||||
let id = '';
|
||||
let title = '';
|
||||
let className = '';
|
||||
|
@ -374,7 +379,6 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
className = 'error';
|
||||
break;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
switch (network) {
|
||||
|
@ -453,4 +457,4 @@ const MenuSyncStatus = observer(class MenuSyncStatus extends React.Component<I.M
|
|||
|
||||
});
|
||||
|
||||
export default MenuSyncStatus;
|
||||
export default MenuSyncStatus;
|
|
@ -1,6 +1,6 @@
|
|||
import React, { forwardRef, useRef, useState, useImperativeHandle, useEffect } from 'react';
|
||||
import { Loader, Title, Error, Frame, Button, Footer } from 'Component';
|
||||
import { I, C, S, U, J, translate, analytics } from 'Lib';
|
||||
import { I, C, S, U, J, translate, Preview } from 'Lib';
|
||||
|
||||
interface PageMainInviteRefProps {
|
||||
resize: () => void;
|
||||
|
@ -104,7 +104,13 @@ const PageMainInvite = forwardRef<PageMainInviteRefProps, I.PageComponent>((prop
|
|||
text: U.Common.sprintf(translate('popupConfirmJoinSpaceText'), spaceName, creatorName),
|
||||
textConfirm: translate('popupConfirmJoinSpaceButtonConfirm'),
|
||||
onConfirm: () => {
|
||||
C.SpaceJoin(account.info.networkId, message.spaceId, cid, key);
|
||||
C.SpaceJoin(account.info.networkId, message.spaceId, cid, key, () => {
|
||||
if (message.error.code) {
|
||||
setError(message.error.description);
|
||||
} else {
|
||||
Preview.toastShow({ text: U.Common.sprintf(translate('toastJoinSpace'), spaceName) });
|
||||
};
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -487,7 +487,7 @@ const PopupSearch = observer(class PopupSearch extends React.Component<I.Popup,
|
|||
|
||||
window.clearTimeout(this.timeout);
|
||||
|
||||
if (v && (this.filter == v)) {
|
||||
if (this.filter == v) {
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ const PreviewObject = observer(forwardRef<{}, Props>(({
|
|||
|
||||
const nodeRef = useRef(null);
|
||||
const idRef = useRef('');
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ dummy, setDummy ] = useState(0);
|
||||
|
||||
let n = 0;
|
||||
let c = 0;
|
||||
|
@ -299,19 +299,18 @@ const PreviewObject = observer(forwardRef<{}, Props>(({
|
|||
const load = () => {
|
||||
const contextId = getRootId();
|
||||
|
||||
if (isLoading || (idRef.current == rootId)) {
|
||||
if (idRef.current == rootId) {
|
||||
return;
|
||||
};
|
||||
|
||||
idRef.current = rootId;
|
||||
setIsLoading(true);
|
||||
|
||||
C.ObjectShow(rootId, TRACE_ID, U.Router.getRouteSpaceId(), () => {
|
||||
if (setObject) {
|
||||
setObject(S.Detail.get(contextId, rootId, []));
|
||||
};
|
||||
|
||||
setIsLoading(false);
|
||||
setDummy(dummy + 1);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -340,10 +339,10 @@ const PreviewObject = observer(forwardRef<{}, Props>(({
|
|||
const object = S.Detail.get(contextId, rootId);
|
||||
const { name, description, coverType, coverId, coverX, coverY, coverScale, iconImage } = object;
|
||||
const childBlocks = S.Block.getChildren(contextId, rootId, it => !it.isLayoutHeader()).slice(0, 10);
|
||||
const isTask = U.Object.isTaskLayout(object.layout);
|
||||
const isBookmark = U.Object.isBookmarkLayout(object.layou);
|
||||
const isTask = U.Object.isTaskLayout(check.layout);
|
||||
const isBookmark = U.Object.isBookmarkLayout(check.layou);
|
||||
const cn = [ 'previewObject' , check.className, className ];
|
||||
const withName = !U.Object.isNoteLayout(object.layout);
|
||||
const withName = !U.Object.isNoteLayout(check.layout);
|
||||
const withIcon = check.withIcon || isTask || isBookmark;
|
||||
|
||||
switch (size) {
|
||||
|
@ -367,6 +366,53 @@ const PreviewObject = observer(forwardRef<{}, Props>(({
|
|||
};
|
||||
};
|
||||
|
||||
let content = null;
|
||||
|
||||
if (!object._empty_) {
|
||||
content = (
|
||||
<>
|
||||
{object.templateIsBundled ? <Icon className="logo" tooltipParam={{ text: translate('previewObjectTemplateIsBundled') }} /> : ''}
|
||||
|
||||
{(coverType != I.CoverType.None) && coverId ? (
|
||||
<Cover
|
||||
type={coverType}
|
||||
id={coverId}
|
||||
image={coverId}
|
||||
className={coverId}
|
||||
x={coverX}
|
||||
y={coverY}
|
||||
scale={coverScale}
|
||||
withScale={true}
|
||||
/>
|
||||
) : ''}
|
||||
|
||||
<div className="heading">
|
||||
{withIcon ? <IconObject size={iconSize} object={object} /> : ''}
|
||||
{withName ? <ObjectName object={object} /> : ''}
|
||||
<div className="featured" />
|
||||
</div>
|
||||
|
||||
<div className="blocks">
|
||||
{childBlocks.map((child: any, i: number) => {
|
||||
const cn = [ n % 2 == 0 ? 'even' : 'odd' ];
|
||||
|
||||
if (i == 0) {
|
||||
cn.push('first');
|
||||
};
|
||||
|
||||
if (i == childBlocks.length - 1) {
|
||||
cn.push('last');
|
||||
};
|
||||
|
||||
n++;
|
||||
n = checkNumber(child, n);
|
||||
return <Block key={child.id} className={cn.join(' ')} {...child} />;
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
cn.push(cnPreviewSize);
|
||||
|
||||
if (isTask || isBookmark) {
|
||||
|
@ -406,59 +452,18 @@ const PreviewObject = observer(forwardRef<{}, Props>(({
|
|||
onMouseEnter={onMouseEnterHandler}
|
||||
onMouseLeave={onMouseLeaveHandler}
|
||||
>
|
||||
{isLoading ? <Loader /> : (
|
||||
<>
|
||||
{onMore ? (
|
||||
<div id={`item-more-${rootId}`} className="moreWrapper" onClick={onMore}>
|
||||
<Icon />
|
||||
</div>
|
||||
) : ''}
|
||||
{onMore ? (
|
||||
<div id={`item-more-${rootId}`} className="moreWrapper" onClick={onMore}>
|
||||
<Icon />
|
||||
</div>
|
||||
) : ''}
|
||||
|
||||
<div onClick={onClick} onContextMenu={onContextMenu}>
|
||||
<div className="scroller">
|
||||
{object.templateIsBundled ? <Icon className="logo" tooltipParam={{ text: translate('previewObjectTemplateIsBundled') }} /> : ''}
|
||||
|
||||
{(coverType != I.CoverType.None) && coverId ? (
|
||||
<Cover
|
||||
type={coverType}
|
||||
id={coverId}
|
||||
image={coverId}
|
||||
className={coverId}
|
||||
x={coverX}
|
||||
y={coverY}
|
||||
scale={coverScale}
|
||||
withScale={true}
|
||||
/>
|
||||
) : ''}
|
||||
|
||||
<div className="heading">
|
||||
{withIcon ? <IconObject size={iconSize} object={object} /> : ''}
|
||||
{withName ? <ObjectName object={object} /> : ''}
|
||||
<div className="featured" />
|
||||
</div>
|
||||
|
||||
<div className="blocks">
|
||||
{childBlocks.map((child: any, i: number) => {
|
||||
const cn = [ n % 2 == 0 ? 'even' : 'odd' ];
|
||||
|
||||
if (i == 0) {
|
||||
cn.push('first');
|
||||
};
|
||||
|
||||
if (i == childBlocks.length - 1) {
|
||||
cn.push('last');
|
||||
};
|
||||
|
||||
n++;
|
||||
n = checkNumber(child, n);
|
||||
return <Block key={child.id} className={cn.join(' ')} {...child} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border" />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div onClick={onClick} onContextMenu={onContextMenu}>
|
||||
<div className="scroller">
|
||||
{content}
|
||||
</div>
|
||||
<div className="border" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}));
|
||||
|
|
|
@ -236,7 +236,9 @@ const SelectionProvider = observer(forwardRef<SelectionRefProps, Props>((props,
|
|||
$(window).trigger('selectionClear');
|
||||
};
|
||||
} else {
|
||||
let needCheck = false;
|
||||
const needCheck = e.ctrlKey || e.metaKey;
|
||||
|
||||
/*
|
||||
if (e.ctrlKey || e.metaKey) {
|
||||
for (const i in I.SelectType) {
|
||||
const list = idsOnStart.current.get(I.SelectType[i]) || [];
|
||||
|
@ -244,6 +246,7 @@ const SelectionProvider = observer(forwardRef<SelectionRefProps, Props>((props,
|
|||
needCheck = needCheck || Boolean(list.length);
|
||||
};
|
||||
};
|
||||
*/
|
||||
|
||||
if (needCheck) {
|
||||
checkNodes(e);
|
||||
|
|
|
@ -223,6 +223,7 @@ const SidebarLeft = observer(class SidebarLeft extends React.Component<{}, State
|
|||
element: '#sidebarSync',
|
||||
className: 'fixed',
|
||||
classNameWrap: 'fromSidebar',
|
||||
subIds: J.Menu.syncStatus,
|
||||
data: {
|
||||
rootId: keyboard.getRootId(),
|
||||
},
|
||||
|
|
|
@ -41,6 +41,8 @@ const SidebarSettingsLibrary = observer(class SidebarSettingsLibrary extends Rea
|
|||
this.onMore = this.onMore.bind(this);
|
||||
this.getAnalyticsSuffix = this.getAnalyticsSuffix.bind(this);
|
||||
this.openFirst = this.openFirst.bind(this);
|
||||
|
||||
this.cache = new CellMeasurerCache({ fixedWidth: true, defaultHeight: HEIGHT_ITEM });
|
||||
};
|
||||
|
||||
render () {
|
||||
|
@ -184,16 +186,6 @@ const SidebarSettingsLibrary = observer(class SidebarSettingsLibrary extends Rea
|
|||
this.load(true, this.openFirst);
|
||||
};
|
||||
|
||||
componentDidUpdate () {
|
||||
const items = this.getItems();
|
||||
|
||||
this.cache = new CellMeasurerCache({
|
||||
fixedWidth: true,
|
||||
defaultHeight: i => this.getRowHeight(items[i]),
|
||||
keyMapper: i => (items[i] || {}).id,
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount () {
|
||||
U.Subscription.destroyList([ J.Constant.subId.library ]);
|
||||
};
|
||||
|
|
|
@ -176,6 +176,8 @@ const SidebarPageType = observer(class SidebarPageType extends React.Component<I
|
|||
this.updateSections();
|
||||
this.disableButton(!U.Common.objectLength(this.update) || (!this.object.name && !this.object.pluralName));
|
||||
|
||||
C.BlockDataviewRelationSet(this.object.id, J.Constant.blockId.dataview, [ 'name', 'description' ].concat(U.Object.getTypeRelationKeys(this.object.id)));
|
||||
|
||||
// analytics
|
||||
let eventId = '';
|
||||
if (update.recommendedLayout) {
|
||||
|
|
|
@ -235,7 +235,10 @@ const SidebarPageWidget = observer(class SidebarPageWidget extends React.Compone
|
|||
const targets = [];
|
||||
const node = $(this.node);
|
||||
const body = node.find('#body');
|
||||
const position = body.outerHeight() + 350 > node.outerHeight() ? I.MenuDirection.Top : I.MenuDirection.Bottom;
|
||||
const nh = node.outerHeight();
|
||||
const button = node.find('#widget-list-add');
|
||||
const { top } = button.offset();
|
||||
const position = top + 350 > nh ? I.MenuDirection.Top : I.MenuDirection.Bottom;
|
||||
|
||||
blocks.forEach(block => {
|
||||
const children = S.Block.getChildren(widgets, block.id);
|
||||
|
@ -278,7 +281,7 @@ const SidebarPageWidget = observer(class SidebarPageWidget extends React.Compone
|
|||
element: '#widget-list-add',
|
||||
className: 'fixed',
|
||||
classNameWrap: 'fromSidebar',
|
||||
offsetY: -4,
|
||||
offsetY: position == I.MenuDirection.Top ? -4 : 4,
|
||||
vertical: position,
|
||||
subIds: J.Menu.widgetAdd,
|
||||
data: {
|
||||
|
|
|
@ -129,7 +129,7 @@ const Progress: FC = observer(() => {
|
|||
resizeObserver.disconnect();
|
||||
};
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => resize(), [ list.length ]);
|
||||
|
||||
|
|
|
@ -80,10 +80,6 @@ const Vault = observer(forwardRef<VaultRefProps>((props, ref) => {
|
|||
pressed.current.add(key);
|
||||
};
|
||||
|
||||
if ([ Key.ctrl, Key.tab, Key.shift ].includes(key)) {
|
||||
pressed.current.add(key);
|
||||
};
|
||||
|
||||
keyboard.shortcut('nextSpace, prevSpace', e, pressed => {
|
||||
checkKeyUp.current = true;
|
||||
onArrow(pressed.match('shift') ? -1 : 1);
|
||||
|
@ -114,9 +110,10 @@ const Vault = observer(forwardRef<VaultRefProps>((props, ref) => {
|
|||
pressed.current.delete(key);
|
||||
|
||||
if (
|
||||
(pressed.current.has(Key.ctrl) ||
|
||||
pressed.current.has(Key.tab) ||
|
||||
pressed.current.has(Key.shift)) ||
|
||||
(
|
||||
pressed.current.has(Key.ctrl) ||
|
||||
pressed.current.has(Key.tab)
|
||||
) ||
|
||||
!checkKeyUp.current
|
||||
) {
|
||||
return;
|
||||
|
@ -346,10 +343,6 @@ const Vault = observer(forwardRef<VaultRefProps>((props, ref) => {
|
|||
onDragEnd={onSortEnd}
|
||||
modifiers={[ restrictToVerticalAxis, restrictToFirstScrollableAncestor ]}
|
||||
>
|
||||
<SortableContext
|
||||
items={items.map((item) => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div id="scroll" className="side top" onScroll={onScroll}>
|
||||
{itemsWithCounter.map((item, i) => (
|
||||
<VaultItem
|
||||
|
@ -364,18 +357,22 @@ const Vault = observer(forwardRef<VaultRefProps>((props, ref) => {
|
|||
|
||||
{itemsWithCounter.length > 0 ? <div className="div" /> : ''}
|
||||
|
||||
{itemsWithoutCounter.map((item, i) => (
|
||||
<VaultItem
|
||||
key={`item-space-${item.id}`}
|
||||
item={item}
|
||||
onClick={e => onClick(e, item)}
|
||||
onMouseEnter={e => onMouseEnter(e, item)}
|
||||
onMouseLeave={() => Preview.tooltipHide()}
|
||||
onContextMenu={item.isButton ? null : e => onContextMenu(e, item)}
|
||||
/>
|
||||
))}
|
||||
<SortableContext
|
||||
items={itemsWithoutCounter.map(item => item.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{itemsWithoutCounter.map((item, i) => (
|
||||
<VaultItem
|
||||
key={`item-space-${item.id}`}
|
||||
item={item}
|
||||
onClick={e => onClick(e, item)}
|
||||
onMouseEnter={e => onMouseEnter(e, item)}
|
||||
onMouseLeave={() => Preview.tooltipHide()}
|
||||
onContextMenu={item.isButton ? null : e => onContextMenu(e, item)}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<div className="side bottom" onDragStart={e => e.preventDefault()}>
|
||||
{canCreate ? (
|
||||
|
|
|
@ -17,6 +17,7 @@ const WidgetSpace = observer(forwardRef<{}, I.WidgetComponent>((props, ref) => {
|
|||
const alt = keyboard.altSymbol();
|
||||
const buttons = [
|
||||
{ id: 'search', name: translate('commonSearch') },
|
||||
{ id: 'all', name: translate('commonAllContent') },
|
||||
!space.isPersonal ? { id: 'member', name: translate('pageSettingsSpaceIndexInviteMembers') } : null,
|
||||
].filter(it => it);
|
||||
|
||||
|
@ -54,6 +55,11 @@ const WidgetSpace = observer(forwardRef<{}, I.WidgetComponent>((props, ref) => {
|
|||
e.stopPropagation();
|
||||
|
||||
switch (item.id) {
|
||||
case 'all': {
|
||||
sidebar.leftPanelSetState({ page: 'object' });
|
||||
break;
|
||||
};
|
||||
|
||||
case 'member': {
|
||||
U.Object.openRoute({ id: 'spaceShare', layout: I.ObjectLayout.Settings });
|
||||
analytics.event('ClickSpaceWidgetInvite', { route: analytics.route.widget });
|
||||
|
|
|
@ -249,9 +249,11 @@ const WidgetTree = observer(forwardRef<WidgetTreeRefProps, I.WidgetComponent>((p
|
|||
let content = null;
|
||||
|
||||
if (!length) {
|
||||
const label = targetId == J.Constant.widgetId.favorite ? translate('widgetEmptyFavoriteLabel') : translate('widgetEmptyLabel');
|
||||
|
||||
content = (
|
||||
<div className="emptyWrap">
|
||||
<Label className="empty" text={translate('widgetEmptyLabel')} />
|
||||
<Label className="empty" text={label} />
|
||||
</div>
|
||||
);
|
||||
} else
|
||||
|
|
|
@ -188,9 +188,11 @@ const WidgetView = observer(forwardRef<WidgetViewRefProps, I.WidgetComponent>((p
|
|||
};
|
||||
|
||||
if (isEmpty) {
|
||||
const label = targetId == J.Constant.widgetId.favorite ? translate('widgetEmptyFavoriteLabel') : translate('widgetEmptyLabel');
|
||||
|
||||
content = (
|
||||
<div className="emptyWrap">
|
||||
{!isLoading ? <Label className="empty" text={translate('widgetEmptyLabel')} /> : ''}
|
||||
{!isLoading ? <Label className="empty" text={label} /> : ''}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
|
|
|
@ -338,15 +338,24 @@ class Action {
|
|||
text: translate('popupConfirmDeleteWarningText'),
|
||||
textConfirm: translate('commonDelete'),
|
||||
onConfirm: () => {
|
||||
Storage.deleteLastOpenedByObjectId(ids);
|
||||
C.ObjectListDelete(ids);
|
||||
|
||||
|
||||
const isPopup = keyboard.isPopup();
|
||||
const match = keyboard.getMatch();
|
||||
|
||||
if (ids.includes(match.params.id)) {
|
||||
if (isPopup) {
|
||||
S.Popup.close('page');
|
||||
} else {
|
||||
U.Space.openDashboard();
|
||||
};
|
||||
};
|
||||
|
||||
if (callBack) {
|
||||
callBack();
|
||||
};
|
||||
|
||||
// Remove last opened objects in case any is deleted
|
||||
Storage.deleteLastOpenedByObjectId(ids);
|
||||
|
||||
analytics.event('RemoveCompletely', { count, route });
|
||||
},
|
||||
onCancel: () => {
|
||||
|
|
|
@ -925,11 +925,7 @@ class Dispatcher {
|
|||
const { object } = payload;
|
||||
|
||||
U.Object.openAuto(object);
|
||||
window.focus();
|
||||
|
||||
if (electron.focus) {
|
||||
electron.focus();
|
||||
};
|
||||
Renderer.send('focusWindow');
|
||||
|
||||
analytics.createObject(object.type, object.layout, analytics.route.webclipper, 0);
|
||||
break;
|
||||
|
|
|
@ -283,7 +283,6 @@ class Sidebar {
|
|||
this.footer.css({ width: '' });
|
||||
|
||||
this.header.toggleClass('sidebarAnimation', animate);
|
||||
this.header.toggleClass('isSmall', hw < 750);
|
||||
this.footer.toggleClass('sidebarAnimation', animate);
|
||||
//this.page.toggleClass('sidebarAnimation', animate);
|
||||
|
||||
|
|
|
@ -247,21 +247,25 @@ class Storage {
|
|||
return this.get('pin');
|
||||
};
|
||||
|
||||
getLastOpened () {
|
||||
return this.get('lastOpenedObject') || {};
|
||||
};
|
||||
|
||||
setLastOpened (windowId: string, param: any) {
|
||||
const obj = this.get('lastOpenedObject') || {};
|
||||
const obj = this.getLastOpened();
|
||||
|
||||
obj[windowId] = Object.assign(obj[windowId] || {}, param);
|
||||
this.set('lastOpenedObject', obj);
|
||||
};
|
||||
|
||||
deleteLastOpenedByObjectId (objectIds: string[]) {
|
||||
objectIds = objectIds || [];
|
||||
deleteLastOpenedByObjectId (ids: string[]) {
|
||||
ids = ids || [];
|
||||
|
||||
const obj = this.get('lastOpenedObject') || {};
|
||||
const obj = this.getLastOpened();
|
||||
const windowIds = [];
|
||||
|
||||
for (const windowId in obj) {
|
||||
if (!obj[windowId] || objectIds.includes(obj[windowId].id)) {
|
||||
if (!obj[windowId] || ids.includes(obj[windowId].id)) {
|
||||
windowIds.push(windowId);
|
||||
};
|
||||
};
|
||||
|
@ -269,22 +273,20 @@ class Storage {
|
|||
this.deleteLastOpenedByWindowId(windowIds);
|
||||
};
|
||||
|
||||
deleteLastOpenedByWindowId (windowIds: string[]) {
|
||||
windowIds = windowIds.filter(id => id != '1');
|
||||
|
||||
if (!windowIds.length) {
|
||||
deleteLastOpenedByWindowId (ids: string[]) {
|
||||
if (!ids.length) {
|
||||
return;
|
||||
};
|
||||
|
||||
const obj = this.get('lastOpenedObject') || {};
|
||||
const obj = this.getLastOpened();
|
||||
|
||||
windowIds.forEach(windowId => delete(obj[windowId]));
|
||||
ids.forEach(ids => delete(obj[ids]));
|
||||
this.set('lastOpenedObject', obj);
|
||||
};
|
||||
|
||||
getLastOpened (windowId: string) {
|
||||
const obj = this.get('lastOpenedObject') || {};
|
||||
return obj[windowId] || obj[1] || null;
|
||||
getLastOpenedByWindowId (id: string) {
|
||||
const obj = this.getLastOpened();
|
||||
return obj[id] || obj[1] || null;
|
||||
};
|
||||
|
||||
setToggle (rootId: string, id: string, value: boolean) {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -180,8 +180,6 @@ class UtilData {
|
|||
} else {
|
||||
U.Space.openDashboard(routeParam);
|
||||
};
|
||||
|
||||
S.Common.redirectSet('');
|
||||
};
|
||||
|
||||
if (callBack) {
|
||||
|
|
|
@ -19,11 +19,22 @@ const IFRAME_PARAM = 'frameborder="0" scrolling="no" allowfullscreen';
|
|||
|
||||
class UtilEmbed {
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding content based on the processor type.
|
||||
* @param {I.EmbedProcessor} processor - The embed processor type.
|
||||
* @param {any} content - The content to embed.
|
||||
* @returns {string} The HTML string for embedding.
|
||||
*/
|
||||
getHtml (processor: I.EmbedProcessor, content: any): string {
|
||||
const fn = U.Common.toCamelCase(`get-${I.EmbedProcessor[processor]}-html`);
|
||||
return this[fn] ? this[fn](content) : content;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a YouTube video.
|
||||
* @param {string} content - The YouTube URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getYoutubeHtml (content: string): string {
|
||||
let url = '';
|
||||
|
||||
|
@ -35,30 +46,65 @@ class UtilEmbed {
|
|||
return `<iframe id="player" src="${url.toString()}" ${IFRAME_PARAM} title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Vimeo video.
|
||||
* @param {string} content - The Vimeo URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getVimeoHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM} allow="autoplay; fullscreen; picture-in-picture"></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Google Maps location.
|
||||
* @param {string} content - The Google Maps URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getGoogleMapsHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM} loading="lazy"></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Miro board.
|
||||
* @param {string} content - The Miro URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getMiroHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM} allow="fullscreen; clipboard-read; clipboard-write"></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Figma file.
|
||||
* @param {string} content - The Figma URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getFigmaHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM}></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding an OpenStreetMap view.
|
||||
* @param {string} content - The OpenStreetMap URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getOpenStreetMapHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM}></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a GitHub Gist.
|
||||
* @param {string} content - The Gist URL.
|
||||
* @returns {string} The HTML script tag.
|
||||
*/
|
||||
getGithubGistHtml (content: string): string {
|
||||
return `<script src="${content}.js"></script>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Codepen snippet.
|
||||
* @param {string} content - The Codepen URL.
|
||||
* @returns {string} The HTML string for Codepen.
|
||||
*/
|
||||
getCodepenHtml (content: string): string {
|
||||
let p = [];
|
||||
|
||||
|
@ -75,22 +121,47 @@ class UtilEmbed {
|
|||
return `<p class="codepen" data-height="300" data-default-tab="html,result" data-slug-hash="${p[3]}" data-user="${p[1]}"></p>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Bilibili video.
|
||||
* @param {string} content - The Bilibili URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getBilibiliHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM}></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Sketchfab model.
|
||||
* @param {string} content - The Sketchfab URL.
|
||||
* @returns {string} The HTML iframe string.
|
||||
*/
|
||||
getSketchfabHtml (content: string): string {
|
||||
return `<iframe src="${content}" ${IFRAME_PARAM}></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding a Drawio diagram or SVG.
|
||||
* @param {string} content - The Drawio URL or SVG content.
|
||||
* @returns {string} The HTML iframe or SVG string.
|
||||
*/
|
||||
getDrawioHtml (content: string): string {
|
||||
return content.match(/^<svg/) ? content : `<iframe src="${content}" ${IFRAME_PARAM}></iframe>`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the HTML for embedding an image.
|
||||
* @param {string} content - The image URL.
|
||||
* @returns {string} The HTML img tag.
|
||||
*/
|
||||
getImageHtml (content: string): string {
|
||||
return `<img src="${content}" />`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines the embed processor type for a given URL.
|
||||
* @param {string} url - The URL to check.
|
||||
* @returns {I.EmbedProcessor|null} The processor type or null if not found.
|
||||
*/
|
||||
getProcessorByUrl (url: string): I.EmbedProcessor {
|
||||
let p = null;
|
||||
for (const i in DOMAINS) {
|
||||
|
@ -115,6 +186,11 @@ class UtilEmbed {
|
|||
return p;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a parsed embed URL for a given processor type.
|
||||
* @param {string} url - The original URL.
|
||||
* @returns {string|undefined} The parsed embed URL or undefined.
|
||||
*/
|
||||
getParsedUrl (url: string): string {
|
||||
const processor = this.getProcessorByUrl(url);
|
||||
|
||||
|
|
|
@ -12,6 +12,11 @@ const UNITS = {
|
|||
|
||||
class UtilFile {
|
||||
|
||||
/**
|
||||
* Returns a human-readable file size string for a given number of bytes.
|
||||
* @param {number} v - The file size in bytes.
|
||||
* @returns {string} The formatted file size string.
|
||||
*/
|
||||
size (v: number): string {
|
||||
v = Number(v) || 0;
|
||||
|
||||
|
@ -30,6 +35,11 @@ class UtilFile {
|
|||
return ret ? U.Common.formatNumber(Number(U.Common.sprintf(`%0.2f`, ret))) + unit : '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the icon name for a file object based on its properties.
|
||||
* @param {any} object - The file object.
|
||||
* @returns {string} The icon name.
|
||||
*/
|
||||
icon (object: any): string {
|
||||
object = object || {};
|
||||
|
||||
|
@ -120,10 +130,21 @@ class UtilFile {
|
|||
return icon;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the icon path for a file object based on its properties and theme.
|
||||
* @param {any} object - The file object.
|
||||
* @returns {string} The icon path.
|
||||
*/
|
||||
iconPath (object: any) {
|
||||
return `./img/${S.Common.getThemePath()}icon/file/${this.icon(object)}.svg`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a preview canvas for an image file and calls a callback with the canvas.
|
||||
* @param {any} file - The image file.
|
||||
* @param {any} param - The parameters for loading.
|
||||
* @param {function} [success] - Callback with the loaded canvas.
|
||||
*/
|
||||
loadPreviewCanvas (file: any, param: any, success?: (canvas: any) => void) {
|
||||
if (!file) {
|
||||
return;
|
||||
|
@ -145,6 +166,13 @@ class UtilFile {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads a preview image as a base64 string and calls a callback with the image and parameters.
|
||||
* @param {any} file - The image file.
|
||||
* @param {any} param - The parameters for loading.
|
||||
* @param {function} [success] - Callback with the image and parameters.
|
||||
* @param {function} [error] - Callback if loading fails.
|
||||
*/
|
||||
loadPreviewBase64 (file: any, param: any, success?: (image: string, param: any) => void, error?: (error: string) => void) {
|
||||
this.loadPreviewCanvas(file, param, (canvas: any) => {
|
||||
const image = canvas.toDataURL(param.type, param.quality);
|
||||
|
@ -159,10 +187,19 @@ class UtilFile {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the current date as a string suitable for filenames.
|
||||
* @returns {string} The date string.
|
||||
*/
|
||||
date () {
|
||||
return new Date().toISOString().replace(/:/g, '_').replace(/\..+/, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the file name for a file object, appending the extension if needed.
|
||||
* @param {any} object - The file object.
|
||||
* @returns {string} The file name.
|
||||
*/
|
||||
name (object: any) {
|
||||
object = object || {};
|
||||
|
||||
|
@ -176,6 +213,11 @@ class UtilFile {
|
|||
return `${name}.${fileExt}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the object layout type for a given mime type.
|
||||
* @param {string} mime - The mime type string.
|
||||
* @returns {I.ObjectLayout} The object layout type.
|
||||
*/
|
||||
layoutByMime (mime: string) {
|
||||
const t = mime.split('/');
|
||||
|
||||
|
@ -199,6 +241,11 @@ class UtilFile {
|
|||
return layout;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a drag event contains files.
|
||||
* @param {React.DragEvent} e - The drag event.
|
||||
* @returns {boolean} True if files are present, false otherwise.
|
||||
*/
|
||||
checkDropFiles (e: React.DragEvent): boolean {
|
||||
return (e.dataTransfer.files && e.dataTransfer.files.length) ? true : false;
|
||||
};
|
||||
|
|
|
@ -2,6 +2,11 @@ import { I, S, U, J, Relation } from 'Lib';
|
|||
|
||||
class UtilGraph {
|
||||
|
||||
/**
|
||||
* Returns the image source path for a graph node based on its layout and properties.
|
||||
* @param {any} d - The node data object.
|
||||
* @returns {string} The image source path.
|
||||
*/
|
||||
imageSrc (d: any) {
|
||||
d = d || {};
|
||||
|
||||
|
|
|
@ -871,7 +871,7 @@ class UtilMenu {
|
|||
getSystemWidgets () {
|
||||
const space = U.Space.getSpaceview();
|
||||
return [
|
||||
{ id: J.Constant.widgetId.favorite, name: translate('widgetFavorite'), icon: 'widget-star' },
|
||||
{ id: J.Constant.widgetId.favorite, name: translate('widgetFavorite'), icon: 'widget-pin' },
|
||||
space.chatId || U.Object.isAllowedChat() ? { id: J.Constant.widgetId.chat, name: translate('commonMainChat'), icon: 'widget-chat' } : null,
|
||||
{ id: J.Constant.widgetId.allObject, name: translate('commonAllContent'), icon: 'widget-all' },
|
||||
{ id: J.Constant.widgetId.recentEdit, name: translate('widgetRecent'), icon: 'widget-pencil' },
|
||||
|
|
|
@ -10,7 +10,12 @@ class UtilPrism {
|
|||
this.aliasMap = this.getAliasMap();
|
||||
};
|
||||
|
||||
private getDependencies (lang: string) {
|
||||
/**
|
||||
* Returns an array of dependencies for a given Prism language, in load order.
|
||||
* @param {string} lang - The language key.
|
||||
* @returns {string[]} The array of dependencies.
|
||||
*/
|
||||
private getDependencies (lang: string): string[] {
|
||||
const result = [];
|
||||
const language = Components.languages[lang] || {};
|
||||
// the type of `require`, `optional`, `alias` is one of `undefined`, `string[]` and `string`
|
||||
|
@ -24,7 +29,10 @@ class UtilPrism {
|
|||
return result;
|
||||
};
|
||||
|
||||
/** returns an array which is the correct order of loading all Prism components */
|
||||
/**
|
||||
* Returns an array which is the correct order of loading all Prism components.
|
||||
* @returns {string[]} The ordered list of component keys.
|
||||
*/
|
||||
get components (): string[] {
|
||||
const result = [];
|
||||
for (const key in Components.languages) {
|
||||
|
@ -33,6 +41,10 @@ class UtilPrism {
|
|||
return [ ...new Set(result) ];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map of language keys to their display titles.
|
||||
* @returns {object} The map of language keys to titles.
|
||||
*/
|
||||
private getMap () {
|
||||
const result = {};
|
||||
|
||||
|
@ -52,6 +64,10 @@ class UtilPrism {
|
|||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map of display titles to arrays of language keys.
|
||||
* @returns {Map<string, string[]>} The value-key map.
|
||||
*/
|
||||
getValueKeyMap (): Map<string, string[]> {
|
||||
const ret: Map<string, string[]> = new Map();
|
||||
for (const [ key, value ] of Object.entries(this.map)) {
|
||||
|
@ -60,6 +76,10 @@ class UtilPrism {
|
|||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a map of language keys to their canonical alias.
|
||||
* @returns {object} The alias map.
|
||||
*/
|
||||
getAliasMap () {
|
||||
const map = this.getValueKeyMap();
|
||||
const result: { [ key: string ]: string } = {};
|
||||
|
@ -73,6 +93,10 @@ class UtilPrism {
|
|||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns an array of language titles and their canonical key.
|
||||
* @returns {{ id: string, name: string }[]} The array of language titles.
|
||||
*/
|
||||
getTitles () {
|
||||
const map = this.getValueKeyMap();
|
||||
const result: { id: string; name: string }[] = [];
|
||||
|
|
|
@ -5,10 +5,10 @@ interface RouteParam {
|
|||
page: string;
|
||||
action: string;
|
||||
id: string;
|
||||
spaceId: string;
|
||||
viewId: string;
|
||||
relationKey: string;
|
||||
additional: { key: string, value: string }[];
|
||||
spaceId?: string;
|
||||
viewId?: string;
|
||||
relationKey?: string;
|
||||
additional?: { key: string, value: string }[];
|
||||
};
|
||||
|
||||
class UtilRouter {
|
||||
|
@ -16,10 +16,19 @@ class UtilRouter {
|
|||
history: any = null;
|
||||
isOpening = false;
|
||||
|
||||
/**
|
||||
* Initializes the router with a history object.
|
||||
* @param {any} history - The history object to use for navigation.
|
||||
*/
|
||||
init (history: any) {
|
||||
this.history = history;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses a route path into its parameter object.
|
||||
* @param {string} path - The route path string.
|
||||
* @returns {RouteParam} The parsed route parameters.
|
||||
*/
|
||||
getParam (path: string): any {
|
||||
const route = path.split('/');
|
||||
if (!route.length) {
|
||||
|
@ -45,6 +54,11 @@ class UtilRouter {
|
|||
return param;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a route string from route parameters.
|
||||
* @param {Partial<RouteParam>} param - The route parameters.
|
||||
* @returns {string} The route string.
|
||||
*/
|
||||
build (param: Partial<RouteParam>): string {
|
||||
const { page, action } = param;
|
||||
const id = String(param.id || '');
|
||||
|
@ -71,6 +85,11 @@ class UtilRouter {
|
|||
return route.join('/');
|
||||
};
|
||||
|
||||
/**
|
||||
* Navigates to a route with optional parameters and animation.
|
||||
* @param {string} route - The route string.
|
||||
* @param {Partial<I.RouteParam>} param - Additional navigation parameters.
|
||||
*/
|
||||
go (route: string, param: Partial<I.RouteParam>) {
|
||||
if (!route) {
|
||||
return;
|
||||
|
@ -89,6 +108,7 @@ class UtilRouter {
|
|||
|
||||
S.Menu.closeAll();
|
||||
S.Popup.closeAll();
|
||||
S.Common.redirectSet('');
|
||||
sidebar.rightPanelToggle(false, false, keyboard.isPopup());
|
||||
|
||||
if (routeParam.spaceId && ![ space ].includes(routeParam.spaceId)) {
|
||||
|
@ -153,6 +173,14 @@ class UtilRouter {
|
|||
timeout ? window.setTimeout(() => onTimeout(), timeout) : onTimeout();
|
||||
};
|
||||
|
||||
/**
|
||||
* Switches to a different space, handling errors and fallbacks.
|
||||
* @param {string} id - The space ID to switch to.
|
||||
* @param {string} route - The route to navigate after switching.
|
||||
* @param {boolean} sendEvent - Whether to send analytics event.
|
||||
* @param {any} routeParam - Additional route parameters.
|
||||
* @param {boolean} useFallback - Whether to use fallback on error.
|
||||
*/
|
||||
switchSpace (id: string, route: string, sendEvent: boolean, routeParam: any, useFallback: boolean) {
|
||||
if (this.isOpening) {
|
||||
return;
|
||||
|
@ -219,10 +247,18 @@ class UtilRouter {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the current route path as a string.
|
||||
* @returns {string} The current route path.
|
||||
*/
|
||||
getRoute () {
|
||||
return String(this.history?.location?.pathname || '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the spaceId from the current route or the default space.
|
||||
* @returns {string} The spaceId.
|
||||
*/
|
||||
getRouteSpaceId () {
|
||||
const param = this.getParam(this.getRoute());
|
||||
return param.spaceId || S.Common.space;
|
||||
|
|
|
@ -10,6 +10,9 @@ class UtilSmile {
|
|||
cache: any = {};
|
||||
aliases: any = {};
|
||||
|
||||
/**
|
||||
* Initializes the emoji data, icons, cache, and aliases.
|
||||
*/
|
||||
init () {
|
||||
init({ data: J.Emoji });
|
||||
|
||||
|
@ -28,6 +31,12 @@ class UtilSmile {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the native emoji character by its ID and skin tone.
|
||||
* @param {string} id - The emoji ID.
|
||||
* @param {number} skin - The skin tone index.
|
||||
* @returns {string} The native emoji character.
|
||||
*/
|
||||
nativeById (id: string, skin: number): string {
|
||||
if (!id) {
|
||||
return '';
|
||||
|
@ -51,6 +60,10 @@ class UtilSmile {
|
|||
return skinItem.native;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random emoji parameter (id and skin).
|
||||
* @returns {{ id: string, skin: number }} The random emoji parameter.
|
||||
*/
|
||||
randomParam (): { id: string, skin: number } {
|
||||
return {
|
||||
id: this.icons[U.Common.rand(0, this.icons.length - 1)],
|
||||
|
@ -58,11 +71,20 @@ class UtilSmile {
|
|||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns a random native emoji character.
|
||||
* @returns {string} The random emoji character.
|
||||
*/
|
||||
random (): string {
|
||||
const param = this.randomParam();
|
||||
return this.nativeById(param.id, param.skin);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the image source path for an emoji given its colons code.
|
||||
* @param {string} colons - The emoji colons code.
|
||||
* @returns {string} The image source path.
|
||||
*/
|
||||
srcFromColons (colons: string) {
|
||||
if (!colons) {
|
||||
return '';
|
||||
|
@ -94,6 +116,11 @@ class UtilSmile {
|
|||
return `${prefix}${code}.png`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the shortcodes for a native emoji character.
|
||||
* @param {string} icon - The native emoji character.
|
||||
* @returns {string} The shortcodes for the emoji.
|
||||
*/
|
||||
getCode (icon: string) {
|
||||
icon = icon.trim();
|
||||
|
||||
|
@ -129,11 +156,20 @@ class UtilSmile {
|
|||
return this.cache[icon] || '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Removes emoji characters from a string.
|
||||
* @param {string} t - The string to strip.
|
||||
* @returns {string} The string without emojis.
|
||||
*/
|
||||
strip (t: string) {
|
||||
const r = /(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/g;
|
||||
return t.replace(r, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the list of emoji categories with translated names.
|
||||
* @returns {any[]} The list of emoji categories.
|
||||
*/
|
||||
getCategories () {
|
||||
return J.Emoji.categories.filter(it => it.id != 'frequent').map(it => ({
|
||||
...it,
|
||||
|
|
|
@ -90,7 +90,7 @@ class UtilSpace {
|
|||
};
|
||||
|
||||
getLastObject () {
|
||||
let home = Storage.getLastOpened(U.Common.getCurrentElectronWindowId());
|
||||
let home = Storage.getLastOpenedByWindowId(U.Common.getCurrentElectronWindowId());
|
||||
|
||||
// Invalid data protection
|
||||
if (!home || !home.id) {
|
||||
|
|
|
@ -214,7 +214,6 @@ class CommonStore {
|
|||
if (this.pinTimeId === null) {
|
||||
this.pinTimeId = Storage.get('pinTime');
|
||||
};
|
||||
|
||||
return (Number(this.pinTimeId) || J.Constant.default.pinTime) * 1000;
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue