diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..72ee4d4c03 --- /dev/null +++ b/AGENTS.md @@ -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 ( + + ); +}); + +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 = () => ( + + + + + + +); +``` + +--- + +## 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 = () => ( + + {/* Application components */} + +); +``` + +- **Usage in Components**: + +```typescript +import { FormattedMessage } from 'react-intl'; + +const Greeting = () => ( +

+ +

+); +``` + +--- + +## 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(); + 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/) diff --git a/dist/extension/manifest.chrome.json b/dist/extension/manifest.chrome.json deleted file mode 100644 index a3222ea796..0000000000 --- a/dist/extension/manifest.chrome.json +++ /dev/null @@ -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": [ "" ] - } - ], - "web_accessible_resources": [ - { - "resources": [ "iframe/index.html" ], - "matches": [ "" ] - } - ] -} \ No newline at end of file diff --git a/dist/extension/manifest.json b/dist/extension/manifest.json index 0975594645..f5d7b28cee 100644 --- a/dist/extension/manifest.json +++ b/dist/extension/manifest.json @@ -29,7 +29,7 @@ { "js": [ "js/browser-polyfill.min.js", - "js/foreground.min.js" + "js/foreground.js" ], "css": [ "css/foreground.css" ], "matches": [ "" ] diff --git a/electron.js b/electron.js index 158bb9325c..d50841234d 100644 --- a/electron.js +++ b/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); diff --git a/electron/js/api.js b/electron/js/api.js index b3cfeeb86f..0e041b1cbf 100644 --- a/electron/js/api.js +++ b/electron/js/api.js @@ -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(); \ No newline at end of file diff --git a/electron/js/lib/installNativeMessagingHost.js b/electron/js/lib/installNativeMessagingHost.js index 044ab8f15b..6bc577adbe 100644 --- a/electron/js/lib/installNativeMessagingHost.js +++ b/electron/js/lib/installNativeMessagingHost.js @@ -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'), diff --git a/electron/js/window.js b/electron/js/window.js index 76c6ffe4e7..2aa9c0184c 100644 --- a/electron/js/window.js +++ b/electron/js/window.js @@ -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, diff --git a/middleware.version b/middleware.version index d3924d2cb8..3a1a2c94c1 100644 --- a/middleware.version +++ b/middleware.version @@ -1 +1 @@ -0.41.0-rc14 \ No newline at end of file +0.41.0-rc16 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c7f460f134..b61ee87b6c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 37249d2f30..c260892088 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "anytype", - "version": "0.46.32-beta", + "version": "0.46.34-alpha", "description": "Anytype", "main": "electron.js", "scripts": { diff --git a/src/img/icon/widget/button/arrow.svg b/src/img/icon/widget/button/arrow.svg index 48b8e2f67b..08638c0921 100644 --- a/src/img/icon/widget/button/arrow.svg +++ b/src/img/icon/widget/button/arrow.svg @@ -1,3 +1,3 @@ - - - \ No newline at end of file + + + diff --git a/src/img/icon/widget/button/create.svg b/src/img/icon/widget/button/create.svg new file mode 100644 index 0000000000..f5aa8ddaf8 --- /dev/null +++ b/src/img/icon/widget/button/create.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/img/icon/widget/emptyMore.svg b/src/img/icon/widget/emptyMore.svg new file mode 100644 index 0000000000..b68243e43f --- /dev/null +++ b/src/img/icon/widget/emptyMore.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/img/icon/widget/system/pin.svg b/src/img/icon/widget/system/pin.svg new file mode 100644 index 0000000000..fed93bc164 --- /dev/null +++ b/src/img/icon/widget/system/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/img/theme/dark/icon/widget/system/pin.svg b/src/img/theme/dark/icon/widget/system/pin.svg new file mode 100644 index 0000000000..fd7e912d94 --- /dev/null +++ b/src/img/theme/dark/icon/widget/system/pin.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/json/menu.ts b/src/json/menu.ts index bd79c57141..7a495cf3c8 100644 --- a/src/json/menu.ts +++ b/src/json/menu.ts @@ -70,4 +70,5 @@ export default { table: [ 'select2', 'blockColor', 'blockBackground' ], dataviewNew: [ 'searchObject', 'typeSuggest', 'dataviewTemplateList' ], graphSettings: [ 'select' ], + syncStatus: [ 'syncStatusInfo' ], }; diff --git a/src/json/text.json b/src/json/text.json index 0c5ddac008..f57ffc3b61 100644 --- a/src/json/text.json +++ b/src/json/text.json @@ -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 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 %s has been added", + "toastJoinSpace": "You have joined the %s", "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
menu", "widgetShowAll": "See all Objects", "widgetItemClipboard": "Create from clipboard", diff --git a/src/scss/block/dataview.scss b/src/scss/block/dataview.scss index fef4dc73a3..a3ef1b9e04 100644 --- a/src/scss/block/dataview.scss +++ b/src/scss/block/dataview.scss @@ -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 { diff --git a/src/scss/block/dataview/view/calendar.scss b/src/scss/block/dataview/view/calendar.scss index 4ae75ed3d5..92cc664378 100644 --- a/src/scss/block/dataview/view/calendar.scss +++ b/src/scss/block/dataview/view/calendar.scss @@ -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; } diff --git a/src/scss/block/dataview/view/grid.scss b/src/scss/block/dataview/view/grid.scss index 92db764a36..16f839b73a 100644 --- a/src/scss/block/dataview/view/grid.scss +++ b/src/scss/block/dataview/view/grid.scss @@ -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; } diff --git a/src/scss/block/featured.scss b/src/scss/block/featured.scss index ba57893deb..a5821913e6 100644 --- a/src/scss/block/featured.scss +++ b/src/scss/block/featured.scss @@ -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; } diff --git a/src/scss/block/relation.scss b/src/scss/block/relation.scss index 797a40b907..18ede77a89 100644 --- a/src/scss/block/relation.scss +++ b/src/scss/block/relation.scss @@ -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); } diff --git a/src/scss/component/icon.scss b/src/scss/component/icon.scss index c952d7771b..5e83e3f8ad 100644 --- a/src/scss/component/icon.scss +++ b/src/scss/component/icon.scss @@ -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'); } diff --git a/src/scss/component/sidebar/allObject.scss b/src/scss/component/sidebar/allObject.scss index acff2f1f1d..eee7843fd6 100644 --- a/src/scss/component/sidebar/allObject.scss +++ b/src/scss/component/sidebar/allObject.scss @@ -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 { diff --git a/src/scss/menu/syncStatus.scss b/src/scss/menu/syncStatus.scss index de88987dbf..5a23cdf206 100644 --- a/src/scss/menu/syncStatus.scss +++ b/src/scss/menu/syncStatus.scss @@ -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; } diff --git a/src/scss/page/main/relation.scss b/src/scss/page/main/relation.scss index 5652d38cd9..3e71818671 100644 --- a/src/scss/page/main/relation.scss +++ b/src/scss/page/main/relation.scss @@ -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; } +} \ No newline at end of file diff --git a/src/scss/popup/common.scss b/src/scss/popup/common.scss index b893d167d9..7952727a22 100644 --- a/src/scss/popup/common.scss +++ b/src/scss/popup/common.scss @@ -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; } diff --git a/src/scss/theme/dark/common.scss b/src/scss/theme/dark/common.scss index fb0c3bbdca..a8484c9810 100644 --- a/src/scss/theme/dark/common.scss +++ b/src/scss/theme/dark/common.scss @@ -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'); } diff --git a/src/scss/widget/common.scss b/src/scss/widget/common.scss index f78ed05ad5..071dfe6cc8 100644 --- a/src/scss/widget/common.scss +++ b/src/scss/widget/common.scss @@ -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; } diff --git a/src/scss/widget/space.scss b/src/scss/widget/space.scss index 78a37757d8..e98db63b40 100644 --- a/src/scss/widget/space.scss +++ b/src/scss/widget/space.scss @@ -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'); } } } diff --git a/src/ts/component/block/dataview.tsx b/src/ts/component/block/dataview.tsx index aa4f7f6d5d..4e0fdea22a 100644 --- a/src/ts/component/block/dataview.tsx +++ b/src/ts/component/block/dataview.tsx @@ -971,6 +971,10 @@ const BlockDataview = observer(class BlockDataview extends React.Component { 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 { let more = null; if (length > LIMIT) { more = ( -
+
+{length - LIMIT} {translate('commonMore')} {U.Common.plural(length, translate('pluralObject')).toLowerCase()}
); @@ -55,16 +59,33 @@ const Item = observer(class Item extends React.Component { const canEdit = !item.isReadonly && S.Block.isAllowed(item.restrictions, [ I.RestrictionObject.Details ]) && U.Object.isTaskLayout(item.layout); const icon = hideIcon ? null : ; + let content = ( + <> + {icon} + this.onOpen(item)} /> + + ); + + if (canDrag) { + content = ( + + {content} + + ); + }; + return ( +
onContext(e, item.id)} onMouseEnter={e => this.onMouseEnter(e, item)} onMouseLeave={this.onMouseLeave} + onDragStart={e => this.onDragStart(e, item)} > - {icon} - this.onOpen(item)} /> + {content}
); }; @@ -96,6 +117,8 @@ const Item = observer(class Item extends React.Component { ))} {more} + +
); @@ -107,7 +130,7 @@ const Item = observer(class Item extends React.Component { 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 { 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; diff --git a/src/ts/component/block/featured.tsx b/src/ts/component/block/featured.tsx index 445cd2ab43..821b2a5dd0 100644 --- a/src/ts/component/block/featured.tsx +++ b/src/ts/component/block/featured.tsx @@ -43,7 +43,7 @@ const BlockFeatured = observer(class BlockFeatured extends React.Component {headerRelationsLayout == I.FeaturedRelationLayout.Column ? (
- {items.map((relation: any) => ( - - ))} + {items.map((relation: any) => { + const value = object[relation.relationKey]; + const canEdit = !readonly && allowedValue && !relation.isReadonlyValue; + + return ( + + ); + })}
) : (
{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) { diff --git a/src/ts/component/block/index.tsx b/src/ts/component/block/index.tsx index 1cb98c3ca1..056d0b44b0 100644 --- a/src/ts/component/block/index.tsx +++ b/src/ts/component/block/index.tsx @@ -544,6 +544,10 @@ const Block = observer(class Block extends React.Component { const { rootId, block, readonly, isContextMenuDisabled } = this.props; const selection = S.Common.getRef('selectionProvider'); + if (e.ctrlKey) { + return; + }; + if ( isContextMenuDisabled || readonly || diff --git a/src/ts/component/block/text.tsx b/src/ts/component/block/text.tsx index 04a9d6090f..bc04b6418a 100644 --- a/src/ts/component/block/text.tsx +++ b/src/ts/component/block/text.tsx @@ -960,13 +960,21 @@ const BlockText = observer(class BlockText extends React.Component { 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) { diff --git a/src/ts/component/drag/provider.tsx b/src/ts/component/drag/provider.tsx index d5584f458c..2a3193aeaf 100644 --- a/src/ts/component/drag/provider.tsx +++ b/src/ts/component/drag/provider.tsx @@ -54,6 +54,7 @@ const DragProvider = observer(forwardRef((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((props, re const { onRecordDrop } = origin.current; if (onRecordDrop) { - onRecordDrop(targetId, ids); + onRecordDrop(targetId, ids, position); }; break; }; diff --git a/src/ts/component/drag/target.tsx b/src/ts/component/drag/target.tsx index d15e327800..c6809702a2 100644 --- a/src/ts/component/drag/target.tsx +++ b/src/ts/component/drag/target.tsx @@ -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 = ({ cacheKey = '', targetContextId = '', dropType = I.DropType.None, + viewType = null, type, style = 0, className = '', @@ -71,6 +73,7 @@ const DropTarget: FC = ({ 'drop-type': dropType, 'context-id': targetContextId, 'drop-middle': Number(canDropMiddle) || 0, + 'view-type': Number(viewType) || 0, })} > {children} diff --git a/src/ts/component/form/drag/horizontal.tsx b/src/ts/component/form/drag/horizontal.tsx index 00a566c1a5..ff2fecdb6d 100644 --- a/src/ts/component/form/drag/horizontal.tsx +++ b/src/ts/component/form/drag/horizontal.tsx @@ -25,7 +25,7 @@ const SNAP = 0.025; const DragHorizontal = forwardRef(({ id = '', className = '', - value: initalValue = 0, + value: initialValue = 0, snaps = [], strictSnap = false, iconIsOutside = false, @@ -34,7 +34,7 @@ const DragHorizontal = forwardRef(({ onMove, onEnd, }, ref) => { - let value = initalValue; + let value = initialValue; const nodeRef = useRef(null); const iconRef = useRef(null); @@ -154,7 +154,7 @@ const DragHorizontal = forwardRef(({ $(nodeRef.current).removeClass('isDragging'); }; - useEffect(() => setValue(initalValue), []); + useEffect(() => setValue(initialValue), []); useImperativeHandle(ref, () => ({ getValue, diff --git a/src/ts/component/header/index.tsx b/src/ts/component/header/index.tsx index 2e35c8020e..551d4b8689 100644 --- a/src/ts/component/header/index.tsx +++ b/src/ts/component/header/index.tsx @@ -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 (