diff --git a/.all-contributorsrc b/.all-contributorsrc index 01e5d122de..8993984505 100644 --- a/.all-contributorsrc +++ b/.all-contributorsrc @@ -266,6 +266,43 @@ "contributions": [ "doc" ] + }, + { + "login": "LuciNyan", + "name": "子瞻 Luci", + "avatar_url": "https://avatars.githubusercontent.com/u/22126563?v=4", + "profile": "https://github.com/LuciNyan", + "contributions": [ + "code" + ] + }, + { + "login": "m1911star", + "name": "Horus", + "avatar_url": "https://avatars.githubusercontent.com/u/4948120?v=4", + "profile": "http://blog.ipili.me/", + "contributions": [ + "code", + "platform" + ] + }, + { + "login": "fanshyiis", + "name": "Super.x", + "avatar_url": "https://avatars.githubusercontent.com/u/15103283?v=4", + "profile": "https://segmentfault.com/u/qzuser_584786517d31a", + "contributions": [ + "code" + ] + }, + { + "login": "wangyu-1999", + "name": "Wang Yu", + "avatar_url": "https://avatars.githubusercontent.com/u/80874770?v=4", + "profile": "https://wangyu-1999.github.io/", + "contributions": [ + "code" + ] } ] } diff --git a/.github/deployment/Dockerfile-affine b/.github/deployment/Dockerfile-affine index 5cf4243eab..9d54eb0cf4 100644 --- a/.github/deployment/Dockerfile-affine +++ b/.github/deployment/Dockerfile-affine @@ -1,8 +1,9 @@ FROM node:16-alpine as builder WORKDIR /app COPY . . -RUN apk add g++ make python3 git libpng-dev -RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store && pnpm run build:local --skip-nx-cache +# RUN apk add g++ make python3 git libpng-dev +RUN apk add git +RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store --filter "!ligo-virgo-e2e" --filter "!keck" --filter "!venus" && pnpm run build:local --skip-nx-cache FROM node:16-alpine as relocate WORKDIR /app diff --git a/.github/deployment/Dockerfile-lisa b/.github/deployment/Dockerfile-lisa index 471e4d0d48..f469a7fce8 100644 --- a/.github/deployment/Dockerfile-lisa +++ b/.github/deployment/Dockerfile-lisa @@ -1,8 +1,9 @@ FROM node:16-alpine as builder WORKDIR /app COPY . . -RUN apk add g++ make python3 git libpng-dev -RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store && pnpm run build +# RUN apk add g++ make python3 git libpng-dev +RUN apk add git +RUN npm i -g pnpm@7 && pnpm i --frozen-lockfile --store=node_modules/.pnpm-store --filter "!ligo-virgo-e2e" --filter "!keck" --filter "!venus" && pnpm run build FROM node:16-alpine as relocate WORKDIR /app diff --git a/README.md b/README.md index 5352732aa9..8789712501 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,13 @@ AFFiNE.PRO
- The Next-Gen Knowledge Base to Replace Notion & Miro. + The Next-Gen Collaborative Knowledge Base

-Planning, Sorting and Creating all Together. Open-source, Privacy-First, and Free to use. +Open-source and privacy-first.
+A free replacement for Notion & Miro.

@@ -18,13 +19,14 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066 --> -[all-contributors-badge]: https://img.shields.io/badge/all_contributors-27-orange.svg?style=flat-square +[all-contributors-badge]: https://img.shields.io/badge/all_contributors-31-orange.svg?style=flat-square -[![affine.pro](https://img.shields.io/static/v1?label=live%20demo&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=→)](https://affine.pro) +[![affine.pro](https://img.shields.io/static/v1?label=live%20demo&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&color=orange&message=→)](https://livedemo.affine.pro) [![stars](https://img.shields.io/github/stars/toeverything/AFFiNE.svg?style=flat&logo=github&colorB=red&label=stars)](https://github.com/toeverything/AFFiNE) [![All Contributors][all-contributors-badge]](#contributors) +
[![Node](https://img.shields.io/badge/node->=16.0-success)](https://www.typescriptlang.org/) [![React](https://img.shields.io/badge/TypeScript-4.7-3178c6)](https://www.typescriptlang.org/) [![React](https://img.shields.io/badge/React-18-61dafb)](https://reactjs.org/) @@ -35,11 +37,11 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066

  - +   - +   - +  

@@ -48,96 +50,53 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066

affine_screen

-# Stay Up-to-Date and Support Us +# :star: Support Us and Keep Updated :star: ![952cd7a5-70fe-48ab-b74f-23981d94d2c5](https://user-images.githubusercontent.com/79301703/182365526-df074c64-cee4-45f6-b8e0-b912f17332c6.gif) -# How to use +# Getting Started -

-🥳🥳🥳 Our web live demo is ready! 🥳🥳🥳 -

+[![affine.pro](https://img.shields.io/static/v1?label=Try%20it%20Online&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAhpJREFUWEdjZEACtnl3MxgY/0YzMjAaMzAwcCLLUYH9/T/D/7MM/5mXHp6kPANmHiOI4Zx9Xfg3C+tKBob/zlSwiAgjGPey/vkdvneq5luwA+zy7+yhn+Vwv+89NFHFhREU7IyM/6YT4WyqK/n/nymT0Tb/1mFGBkYbqptOhIH/Gf4fYbTLv/2NBgmOCOvBSr6DHPCfWNW0UEe2A2x1uRlakiXBbtpx6jND+7KXZLmPbAdURokzeJjxwi31rrzH8OX7P5IdQbYDtnUoMXBzMMEt7Fj2imH7qU/0cQBy8MNsPHL5K0P13Of0cQB68MNsJScaSI4CHk4mhq3tSnCf3n36k0FZmh3Mn7L+DcPqgx9ICgWSHeBpxsdQESUGtgRk+eqDH+H8O09/MiR3P6atA1qTJRlsdLnhPgYlPOQQCW96wPDi3R+iHUFSCKAHP8wydEeREg0kOQA9+JOgwR1qL8CQEygC9jWp0UCSA+aVysIT3JqDHxgmr38DtlRCiIVhZZ0CPNhB6QDkEGIA0Q4gZAkuxxFyBNEOQA7ml+/+MIQ1PUAxG1kelAhB6YMYQLQDCPmQUAjhcgxRDiDWcEKOxOYIohyQGyjCEGIvANaPLfhhBiNHA6hmBBXNhABRDgCV/aBQAAFQpYMrn4PUgNTCACiXEMoNRDmAkC8okR8UDhjYRumAN8sHvGMCSkAD2jUDOWDAO6ewbDQQ3XMAy/oxKownQR0AAAAASUVORK5CYII=&message=%E2%86%92&style=for-the-badge)](https://affine.pro) No installation or registration required! Head over to our website and try it out now. -

-Start to play with AFFiNE web version on our landing page: -

- -

- check_live_demo -

- -If you have experience in front-end development, you may wish to refer to our [documentation](https://docs.affine.pro/affine/basic-documentation/contribute-to-affine) to learn more about deploying your own version or contributing further to development. -Also, thanks to Lee who has made a [desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out. -Please notice that AFFiNE is still under Alpha stage and is not ready for production use. - -# Table of contents - -- [Stay Up-to-Date and Support Us](#stay-up-to-date-and-support-us) -- [How to Use](#how-to-use) -- [Table of contents](#table-of-contents) - - [Shape your page](#shape-your-page) - - [Plan your task](#plan-your-task) - - [Sort your knowledge](#sort-your-knowledge) - - [Create your story](#create-your-story) -- [Documentation](#documentation) - - [Getting Started with development](#getting-started-with-development) -- [Roadmap](#roadmap) -- [Releases](#releases) -- [Feature requests](#feature-requests) -- [FAQ](#faq) -- [The Philosophy of AFFiNE](#the-philosophy-of-affine) -- [Community](#community) -- [Contributors](#contributors) -- [Acknowledgments](#acknowledgments) -- [License](#license) - -## Shape your page - -![546163d6-4c39-4128-ae7f-55d59bc3b76b](https://user-images.githubusercontent.com/79301703/182365611-b0ba3690-21c0-4d9b-bfbc-0bc15da05aeb.gif) - -## Plan your task - -![41a7b3a4-32f2-4d18-ac6b-57d1e1fda753](https://user-images.githubusercontent.com/79301703/182366553-1f6558a7-f17b-4611-ab95-aea3ec997154.gif) - -## Sort your knowledge - -![c9e1ff46-cec2-411b-b89d-6727a5e6f6c3](https://user-images.githubusercontent.com/79301703/182366602-08e44d28-a031-4097-9904-52fb9b1e9e17.gif) +Want to deploy it yourself? AFFiNE can run just about anywhere. Check our +[documentation](#documentation) +for more information. +

+⚠️ Please note that AFFiNE is still under active development and is not yet ready for production use. ⚠️ ## Create your story -We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features. -And when it comes to collaboration, these features are not just necessarily for teams -- you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators. -Affine is fully built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released. +There can be more than Notion and Miro. AFFiNE is a next-gen knowledge base that brings planning, sorting and creating all together. Privacy first, open-source, customizable and ready to use, built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, with full support for real-time collaboration through peer-to-peer technology. We don't think "privacy-first" is a good excuse for not supporting modern web features. +And when it comes to collaboration, these features are not just necessarily for teams - you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators. -# Documentation +### Shape your page -AFFiNE is not yet ready for production use. For installation, you may check how to build or deploy AFFiNE from our [quick-start](https://docs.affine.pro/affine/basic-documentation/contribute-to-affine/quick-start) guide. Alternatively, you can view our [full documentation](https://docs.affine.pro/affine/). +![546163d6-4c39-4128-ae7f-55d59bc3b76b](https://user-images.githubusercontent.com/79301703/182365611-b0ba3690-21c0-4d9b-bfbc-0bc15da05aeb.gif) -## Getting Started with development +### Plan your task -Please view the path Contribute-to-AFFiNE/Software-Contributions/Quick-Start in the documentation. +![41a7b3a4-32f2-4d18-ac6b-57d1e1fda753](https://user-images.githubusercontent.com/79301703/182366553-1f6558a7-f17b-4611-ab95-aea3ec997154.gif) -# Roadmap +### Sort your knowledge -Coming Soon... +![c9e1ff46-cec2-411b-b89d-6727a5e6f6c3](https://user-images.githubusercontent.com/79301703/182366602-08e44d28-a031-4097-9904-52fb9b1e9e17.gif) -# Releases +# Useful Links -Get our latest [release notes](https://github.com/toeverything/AFFiNE/wiki) from here. +- [AFFiNE Documentation](https://docs.affine.pro/affine/) - More detailed documentation on how to use and develop with AFFiNE -# Feature requests +- [Feature Roadmap](https://github.com/toeverything/AFFiNE/issues/293) - Looking for a feature? It might already be planned for release - you can check here +- [Release Notes](https://github.com/toeverything/AFFiNE/wiki) - Find out what changes we are making and how we are improving AFFiNE -Please go to [feature requests](https://github.com/toeverything/AFFiNE/issues). +- [Contributing Guide](/docs/CONTRIBUTING.md) - Want to help improve AFFiNE? You might not even need to write a line of code. Find out how you can contribute. +- [Code of Conduct](/docs/CODE_OF_CONDUCT.md) - How we promote and maintain a harassment-free experience for everyone in our community. -# FAQ +- AFFiNE Communities: [Discord](https://discord.gg/yz6tGVsf5p) | [Telegram](https://t.me/affineworkos) | [Twitter](https://twitter.com/AffineOfficial) | + [Medium](https://medium.com/@affineworkos) | [AFFiNE Blog](https://blog.affine.pro) -Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors. +# Contact Us -Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and the [AFFiNE Blog](https://blog.affine.pro/). +Feel free to send us an email: contact@toeverything.info # The Philosophy of AFFiNE @@ -160,7 +119,7 @@ That's why we are making AFFiNE. Some of the most important features are: - Data is always stored locally by default - CRDTs are applied so that peer-to-peer collaboration is possible. -We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth. +We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a single source of truth. We would like to give special thanks to the innovators and pioneers who greatly inspired us: @@ -182,12 +141,6 @@ We would also like to give thanks to open-source projects that make affine possi Thanks a lot to the community for providing such powerful and simple libraries, so that we can focus more on the implementation of the product logic, and we hope that in the future our projects will also provide a more easy-to-use knowledge base for everyone. -# Community - -For help, discussion about best practices, or any other conversation that would benefit from being searchable: - -[Discuss AFFiNE on GitHub](https://github.com/toeverything/AFFiNE/discussions) - # Contributors @@ -228,6 +181,12 @@ For help, discussion about best practices, or any other conversation that would
Simon Li

💻
Bob Hu

💻
Quavo

📖 +
子瞻 Luci

💻 + + +
Horus

💻 📦 +
Super.x

💻 +
Wang Yu

💻 @@ -240,4 +199,4 @@ For help, discussion about best practices, or any other conversation that would AFFiNE is distributed under the terms of MIT license. -See LICENSE for details. +See [LICENSE](/LICENSE) for details. diff --git a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx index a2440a1757..183adc6094 100644 --- a/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx +++ b/apps/ligo-virgo/src/pages/workspace/docs/Page.tsx @@ -175,7 +175,7 @@ const WorkspaceSidebar = styled('div')(({ theme }) => ({ width: 300, minWidth: 300, borderRadius: '0px 10px 10px 0px', - boxShadow: theme.affine.shadows.shadow1, + boxShadow: theme.affine.shadows.shadow2, backgroundColor: '#FFFFFF', transitionProperty: 'left', transitionDuration: '0.35s', diff --git a/apps/venus/package.json b/apps/venus/package.json index fe69da1b8e..321da2b80d 100644 --- a/apps/venus/package.json +++ b/apps/venus/package.json @@ -7,16 +7,18 @@ "keywords": [], "author": "DarkSky ", "dependencies": { - "@mui/joy": "^5.0.0-alpha.39", "@emotion/react": "^11.10.0", "@emotion/styled": "^11.10.0", - "lozad": "^1.16.0" + "@mui/joy": "^5.0.0-alpha.39", + "i18next": "^21.9.1", + "lozad": "^1.16.0", + "react-i18next": "^11.18.4" }, "devDependencies": { - "mini-css-extract-plugin": "^2.6.1", "image-minimizer-webpack-plugin": "^3.2.3", "imagemin": "^8.0.1", "imagemin-optipng": "^8.0.0", + "mini-css-extract-plugin": "^2.6.1", "webpack": "^5.74.0" } } diff --git a/apps/venus/src/app/i18n/index.ts b/apps/venus/src/app/i18n/index.ts new file mode 100644 index 0000000000..9ddaa8330d --- /dev/null +++ b/apps/venus/src/app/i18n/index.ts @@ -0,0 +1,25 @@ +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import en_US from './resources/en.json'; +import zh_CN from './resources/zh.json'; + +const resources = { + en: en_US, + zh: zh_CN, +} as const; + +i18next.use(initReactI18next).init({ + lng: 'en', + fallbackLng: 'en', + resources, + interpolation: { + escapeValue: false, // not needed for react as it escapes by default + }, +}); + +export const options = [ + { value: 'en', text: 'English' }, + { value: 'zh', text: '简体中文' }, +]; + +export { i18next }; diff --git a/apps/venus/src/app/i18n/resources/en.json b/apps/venus/src/app/i18n/resources/en.json new file mode 100644 index 0000000000..cd7a377352 --- /dev/null +++ b/apps/venus/src/app/i18n/resources/en.json @@ -0,0 +1,34 @@ +{ + "translation": { + "Open Source": "Open Source", + "Privacy First": "Privacy First", + "Alternative": "Alternative", + "Check GitHub": "Check GitHub", + "Try it Online": "Try it Online", + "description1": { + "part1": "Affine is the next-generation collaborative knowledge base for professionals.", + "part2": "It's not just a collection of Docs, whiteboard, and tables.", + "part3": "Transform any building block as you like.", + "part4": "Say goodbye to redundancy. Store your data once, and keep your data as you like it." + }, + "description2": { + "part1": "Shape Your Page", + "part2": "Docs, Kanbans, and Databases are all fully functional anywhere, anytime. A truly what-you-see-is-what-you-get environment for your data.", + "part3": "All pages come with a document (Paper Mode) and whiteboard (Edgeless Mode) view." + }, + "description3": { + "part1": "Plan Your Task", + "part2": "No more chaos managing multiple views.", + "part3": "Set a TODO with Markdown, and seamlessly edit it within a Kanban.", + "part4": "Managing multi-dimensional tables should be this simple - and now it is." + }, + "description4": { + "part1": "Privacy-first, and collaborative. No compromises whatsoever.", + "part2": "We don't like being locked-in, and neither should you. Privacy is at the foundation of everything we do, but it should not limit us that's why there are no compromises.", + "part3": "Your data is yours;it is always locally stored and secured - available to you always. While still being able to enjoy collaboration features such as real-time editing and sharing with others, without any cloud setup." + }, + "BuildFor": "Build for an open and semantic future", + "KeepUpdated": "Keep Updated on", + "Join": "Join Our Community" + } +} diff --git a/apps/venus/src/app/i18n/resources/zh.json b/apps/venus/src/app/i18n/resources/zh.json new file mode 100644 index 0000000000..eb4cf432dd --- /dev/null +++ b/apps/venus/src/app/i18n/resources/zh.json @@ -0,0 +1,34 @@ +{ + "translation": { + "Open Source": "开源", + "Privacy First": "隐私第一", + "Alternative": "的另一种选择", + "Check GitHub": "GitHub中查看", + "Try it Online": "在线上试试", + "description1": { + "part1": "Affine是面向专业人士的下一代协同知识库", + "part2": "它不仅仅是一个文档、白板和表格的集合。", + "part3": "可以根据需要转换任何构建块。", + "part4": "向冗余说再见吧。将数据存储一次,并保留您喜欢的数据。" + }, + "description2": { + "part1": "塑造您的页面", + "part2": "文档、看板和数据库在任何地方、任何时候都是完全可用的,一个真正的“所见即所得”的数据环境。", + "part3": "所有页面都有一个文档(纸张模式)和白板(无边缘模式)视图。" + }, + "description3": { + "part1": "计划您的任务", + "part2": "不再混乱地管理多个视图。", + "part3": "使用 Markdown 设置 TODO,并在看板中无缝地编辑它。", + "part4": "管理多维表格本应该就这么简单——现在的它就是这么简单。" + }, + "description4": { + "part1": "隐私第一,合作无间,绝不妥协。", + "part2": "我们不喜欢被关起来,您也不应该。隐私是我们做任何事情的基础,但它不应该限制我们,这就是为什么我们决不妥协。", + "part3": "您的数据是您自己的,它总是安全的在本地存储并随时供您使用。您不需要任何云设置就可以享受协作功能,即时编辑和与他人共享。" + }, + "BuildFor": "构建一个开放和语义化的未来", + "KeepUpdated": "持续更新在", + "Join": "加入我们的社区" + } +} diff --git a/apps/venus/src/app/index.tsx b/apps/venus/src/app/index.tsx index 71db02985e..e4d9c7854e 100644 --- a/apps/venus/src/app/index.tsx +++ b/apps/venus/src/app/index.tsx @@ -7,12 +7,15 @@ import GitHubIcon from '@mui/icons-material/GitHub'; import RedditIcon from '@mui/icons-material/Reddit'; import TelegramIcon from '@mui/icons-material/Telegram'; import { Box, Button, Container, Grid, SvgIcon, Typography } from '@mui/joy'; +import Option from '@mui/joy/Option'; +import Select from '@mui/joy/Select'; import { CssVarsProvider, styled } from '@mui/joy/styles'; import { LogoIcon } from '@toeverything/components/icons'; // eslint-disable-next-line no-restricted-imports import { useMediaQuery } from '@mui/material'; - +import { useTranslation } from 'react-i18next'; import CollaborationImage from './collaboration.png'; +import { options } from './i18n'; import LogoImage from './logo.png'; import PageImage from './page.png'; import ShapeImage from './shape.png'; @@ -199,6 +202,7 @@ const AffineImage = styled('img')({ const GitHub = (props: { center?: boolean; flat?: boolean }) => { const matches = useMediaQuery('(max-width: 1024px)'); + const { t } = useTranslation(); return ( ); }; const AFFiNEOnline = (props: { center?: boolean; flat?: boolean }) => { const matches = useMediaQuery('(max-width: 1024px)'); - + const { t } = useTranslation(); return ( ); }; export function App() { const matches = useMediaQuery('(max-width: 1024px)'); + const { t, i18n } = useTranslation(); + const changeLanguage = (event: any) => { + i18n.changeLanguage(event); + }; return ( Blog + - Open Source, + {t('Open Source')}, - Privacy First + {t('Privacy First')} @@ -413,7 +428,7 @@ export function App() { }, }} > - Alternative + {t('Alternative')} @@ -432,8 +447,7 @@ export function App() { fontWeight={'400'} sx={{ color: '#888' }} > - Affine is the next-generation collaborative - knowledge base for professionals. + {t('description1.part1')} @@ -488,8 +502,7 @@ export function App() { level={matches ? 'h2' : 'h1'} fontWeight={'bold'} > - It’s not just a collection of Docs, whiteboard, and - tables. + {t('description1.part2')} @@ -506,11 +519,10 @@ export function App() { }} > - Transform any building block as you like. + {t('description1.part3')} - Say goodbye to redundancy. Store your data once, and - keep your data as you like it. + {t('description1.part4')} @@ -547,23 +559,19 @@ export function App() { fontWeight={'bold'} style={{ marginBottom: '0.5em' }} > - Shape Your Page + {t('description2.part1')} - Docs, Kanbans, and Databases are all fully - functional anywhere, anytime. A truly - what-you-see-is-what-you-get environment for - your data. + {t('description2.part2')} - All pages come with a document (Paper Mode) and - whiteboard (Edgeless Mode) view. + {t('description2.part3')} @@ -625,27 +633,25 @@ export function App() { fontWeight={'bold'} style={{ marginBottom: '0.5em' }} > - Plan Your Task + {t('description3.part1')} - No more chaos managing multiple views. + {t('description3.part2')} - Set a TODO with Markdown, and seamlessly edit it - within a Kanban. + {t('description3.part3')} - Managing multi-dimensional tables should be this - simple – and now it is. + {t('description3.part4')} @@ -697,27 +703,19 @@ export function App() { fontWeight={'bold'} style={{ marginBottom: '0.5em' }} > - Privacy-first, and collaborative. No compromises - whatsoever. + {t('description4.part1')} - We don’t like being locked-in, and neither should - you. Privacy is at the foundation of everything we - do, but it should not limit us that’s+ why there are - no compromises. + {t('description4.part2')} - Your data is yours; it is always locally stored and - secured - available to you always. While still being - able to enjoy collaboration features such as - real-time editing and sharing with others, without - any cloud setup. + {t('description4.part3')} @@ -762,7 +760,7 @@ export function App() { }} > - Build for an open and semantic future + {t('BuildFor')} @@ -778,7 +776,7 @@ export function App() { > - Keep Updated on + {t('KeepUpdated')} @@ -795,7 +793,7 @@ export function App() { }} > - Join Our Community + {t('Join')} diff --git a/apps/venus/src/index.tsx b/apps/venus/src/index.tsx index f76f9a6f20..5fdc926e59 100644 --- a/apps/venus/src/index.tsx +++ b/apps/venus/src/index.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; +import './app/i18n'; import App from './app'; diff --git a/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 84% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index 7b7b63a1a5..94806e00f8 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Welcome to ourcontributing guide +# Welcome to our contributing guide Thank you for investing your time in contributing to our project! Any contribution you make will be reflected on our GitHub :sparkles:. @@ -10,7 +10,7 @@ Use the table of contents icon on the top left corner of this document to get to ## New contributor guide -To get an overview of the project, read the [README](README.md). Here are some resources to help you get started with open source contributions: +To get an overview of the project, read the [README](../README.md). Here are some resources to help you get started with open source contributions: - [Finding ways to contribute to open source on GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/finding-ways-to-contribute-to-open-source-on-github) - [Set up Git](https://docs.github.com/en/get-started/quickstart/set-up-git) @@ -19,19 +19,17 @@ To get an overview of the project, read the [README](README.md). Here are some r ## Getting started -To navigate our codebase with confidence, see [the introduction to working in the docs repository](/contributing/working-in-docs-repository.md) :confetti_ball:. For more information on how we write our markdown files, see [the GitHub Markdown reference](contributing/content-markup-reference.md). - -Check to see what [types of contributions](/contributing/types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:. +Check to see what [types of contributions](types-of-contributions.md) we accept before making changes. Some of them don't even require writing a single line of code :sparkles:. ### Issues -#### Create a new issue +#### Create a new issue or feature request If you spot a problem, [search if an issue already exists](https://docs.github.com/en/github/searching-for-information-on-github/searching-on-github/searching-issues-and-pull-requests#search-by-the-title-body-or-comments). If a related issue doesn't exist, you can open a new issue using a relevant [issue form](https://github.com/toeverything/AFFiNE/issues/new/choose). #### Solve an issue -Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See [Labels](/contributing/how-to-use-labels.md) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. +Scan through our [existing issues](https://github.com/toeverything/AFFiNE/issues) to find one that interests you. You can narrow down the search using `labels` as filters. See our [Labels](https://github.com/toeverything/AFFiNE/labels) for more information. As a general rule, we don’t assign issues to anyone. If you find an issue to work on, you are welcome to open a PR with a fix. ### Make Changes diff --git a/docs/types-of-contributions.md b/docs/types-of-contributions.md new file mode 100644 index 0000000000..fd448200e9 --- /dev/null +++ b/docs/types-of-contributions.md @@ -0,0 +1,31 @@ +# Types of contributions :memo: + +You can contribute to AFFiNE in several ways. This repo is a place to discuss and collaborate on AFFiNE! + +### :mega: Discussions + +Discussions are where we have conversations. + +If you'd like help troubleshooting a docs PR you're working on, have a great new idea, or want to share something amazing you've learned in our docs, join us in [discussions](https://github.com/toeverything/AFFiNE/discussions). + +### :lady_beetle: Issues + +[Issues](https://docs.github.com/en/github/managing-your-work-on-github/about-issues) are used to track tasks that contributors can help with. If an issue has a triage label, we haven't reviewed it yet, and you shouldn't begin work on it. + +If you've found something in the content or the website that should be updated, search open issues to see if someone else has reported the same thing. If it's something new, open an issue using a [template](https://github.com/toeverything/AFFiNE/issues/new/choose). We'll use the issue to have a conversation about the problem you want to fix. + +### :hammer_and_wrench: Pull requests + +A [pull request](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) is a way to suggest changes in our repository. When we merge those changes, they should be deployed to the live site within 24 hours. :earth_africa: +You can [create a new pull request](https://github.com/toeverything/AFFiNE/compare) and view [current pull requests](https://github.com/toeverything/AFFiNE/pulls). + +### :question: Support + +We are a small team working hard to keep up with the documentation demands of a continuously changing product. +You may be able to find additional help and information on our social media platforms and groups - the links to these can be found in our [README](../README.md). + +### :earth_asia: Translations + +AFFiNE is internationalized and available in multiple languages. The source content in this repository is written in English. We integrate with an external localization platform to work with the community in localizing the English content. + +**We do not currently offer this feature**, but we hope to in the future. diff --git a/libs/components/affine-board/src/Board.tsx b/libs/components/affine-board/src/Board.tsx index 5730649545..8c521705e7 100644 --- a/libs/components/affine-board/src/Board.tsx +++ b/libs/components/affine-board/src/Board.tsx @@ -22,7 +22,6 @@ const AffineBoard = ({ editor, }: AffineBoardProps & { editor: BlockEditor }) => { const [app, set_app] = useState(); - const [document] = useState(() => { return { ...deepCopy(TldrawApp.default_document), @@ -49,7 +48,7 @@ const AffineBoard = ({ }; }); - const shapes = useShapes(workspace, rootBlockId); + const { shapes } = useShapes(workspace, rootBlockId); useEffect(() => { if (app) { app.replacePageContent(shapes || {}, {}, {}); @@ -66,6 +65,9 @@ const AffineBoard = ({ onMount(app) { set_app(app); }, + async onPaste(e, data) { + console.log('e,data: ', e, data); + }, async onCopy(e, groupIds) { const clip = await editor.clipboard.clipboardUtils.getClipDataOfBlocksById( @@ -77,7 +79,7 @@ const AffineBoard = ({ clip.getData() ); }, - onChangePage(app, shapes, bindings, assets) { + async onChangePage(app, shapes, bindings, assets) { Promise.all( Object.entries(shapes).map(async ([id, shape]) => { if (shape === undefined) { @@ -106,7 +108,7 @@ const AffineBoard = ({ }); } shape.affineId = block.id; - return services.api.editorBlock.update({ + return await services.api.editorBlock.update({ workspace: shape.workspace, id: block.id, properties: { diff --git a/libs/components/affine-board/src/hooks/use-shapes.ts b/libs/components/affine-board/src/hooks/use-shapes.ts index 7d07a25957..23270b6c19 100644 --- a/libs/components/affine-board/src/hooks/use-shapes.ts +++ b/libs/components/affine-board/src/hooks/use-shapes.ts @@ -9,28 +9,37 @@ export const useShapes = (workspace: string, rootBlockId: string) => { const { pageClientWidth } = usePageClientWidth(); // page padding left and right total 300px const editorShapeInitSize = pageClientWidth - 300; - const [blocks, setBlocks] = useState(); + const [blocks, setBlocks] = useState<{ + shapes: [ReturnEditorBlock[]]; + }>(); useEffect(() => { - services.api.editorBlock - .get({ workspace, ids: [rootBlockId] }) - .then(async blockData => { - const shapes = await Promise.all( - (blockData?.[0]?.children || []).map(async childId => { - const childBlock = ( - await services.api.editorBlock.get({ - workspace, - ids: [childId], - }) - )?.[0]; - return childBlock; - }) - ); - setBlocks(shapes); + Promise.all([ + services.api.editorBlock + .get({ workspace, ids: [rootBlockId] }) + .then(async blockData => { + const shapes = await Promise.all( + (blockData?.[0]?.children || []).map(async childId => { + const childBlock = ( + await services.api.editorBlock.get({ + workspace, + ids: [childId], + }) + )?.[0]; + return childBlock; + }) + ); + return shapes; + }), + ]).then(shapes => { + setBlocks({ + shapes: shapes, }); + }); + let unobserve: () => void; services.api.editorBlock .observe({ workspace, id: rootBlockId }, async blockData => { - const shapes = await Promise.all( + Promise.all( (blockData?.children || []).map(async childId => { const childBlock = ( await services.api.editorBlock.get({ @@ -40,8 +49,11 @@ export const useShapes = (workspace: string, rootBlockId: string) => { )?.[0]; return childBlock; }) - ); - setBlocks(shapes); + ).then(shapes => { + setBlocks({ + shapes: [shapes], + }); + }); }) .then(cb => { unobserve = cb; @@ -53,8 +65,7 @@ export const useShapes = (workspace: string, rootBlockId: string) => { }, [workspace, rootBlockId]); let groupCount = 0; - - return blocks?.reduce((acc, block) => { + let blocksShapes = blocks?.shapes[0]?.reduce((acc, block) => { const shapeProps = block.properties.shapeProps?.value ? JSON.parse(block.properties.shapeProps.value) : {}; @@ -75,4 +86,8 @@ export const useShapes = (workspace: string, rootBlockId: string) => { return acc; }, {} as Record); + + return { + shapes: blocksShapes, + }; }; diff --git a/libs/components/affine-editor/src/Editor.tsx b/libs/components/affine-editor/src/Editor.tsx index c280bea984..f1edb2ce74 100644 --- a/libs/components/affine-editor/src/Editor.tsx +++ b/libs/components/affine-editor/src/Editor.tsx @@ -1,7 +1,6 @@ import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'; import { - RenderBlock, RenderRoot, type BlockEditor, } from '@toeverything/components/editor-core'; @@ -88,9 +87,7 @@ export const AffineEditor = forwardRef( editor={editor} editorElement={AffineEditor as any} scrollBlank={scrollBlank} - > - - + /> ); } ); diff --git a/libs/components/board-draw/src/TlDraw.tsx b/libs/components/board-draw/src/TlDraw.tsx index 338fffeeaf..a1444b76c0 100644 --- a/libs/components/board-draw/src/TlDraw.tsx +++ b/libs/components/board-draw/src/TlDraw.tsx @@ -13,6 +13,8 @@ import { TDStatus, } from '@toeverything/components/board-types'; import { styled } from '@toeverything/components/ui'; +// import { FocusButton } from '~components/FocusButton'; +import { usePageClientWidth } from '@toeverything/datasource/state'; import { memo, useEffect, @@ -22,22 +24,20 @@ import { useState, type RefObject, } from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { CommandPanel } from './components/command-panel'; +// import { TopPanel } from '~components/TopPanel'; +import { ContextMenu } from './components/context-menu'; +import { ErrorFallback } from './components/error-fallback'; +import { Loading } from './components/loading'; import { ToolsPanel } from './components/tools-panel'; +import { ZoomBar } from './components/zoom-bar'; import { TldrawContext, useKeyboardShortcuts, useStylesheet, useTldrawApp, } from './hooks'; -// import { TopPanel } from '~components/TopPanel'; -import { ContextMenu } from './components/context-menu'; -// import { FocusButton } from '~components/FocusButton'; -import { usePageClientWidth } from '@toeverything/datasource/state'; -import { ErrorBoundary } from 'react-error-boundary'; -import { CommandPanel } from './components/command-panel'; -import { ErrorFallback } from './components/error-fallback'; -import { Loading } from './components/loading'; -import { ZoomBar } from './components/zoom-bar'; export interface TldrawProps extends TldrawAppCtorProps { /** @@ -295,7 +295,6 @@ const InnerTldraw = memo(function InnerTldraw({ const pageState = document.pageStates[page.id]; const assets = document.assets; const { selectedIds } = pageState; - const isHideBoundsShape = selectedIds.length === 1 && page.shapes[selectedIds[0]] && @@ -363,7 +362,7 @@ const InnerTldraw = memo(function InnerTldraw({ // Hide bounds when not using the select tool, or when the only selected shape has handles const hideBounds = - (isInSession && app.session?.constructor.name !== 'BrushSession') || + (isInSession && app.session?.constructor.name === 'BrushSession') || !isSelecting || isHideBoundsShape || !!pageState.editingId; diff --git a/libs/components/board-draw/src/components/align-panel/index.tsx b/libs/components/board-draw/src/components/align-panel/index.tsx index d0ff48390d..ddc43b1254 100644 --- a/libs/components/board-draw/src/components/align-panel/index.tsx +++ b/libs/components/board-draw/src/components/align-panel/index.tsx @@ -56,7 +56,8 @@ const SelectableContainer = styled('div')<{ selected?: boolean }>( borderRadius: '5px', overflow: 'hidden', margin: '5px', - padding: '3px', + width: '24px', + height: '24px', cursor: 'pointer', boxSizing: 'border-box', '&:hover': { diff --git a/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx b/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx new file mode 100644 index 0000000000..84ef142ef0 --- /dev/null +++ b/libs/components/board-draw/src/components/command-panel/ArrowTo.tsx @@ -0,0 +1,80 @@ +import type { TldrawApp } from '@toeverything/components/board-state'; +import type { + ArrowBinding, + TDShape, +} from '@toeverything/components/board-types'; +import { ConnectorIcon } from '@toeverything/components/icons'; +import { IconButton, Popover, Tooltip } from '@toeverything/components/ui'; +import { useEffect, useState } from 'react'; +import { ListItemContainer, ListItemTitle } from './FontSizeConfig'; + +interface GroupAndUnGroupProps { + app: TldrawApp; + shapes: TDShape[]; +} + +export const ArrowTo = ({ app, shapes }: GroupAndUnGroupProps) => { + const [arrowToArr, setarrowToArr] = useState([]); + useEffect(() => { + let allShape = app.shapes; + let bindings = app.page.bindings; + let activeShape = shapes[0]; + let toNextShapBindings: ArrowBinding[] = []; + let bindingId = ''; + if (!activeShape) { + return; + } + Object.keys(bindings).forEach(key => { + if (bindings[key].toId === activeShape.id) { + bindingId = bindings[key].fromId; + } + }); + Object.keys(bindings).forEach(key => { + if (bindings[key].fromId === bindingId) { + toNextShapBindings.push(bindings[key]); + } + }); + let ArrowToArr: TDShape[] = []; + toNextShapBindings.forEach(binding => { + if (binding.toId !== activeShape.id) { + allShape.forEach(item => { + if (item.id === binding.toId) { + ArrowToArr.push(item); + } + }); + } + }); + setarrowToArr(ArrowToArr); + return () => {}; + }, [app.page.bindings]); + const jumpToNextShap = (shape: TDShape) => { + app.zoomToShapes([shape]); + }; + + return ( + + {arrowToArr.map((arrow: any) => { + return ( + jumpToNextShap(arrow)} + > + {arrow.name} + + ); + })} +
+ } + > + + + + + + + ); +}; diff --git a/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx b/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx index c07bfa88b4..031d4240a6 100644 --- a/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx +++ b/libs/components/board-draw/src/components/command-panel/CommandPanel.tsx @@ -106,6 +106,12 @@ export const CommandPanel = ({ app }: { app: TldrawApp }) => { shapes={config.deleteShapes.selectedShapes} > ) : null, + // toNextShap: ( + // + // ), }; const nodes = Object.entries(configNodes).filter(([key, node]) => !!node); diff --git a/libs/components/board-draw/src/components/command-panel/FontSizeConfig.tsx b/libs/components/board-draw/src/components/command-panel/FontSizeConfig.tsx index c35226911c..38fbbfae22 100644 --- a/libs/components/board-draw/src/components/command-panel/FontSizeConfig.tsx +++ b/libs/components/board-draw/src/components/command-panel/FontSizeConfig.tsx @@ -86,7 +86,7 @@ export const FontSizeConfig = ({ app, shapes }: FontSizeConfigProps) => { ); }; -const ListItemContainer = styled('div')(({ theme }) => ({ +export const ListItemContainer = styled('div')(({ theme }) => ({ display: 'flex', alignItems: 'center', cursor: 'pointer', @@ -100,7 +100,7 @@ const ListItemContainer = styled('div')(({ theme }) => ({ }, })); -const ListItemTitle = styled('span')(({ theme }) => ({ +export const ListItemTitle = styled('span')(({ theme }) => ({ marginLeft: '12px', color: theme.affine.palette.primaryText, })); diff --git a/libs/components/board-draw/src/components/tools-panel/LineTools.tsx b/libs/components/board-draw/src/components/tools-panel/LineTools.tsx index 3ef546ca3c..ce6cb97574 100644 --- a/libs/components/board-draw/src/components/tools-panel/LineTools.tsx +++ b/libs/components/board-draw/src/components/tools-panel/LineTools.tsx @@ -35,6 +35,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool; export const LineTools = ({ app }: { app: TldrawApp }) => { const activeTool = app.useStore(activeToolSelector); + const [visible, setVisible] = useState(false); const [lastActiveTool, setLastActiveTool] = useState( TDShapeType.Line @@ -51,8 +52,10 @@ export const LineTools = ({ app }: { app: TldrawApp }) => { return ( setVisible(prev => !prev)} + onClickAway={() => setVisible(false)} content={ {shapes.map(({ type, label, tooltip, icon: Icon }) => ( @@ -60,6 +63,7 @@ export const LineTools = ({ app }: { app: TldrawApp }) => { { app.selectTool(type); + setVisible(false); setLastActiveTool(type); }} > diff --git a/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx b/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx index eb2fb5c564..0c04126c95 100644 --- a/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx +++ b/libs/components/board-draw/src/components/tools-panel/ShapeTools.tsx @@ -71,6 +71,7 @@ const activeToolSelector = (s: TDSnapshot) => s.appState.activeTool; export const ShapeTools = ({ app }: { app: TldrawApp }) => { const activeTool = app.useStore(activeToolSelector); + const [visible, setVisible] = useState(false); const [lastActiveTool, setLastActiveTool] = useState( TDShapeType.Rectangle @@ -87,8 +88,10 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => { return ( setVisible(prev => !prev)} + onClickAway={() => setVisible(false)} content={ {shapes.map(({ type, label, tooltip, icon: Icon }) => ( @@ -96,6 +99,7 @@ export const ShapeTools = ({ app }: { app: TldrawApp }) => { { app.selectTool(type); + setVisible(false); setLastActiveTool(type); }} > diff --git a/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx b/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx index f659fdff1c..82a5315816 100644 --- a/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx +++ b/libs/components/board-draw/src/components/tools-panel/ToolsPanel.tsx @@ -12,12 +12,12 @@ import { import { IconButton, PopoverContainer, + styled, // MuiIconButton as IconButton, // MuiTooltip as Tooltip, Tooltip, useTheme, } from '@toeverything/components/ui'; -import style9 from 'style9'; import { TldrawApp } from '@toeverything/components/board-state'; import { @@ -90,8 +90,8 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => { }} direction="none" > -
-
+ + {tools.map( ({ type, @@ -127,24 +127,22 @@ export const ToolsPanel = ({ app }: { app: TldrawApp }) => { ) )} -
-
+ + ); }; -const styles = style9.create({ - container: { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - height: '100%', - }, - toolBar: { - display: 'flex', - flexDirection: 'column', - backgroundColor: '#ffffff', - borderRadius: '10px', - padding: '4px 4px', - }, +const Container = styled('div')({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + height: '100%', +}); +const ToolBar = styled('div')({ + display: 'flex', + flexDirection: 'column', + backgroundColor: '#ffffff', + borderRadius: '10px', + padding: '4px 4px', }); diff --git a/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx b/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx index 0536acfce1..c955afba0d 100644 --- a/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx +++ b/libs/components/board-draw/src/components/tools-panel/pen-tools/PenTools.tsx @@ -12,7 +12,7 @@ import { styled, Tooltip, } from '@toeverything/components/ui'; -import { ReactElement, type CSSProperties } from 'react'; +import { useState, type CSSProperties, type ReactElement } from 'react'; import { Palette } from '../../palette'; import { Pen } from './Pen'; @@ -106,6 +106,7 @@ const PENCIL_CONFIGS_MAP = PENCIL_CONFIGS.reduce< export const PenTools = ({ app }: { app: TldrawApp }) => { const appCurrentTool = app.useStore(state => state.appState.activeTool); + const [visible, setVisible] = useState(false); const chosenPen = PENCIL_CONFIGS.find(config => config.key === appCurrentTool) || PENCIL_CONFIGS[0]; @@ -134,8 +135,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => { return ( setVisible(prev => !prev)} + onClickAway={() => setVisible(false)} content={ @@ -153,7 +156,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => { name={title} primaryColor={color_vars['--color-0']} secondaryColor={color_vars['--color-1']} - onClick={() => setPen(key)} + onClick={() => { + setVisible(false); + setPen(key); + }} /> ); } @@ -163,7 +169,10 @@ export const PenTools = ({ app }: { app: TldrawApp }) => { setPenColor(color)} + onSelect={color => { + setVisible(false); + setPenColor(color); + }} />
diff --git a/libs/components/board-shapes/src/arrow-util/arrow-util.tsx b/libs/components/board-shapes/src/arrow-util/arrow-util.tsx index 42045918f4..dace7eeb38 100644 --- a/libs/components/board-shapes/src/arrow-util/arrow-util.tsx +++ b/libs/components/board-shapes/src/arrow-util/arrow-util.tsx @@ -42,7 +42,7 @@ type E = HTMLDivElement; export class ArrowUtil extends TDShapeUtil { type = TDShapeType.Arrow as const; - override hideBounds = true; + override hideBounds = false; override canEdit = true; diff --git a/libs/components/board-shapes/src/editor-util/EditorUtil.tsx b/libs/components/board-shapes/src/editor-util/EditorUtil.tsx index cb6e56275a..805ada0623 100644 --- a/libs/components/board-shapes/src/editor-util/EditorUtil.tsx +++ b/libs/components/board-shapes/src/editor-util/EditorUtil.tsx @@ -8,6 +8,7 @@ import { TDShapeType, TransformInfo, } from '@toeverything/components/board-types'; +import type { BlockEditor } from '@toeverything/components/editor-core'; import { MIN_PAGE_WIDTH } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; import type { MouseEvent, SyntheticEvent } from 'react'; @@ -66,6 +67,7 @@ export class EditorUtil extends TDShapeUtil { Component = TDShapeUtil.Component( ({ shape, meta: { app }, events, isEditing, onShapeChange }, ref) => { const containerRef = useRef(); + const editorRef = useRef(); const { workspace, rootBlockId, @@ -135,6 +137,18 @@ export class EditorUtil extends TDShapeUtil { } }, [app, state, shape.id, editingText, editingId]); + useEffect(() => { + (async () => { + if (isEditing) { + const lastBlock = + await editorRef.current.getLastBlock(); + editorRef.current.selectionManager.activeNodeByNodeId( + lastBlock.id + ); + } + })(); + }, [isEditing]); + const onMouseDown = useCallback( (e: MouseEvent) => { if (e.detail === 2) { @@ -159,6 +173,7 @@ export class EditorUtil extends TDShapeUtil { rootBlockId={rootBlockId} scrollBlank={false} isEdgeless + ref={editorRef} /> {editingText ? null : } diff --git a/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx b/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx index 77ee2cec6a..36d4475b92 100644 --- a/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx +++ b/libs/components/board-shapes/src/rectangle-util/components/DashedRectangle.tsx @@ -1,6 +1,8 @@ import { Utils } from '@tldraw/core'; -import type { ShapeStyles } from '@toeverything/components/board-types'; -import { BINDING_DISTANCE } from '@toeverything/components/board-types'; +import { + BINDING_DISTANCE, + ShapeStyles, +} from '@toeverything/components/board-types'; import * as React from 'react'; import { getShapeStyle } from '../../shared'; @@ -56,23 +58,20 @@ export const DashedRectangle = React.memo(function DashedRectangle({ return ( <> {style.isFilled && ( diff --git a/libs/components/board-state/src/tldraw-app.ts b/libs/components/board-state/src/tldraw-app.ts index 075c4e6f06..1e95912db5 100644 --- a/libs/components/board-state/src/tldraw-app.ts +++ b/libs/components/board-state/src/tldraw-app.ts @@ -175,6 +175,10 @@ interface TDCallbacks { * (optional) A callback to run when the shape is copied. */ onCopy?: (e: ClipboardEvent, ids: string[]) => void; + /** + * (optional) A callback to run when the shape is paste. + */ + onPaste?: (e: ClipboardEvent, data: any) => void; } export interface TldrawAppCtorProps { @@ -444,7 +448,6 @@ export class TldrawApp extends StateManager { if (visitedShapes.has(fromShape)) { return; } - // We only need to update the binding's "from" shape (an arrow) const fromDelta = TLDR.update_arrow_bindings( page, @@ -627,7 +630,7 @@ export class TldrawApp extends StateManager { private prev_bindings = this.page.bindings; private prev_assets = this.document.assets; - private _broadcastPageChanges = () => { + private _broadcastPageChanges = async () => { const visited = new Set(); const changedShapes: Record = {}; @@ -684,7 +687,7 @@ export class TldrawApp extends StateManager { Object.keys(changedAssets).length > 0 ) { this.just_sent = true; - this.callbacks.onChangePage?.( + await this.callbacks.onChangePage?.( this, changedShapes, changedBindings, @@ -860,7 +863,6 @@ export class TldrawApp extends StateManager { if (!page.bindings[binding.id]) { return; } - const fromShape = page.shapes[binding.fromId] as ArrowShape; if (visitedShapes.has(fromShape)) { @@ -868,6 +870,7 @@ export class TldrawApp extends StateManager { } // We only need to update the binding's "from" shape (an arrow) + const fromDelta = TLDR.update_arrow_bindings(page, fromShape); visitedShapes.add(fromShape); @@ -1957,6 +1960,53 @@ export class TldrawApp extends StateManager { paste = async (point?: number[], e?: ClipboardEvent) => { if (this.readOnly) return; + if (e) { + const data = e.clipboardData?.getData('text/html'); + const paste_as_html = (html: string) => { + try { + const maybeJson = html.match(/(.*)<\/tldraw>/)?.[1]; + + if (!maybeJson) return; + + const json: { + type: string; + shapes: TDShape[]; + bindings: TDBinding[]; + assets: TDAsset[]; + } = JSON.parse(maybeJson); + return json; + } catch (e) { + return; + } + }; + this.callbacks.onPaste?.(e, paste_as_html(data)); + return this; + } + + const paste_as_html = (html: string) => { + try { + const maybeJson = html.match(/(.*)<\/tldraw>/)?.[1]; + + if (!maybeJson) return; + + const json: { + type: string; + shapes: TDShape[]; + bindings: TDBinding[]; + assets: TDAsset[]; + } = JSON.parse(maybeJson); + + if (json.type === 'tldr/clipboard') { + pasteInCurrentPage(json.shapes, json.bindings, json.assets); + return; + } else { + throw Error('Not tldraw data!'); + } + } catch (e) { + pasteTextAsShape(html); + return; + } + }; const pasteInCurrentPage = ( shapes: TDShape[], bindings: TDBinding[], @@ -2108,31 +2158,6 @@ export class TldrawApp extends StateManager { // this.select(shapeId); }; - const paste_as_html = (html: string) => { - try { - const maybeJson = html.match(/(.*)<\/tldraw>/)?.[1]; - - if (!maybeJson) return; - - const json: { - type: string; - shapes: TDShape[]; - bindings: TDBinding[]; - assets: TDAsset[]; - } = JSON.parse(maybeJson); - - if (json.type === 'tldr/clipboard') { - pasteInCurrentPage(json.shapes, json.bindings, json.assets); - return; - } else { - throw Error('Not tldraw data!'); - } - } catch (e) { - pasteTextAsShape(html); - return; - } - }; - if (e !== undefined) { const items: DataTransferItemList = e.clipboardData?.items ?? ({} as DataTransferItemList); @@ -3854,7 +3879,7 @@ export class TldrawApp extends StateManager { private get_viewbox_from_svg = (svgStr: string | ArrayBuffer | null) => { if (typeof svgStr === 'string') { - let viewBox = new DOMParser().parseFromString(svgStr, 'text/xml'); + const viewBox = new DOMParser().parseFromString(svgStr, 'text/xml'); return viewBox.children[0].getAttribute('viewBox'); } @@ -4138,7 +4163,7 @@ export class TldrawApp extends StateManager { }; onPointerDown: TLPointerEventHandler = (info, e) => { - if (e.buttons === 4) { + if (e.button === 1) { this.patchState({ settings: { forcePanning: true, @@ -4155,6 +4180,13 @@ export class TldrawApp extends StateManager { }; onPointerUp: TLPointerEventHandler = (info, e) => { + if (e.button === 1) { + this.patchState({ + settings: { + forcePanning: false, + }, + }); + } this.isPointing = false; this.updateInputs(info, e); this.currentTool.onPointerUp?.(info, e); diff --git a/libs/components/common/src/lib/index.ts b/libs/components/common/src/lib/index.ts index 9a1eea5191..f42927223e 100644 --- a/libs/components/common/src/lib/index.ts +++ b/libs/components/common/src/lib/index.ts @@ -1,5 +1,6 @@ import { BaseEditor } from 'slate'; import { ReactEditor } from 'slate-react'; +import { DoubleLinkElement } from './text/plugins/DoubleLink'; import { LinkElement } from './text/plugins/link'; import { RefLinkElement } from './text/plugins/reflink'; @@ -8,7 +9,8 @@ export type CustomElement = | { type: string; children: CustomElement[] } | CustomText | LinkElement - | RefLinkElement; + | RefLinkElement + | DoubleLinkElement; declare module 'slate' { interface CustomTypes { Editor: BaseEditor & ReactEditor; @@ -19,35 +21,32 @@ declare module 'slate' { export { BlockPreview, StyledBlockPreview } from './block-preview'; export { default as Button } from './button'; -export type { CommonListItem } from './list'; -export { CommonList, BackLink, commonListContainer } from './list'; -export * from './Logo'; -export { default as Toolbar } from './toolbar'; export { CollapsibleTitle } from './collapsible-title'; - -export * from './text'; - +export * from './comming-soon/CommingSoon'; export { - NewpageIcon, + AddIcon, ClockIcon, - ViewSidebarIcon, + CloseIcon, + CodeBlockInlineIcon, + DocumentIcon, + FilterIcon, + FullScreenIcon, + HighlighterDuotoneIcon, + KanbanIcon, ListIcon, - SpaceIcon, + NewpageIcon, + PagesIcon, PencilDotDuotoneIcon, PencilDuotoneIcon, - HighlighterDuotoneIcon, - CodeBlockInlineIcon, - PagesIcon, - CloseIcon, - DocumentIcon, - TodoListIcon, - KanbanIcon, - TableIcon, - AddIcon, - FilterIcon, SorterIcon, - FullScreenIcon, + SpaceIcon, + TableIcon, + TodoListIcon, UnGroupIcon, + ViewSidebarIcon, } from './icon'; - -export * from './comming-soon/CommingSoon'; +export { BackLink, CommonList, commonListContainer } from './list'; +export type { CommonListItem } from './list'; +export * from './Logo'; +export * from './text'; +export { default as Toolbar } from './toolbar'; diff --git a/libs/components/common/src/lib/list/index.tsx b/libs/components/common/src/lib/list/index.tsx index 8f57a1d871..c00a4e1415 100644 --- a/libs/components/common/src/lib/list/index.tsx +++ b/libs/components/common/src/lib/list/index.tsx @@ -1,8 +1,4 @@ -import { useState } from 'react'; -import { useNavigate } from 'react-router'; -import clsx from 'clsx'; -import style9 from 'style9'; - +import { BackwardUndoIcon } from '@toeverything/components/icons'; import { BaseButton, ListButton, @@ -12,9 +8,11 @@ import { } from '@toeverything/components/ui'; // eslint-disable-next-line @nrwl/nx/enforce-module-boundaries import { BlockSearchItem } from '@toeverything/datasource/jwt'; - +import clsx from 'clsx'; +import { useState } from 'react'; +import { useNavigate } from 'react-router'; +import style9 from 'style9'; import { BlockPreview } from '../block-preview'; -import { BackwardUndoIcon } from '@toeverything/components/icons'; export const commonListContainer = 'commonListContainer'; @@ -28,6 +26,7 @@ export type CommonListItem = { divider?: string; content?: Content; block?: BlockSearchItem; + renderCustom?: (props: CommonListItem) => JSX.Element; }; type MenuItemsProps = { @@ -45,7 +44,7 @@ export const CommonList = (props: MenuItemsProps) => { // ]); // TODO Insert bidirectional link to be developed const JSONUnsupportedBlockTypes = ['page']; - let usedItems = items.filter(item => { + const usedItems = items.filter(item => { return !JSONUnsupportedBlockTypes.includes(item?.content?.id); }); return ( @@ -58,7 +57,9 @@ export const CommonList = (props: MenuItemsProps) => { > {usedItems?.length ? ( usedItems.map((item, idx) => { - if (item.block) { + if (item.renderCustom) { + return item.renderCustom(item); + } else if (item.block) { return ( { switch (element.type) { case 'link': { + if (element.linkType === 'doubleLink') { + return ; + } return ( { backgroundColor: '#F2F5F9', borderRadius: '5px', color: '#3A4C5C', - padding: '3px 8px', + padding: '1px 8px', margin: '0 2px', }} > @@ -839,6 +841,14 @@ const EditorLeaf = ({ attributes, children, leaf }: any) => { } } + if (leaf.doubleLinkSearch) { + customChildren = ( + + {customChildren} + + ); + } + customChildren = ( {customChildren} diff --git a/libs/components/common/src/lib/text/plugins/DoubleLink.tsx b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx new file mode 100644 index 0000000000..e4bcb9bec2 --- /dev/null +++ b/libs/components/common/src/lib/text/plugins/DoubleLink.tsx @@ -0,0 +1,40 @@ +import { PagesIcon } from '@toeverything/components/icons'; +import React, { useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Descendant } from 'slate'; +import { RenderElementProps } from 'slate-react'; + +export type DoubleLinkElement = { + type: 'link'; + workspaceId: string; + blockId: string; + children: Descendant[]; + id: string; +}; + +export const DoubleLinkComponent = (props: RenderElementProps) => { + const { attributes, children, element } = props; + const doubleLinkElement = element as DoubleLinkElement; + const navigate = useNavigate(); + + const handleClickLinkText = useCallback( + (event: React.MouseEvent) => { + const { workspaceId, blockId } = doubleLinkElement; + navigate(`/${workspaceId}/${blockId}`); + }, + [doubleLinkElement, navigate] + ); + + return ( + + + + {children} + + + ); +}; diff --git a/libs/components/common/src/lib/text/slate-utils.ts b/libs/components/common/src/lib/text/slate-utils.ts index 160f1a416b..1f3fd1fac2 100644 --- a/libs/components/common/src/lib/text/slate-utils.ts +++ b/libs/components/common/src/lib/text/slate-utils.ts @@ -11,7 +11,7 @@ import { Transforms, } from 'slate'; import { ReactEditor } from 'slate-react'; - +import type { CustomElement } from '..'; import { fontBgColorPalette, fontColorPalette, @@ -21,6 +21,7 @@ import { import { getCommentsIdsOnTextNode, getEditorMarkForCommentId, + getRandomString, MARKDOWN_STYLE_MAP, MatchRes, } from './utils'; @@ -247,7 +248,11 @@ Editor.insertText = function ( const { path, offset } = at; if (text.length > 0) { const marks = Editor.marks(editor); - if (text === '\u0020' && Object.keys(marks).length) { + if ( + text === '\u0020' && + Object.keys(marks).length && + !(marks as any).doubleLinkSearch + ) { // If the input is a space and the mark has content, remove the mark const newPath = [path[0], path[1] + 1]; const newPoint = { @@ -594,8 +599,13 @@ class SlateUtils { const fragmentChildren = firstFragment.children; - const textChildren: Text[] = []; - for (const child of fragmentChildren) { + const textChildren: CustomElement[] = []; + for (let i = 0; i < fragmentChildren.length; i++) { + const child = fragmentChildren[i]; + if ('type' in child && child.type === 'link') { + i !== fragmentChildren.length - 1 && textChildren.push(child); + continue; + } if (!('text' in child)) { console.error('Debug information:', point1, point2, fragment); throw new Error('Fragment exists nested!'); @@ -1119,34 +1129,130 @@ class SlateUtils { }); } - public insertReference(reference: string) { - try { - // Transforms.setSelection(this.editor, this.getEndSelection()); - const { anchor, focus } = this.getEndSelection(); - Transforms.insertNodes( + public setDoubleLinkSearchSlash(point: Point) { + const str = Editor.string(this.editor, { + anchor: this.getStart(), + focus: point, + }); + if (str.endsWith('[[')) { + Transforms.select(this.editor, { + anchor: point, + focus: Object.assign({}, point, { + offset: point.offset - 2, + }), + }); + Editor.addMark(this.editor, 'doubleLinkSearch', true); + Transforms.select(this.editor, { + anchor: this.editor.selection.anchor, + focus: this.editor.selection.anchor, + }); + } + } + + public getDoubleLinkSearchSlashText() { + const nodes = Editor.nodes(this.editor, { + at: [], + //@ts-ignore + match: node => !!node.doubleLinkSearch, + }); + const searchNode = nodes.next().value; + if (searchNode && (searchNode[0] as { text?: string }).text) { + return (searchNode[0] as { text?: string }).text; + } + return ''; + } + + public setSelectDoubleLinkSearchSlash() { + const nodes = Editor.nodes(this.editor, { + at: [], + //@ts-ignore + match: node => !!node.doubleLinkSearch, + }); + const searchNode = nodes.next().value; + if (searchNode) { + const text = (searchNode[0] as { text?: string })?.text || ''; + const path = searchNode[1]; + const anchor = Editor.before( this.editor, { - type: 'reflink', - reference, - children: [], + path, + offset: 1, }, - { at: focus || anchor } + { + unit: 'offset', + } ); - - // requestAnimationFrame(() => { - // console.log(this.editor.selection, this.editor.insertNode); - // this.editor.insertNode({ - // type: 'reflink', - // reference, - // children: [{ text: '' }] - // }); - // // Transforms.select(); - // }); - } catch (e) { - console.log(e); + const focus = Editor.after( + this.editor, + { + path, + offset: text.length - 1, + }, + { + unit: 'offset', + } + ); + Transforms.select(this.editor, { anchor, focus }); } } + public removeDoubleLinkSearchSlash(isRemoveSlash?: boolean) { + if (isRemoveSlash) { + const nodes = Editor.nodes(this.editor, { + at: [], + //@ts-ignore + match: node => !!node.doubleLinkSearch, + }); + const searchNode = nodes.next().value; + if (searchNode) { + const text = (searchNode[0] as { text?: string })?.text || ''; + if (text.startsWith('[[')) { + const path = searchNode[1]; + Transforms.delete(this.editor, { + at: { + path, + offset: 0, + }, + distance: text.length, + unit: 'character', + }); + } + } + } + Transforms.setNodes( + this.editor, + { doubleLinkSearch: null } as Partial, + { + at: [], + match: node => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + return !!node.doubleLinkSearch; + }, + } + ); + this.editor.removeMark('doubleLinkSearch'); + } + + public insertDoubleLink( + workspaceId: string, + linkBlockId: string, + children: any[] + ) { + const link = { + type: 'link', + linkType: 'doubleLink', + workspaceId: workspaceId, + blockId: linkBlockId, + children: children, + id: getRandomString('link'), + }; + Transforms.insertNodes(this.editor, link); + requestAnimationFrame(() => { + ReactEditor.focus(this.editor); + }); + } + /** todo improve if selection is collapsed */ public getCommentsIdsBySelection() { const commentedTextNodes = Editor.nodes(this.editor, { diff --git a/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx b/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx index 9f77eda6cb..b2f76e46f9 100644 --- a/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx +++ b/libs/components/editor-blocks/src/blocks/bullet/BulletView.tsx @@ -1,29 +1,28 @@ import type { TextProps } from '@toeverything/components/common'; import { ContentColumnValue, - services, Protocol, + services, } from '@toeverything/datasource/db-service'; import { type CreateView } from '@toeverything/framework/virgo'; import { useEffect, useRef, useState } from 'react'; +import { + BlockPendantProvider, + RenderBlockChildren, + supportChildren, + useOnSelect, +} from '@toeverything/components/editor-core'; +import { styled } from '@toeverything/components/ui'; +import { BlockContainer } from '../../components/BlockContainer'; +import { List } from '../../components/style-container'; import { TextManage, type ExtendedTextUtils, } from '../../components/text-manage'; import { tabBlock } from '../../utils/indent'; +import { BulletIcon, getChildrenType, NumberType } from './data'; import { BulletBlock, BulletProperties } from './types'; -import { - supportChildren, - RenderBlockChildren, - useOnSelect, - BlockPendantProvider, -} from '@toeverything/components/editor-core'; -import { List } from '../../components/style-container'; -import { getChildrenType, BulletIcon, NumberType } from './data'; -import { IndentWrapper } from '../../components/IndentWrapper'; -import { BlockContainer } from '../../components/BlockContainer'; -import { styled } from '@toeverything/components/ui'; export const defaultBulletProps: BulletProperties = { text: { value: [{ text: '' }] }, @@ -208,9 +207,7 @@ export const BulletView = ({ block, editor }: CreateView) => {
- - - + ); }; diff --git a/libs/components/editor-blocks/src/blocks/bullet/index.ts b/libs/components/editor-blocks/src/blocks/bullet/index.ts index 212575da31..3dc79c15dc 100644 --- a/libs/components/editor-blocks/src/blocks/bullet/index.ts +++ b/libs/components/editor-blocks/src/blocks/bullet/index.ts @@ -1,16 +1,18 @@ +import { Protocol } from '@toeverything/datasource/db-service'; import { AsyncBlock, BaseView, BlockEditor, HTML2BlockResult, } from '@toeverything/framework/virgo'; -import { Protocol } from '@toeverything/datasource/db-service'; -import { defaultBulletProps, BulletView } from './BulletView'; + import { Block2HtmlProps, commonBlock2HtmlContent, commonHTML2block, } from '../../utils/commonBlockClip'; +import { BulletView, defaultBulletProps } from './BulletView'; + export class BulletBlock extends BaseView { public type = Protocol.Block.Type.bullet; diff --git a/libs/components/editor-blocks/src/blocks/code/CodeView.tsx b/libs/components/editor-blocks/src/blocks/code/CodeView.tsx index 2739c8a70c..1bcedf4251 100644 --- a/libs/components/editor-blocks/src/blocks/code/CodeView.tsx +++ b/libs/components/editor-blocks/src/blocks/code/CodeView.tsx @@ -1,58 +1,54 @@ -import { useState, useRef, useEffect } from 'react'; -import { StyleWithAtRules } from 'style9'; - -import { CreateView } from '@toeverything/framework/virgo'; -import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror'; -import { styled } from '@toeverything/components/ui'; - -import { javascript } from '@codemirror/lang-javascript'; -import { html } from '@codemirror/lang-html'; -import { css } from '@codemirror/lang-css'; -import { json } from '@codemirror/lang-json'; -import { python } from '@codemirror/lang-python'; -import { markdown } from '@codemirror/lang-markdown'; -import { xml } from '@codemirror/lang-xml'; -import { sql, MySQL, PostgreSQL } from '@codemirror/lang-sql'; -import { java } from '@codemirror/lang-java'; -import { rust } from '@codemirror/lang-rust'; import { cpp } from '@codemirror/lang-cpp'; +import { css } from '@codemirror/lang-css'; +import { html } from '@codemirror/lang-html'; +import { java } from '@codemirror/lang-java'; +import { javascript } from '@codemirror/lang-javascript'; +import { json } from '@codemirror/lang-json'; import { lezer } from '@codemirror/lang-lezer'; +import { markdown } from '@codemirror/lang-markdown'; import { php } from '@codemirror/lang-php'; +import { python } from '@codemirror/lang-python'; +import { rust } from '@codemirror/lang-rust'; +import { MySQL, PostgreSQL, sql } from '@codemirror/lang-sql'; +import { xml } from '@codemirror/lang-xml'; import { StreamLanguage } from '@codemirror/language'; -import { go } from '@codemirror/legacy-modes/mode/go'; -import { ruby } from '@codemirror/legacy-modes/mode/ruby'; -import { shell } from '@codemirror/legacy-modes/mode/shell'; -import { lua } from '@codemirror/legacy-modes/mode/lua'; -import { swift } from '@codemirror/legacy-modes/mode/swift'; -import { tcl } from '@codemirror/legacy-modes/mode/tcl'; -import { yaml } from '@codemirror/legacy-modes/mode/yaml'; -import { vb } from '@codemirror/legacy-modes/mode/vb'; -import { powerShell } from '@codemirror/legacy-modes/mode/powershell'; import { brainfuck } from '@codemirror/legacy-modes/mode/brainfuck'; -import { stylus } from '@codemirror/legacy-modes/mode/stylus'; -import { erlang } from '@codemirror/legacy-modes/mode/erlang'; -import { elixir } from 'codemirror-lang-elixir'; -import { nginx } from '@codemirror/legacy-modes/mode/nginx'; -import { perl } from '@codemirror/legacy-modes/mode/perl'; -import { pascal } from '@codemirror/legacy-modes/mode/pascal'; -import { liveScript } from '@codemirror/legacy-modes/mode/livescript'; -import { scheme } from '@codemirror/legacy-modes/mode/scheme'; -import { toml } from '@codemirror/legacy-modes/mode/toml'; -import { vbScript } from '@codemirror/legacy-modes/mode/vbscript'; import { clojure } from '@codemirror/legacy-modes/mode/clojure'; import { coffeeScript } from '@codemirror/legacy-modes/mode/coffeescript'; import { dockerFile } from '@codemirror/legacy-modes/mode/dockerfile'; +import { erlang } from '@codemirror/legacy-modes/mode/erlang'; +import { go } from '@codemirror/legacy-modes/mode/go'; import { julia } from '@codemirror/legacy-modes/mode/julia'; +import { liveScript } from '@codemirror/legacy-modes/mode/livescript'; +import { lua } from '@codemirror/legacy-modes/mode/lua'; +import { nginx } from '@codemirror/legacy-modes/mode/nginx'; +import { pascal } from '@codemirror/legacy-modes/mode/pascal'; +import { perl } from '@codemirror/legacy-modes/mode/perl'; +import { powerShell } from '@codemirror/legacy-modes/mode/powershell'; import { r } from '@codemirror/legacy-modes/mode/r'; +import { ruby } from '@codemirror/legacy-modes/mode/ruby'; +import { scheme } from '@codemirror/legacy-modes/mode/scheme'; +import { shell } from '@codemirror/legacy-modes/mode/shell'; +import { stylus } from '@codemirror/legacy-modes/mode/stylus'; +import { swift } from '@codemirror/legacy-modes/mode/swift'; +import { tcl } from '@codemirror/legacy-modes/mode/tcl'; +import { toml } from '@codemirror/legacy-modes/mode/toml'; +import { vb } from '@codemirror/legacy-modes/mode/vb'; +import { vbScript } from '@codemirror/legacy-modes/mode/vbscript'; +import { yaml } from '@codemirror/legacy-modes/mode/yaml'; import { Extension } from '@codemirror/state'; -import { Option, Select } from '@toeverything/components/ui'; - import { - useOnSelect, BlockPendantProvider, + useOnSelect, } from '@toeverything/components/editor-core'; -import { copyToClipboard } from '@toeverything/utils'; import { DuplicateIcon } from '@toeverything/components/icons'; +import { Option, Select, styled } from '@toeverything/components/ui'; +import { CreateView } from '@toeverything/framework/virgo'; +import { copyToClipboard } from '@toeverything/utils'; +import { elixir } from 'codemirror-lang-elixir'; +import { useEffect, useRef, useState } from 'react'; +import { StyleWithAtRules } from 'style9'; +import CodeMirror, { ReactCodeMirrorRef } from './CodeMirror'; interface CreateCodeView extends CreateView { style9?: StyleWithAtRules; @@ -110,13 +106,15 @@ const CodeBlock = styled('div')(({ theme }) => ({ borderRadius: theme.affine.shape.borderRadius, '&:hover': { '.operation': { - display: 'flex', + opacity: 1, }, }, '.operation': { display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', + opacity: 0, + transition: 'opacity 1.5s', }, '.copy-block': { padding: '0px 10px', diff --git a/libs/components/editor-blocks/src/blocks/code/index.ts b/libs/components/editor-blocks/src/blocks/code/index.ts index 306d909716..23e87d9b7e 100644 --- a/libs/components/editor-blocks/src/blocks/code/index.ts +++ b/libs/components/editor-blocks/src/blocks/code/index.ts @@ -15,7 +15,7 @@ import { export class CodeBlock extends BaseView { type = Protocol.Block.Type.code; public override selectable = true; - public override activatable = false; + public override editable = false; // View = CodeView; View = CodeView; override async onCreate(block: AsyncBlock): Promise { diff --git a/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx b/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx index 86881478ff..86b43908d1 100644 --- a/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx +++ b/libs/components/editor-blocks/src/blocks/embed-link/EmbedLinkView.tsx @@ -1,12 +1,12 @@ -import { useState } from 'react'; -import { CreateView } from '@toeverything/framework/virgo'; import { BlockPendantProvider, useOnSelect, } from '@toeverything/components/editor-core'; -import { Upload } from '../../components/upload/upload'; +import { CreateView } from '@toeverything/framework/virgo'; +import { useState } from 'react'; import { SourceView } from '../../components/source-view'; import { LinkContainer } from '../../components/style-container'; +import { Upload } from '../../components/upload/upload'; const MESSAGES = { ADD_EMBED_LINK: 'Add embed link', @@ -38,7 +38,6 @@ export const EmbedLinkView = (props: EmbedLinkView) => { {embedLinkUrl ? ( {block.childrenIds.map(id => { - return ; + return ; })} ); diff --git a/libs/components/editor-blocks/src/blocks/grid-item/index.ts b/libs/components/editor-blocks/src/blocks/grid-item/index.ts index 7f51c47a05..2c6598d18d 100644 --- a/libs/components/editor-blocks/src/blocks/grid-item/index.ts +++ b/libs/components/editor-blocks/src/blocks/grid-item/index.ts @@ -6,7 +6,7 @@ import { GridItemRender } from './GridItemRender'; export class GridItemBlock extends BaseView { public override selectable = false; - public override activatable = false; + public override editable = false; public override allowPendant = false; public override layoutOnly = true; diff --git a/libs/components/editor-blocks/src/blocks/grid/Grid.tsx b/libs/components/editor-blocks/src/blocks/grid/Grid.tsx index 4ecf47f3b5..4b5bfd49b3 100644 --- a/libs/components/editor-blocks/src/blocks/grid/Grid.tsx +++ b/libs/components/editor-blocks/src/blocks/grid/Grid.tsx @@ -1,16 +1,16 @@ -import { RenderBlock } from '@toeverything/components/editor-core'; -import { CreateView } from '@toeverything/framework/virgo'; -import React, { useEffect, useRef, useState } from 'react'; -import { GridHandle } from './GirdHandle'; +import { useBlockRender } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; +import { Protocol } from '@toeverything/datasource/db-service'; +import { CreateView } from '@toeverything/framework/virgo'; +import { debounce, domToRect, Point } from '@toeverything/utils'; +import clsx from 'clsx'; +import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { GRID_ITEM_CLASS_NAME, GRID_ITEM_CONTENT_CLASS_NAME, } from '../grid-item/GridItem'; -import { debounce, domToRect, Point } from '@toeverything/utils'; -import clsx from 'clsx'; -import { Protocol } from '@toeverything/datasource/db-service'; +import { GridHandle } from './GirdHandle'; const DB_UPDATE_DELAY = 50; const GRID_ON_DRAG_CLASS = 'grid-layout-on-drag'; @@ -31,6 +31,7 @@ export const Grid = function (props: CreateView) { const originalLeftWidth = useRef(gridItemMinWidth); const originalRightWidth = useRef(gridItemMinWidth); const [alertHandleId, setAlertHandleId] = useState(null); + const { BlockRender } = useBlockRender(); const getLeftRightGridItemDomByIndex = (index: number) => { const gridItems = Array.from(gridContainerRef.current?.children).filter( @@ -226,7 +227,7 @@ export const Grid = function (props: CreateView) { key={id} className={GRID_ITEM_CLASS_NAME} > - + handleDragGrid(event, i)} editor={editor} diff --git a/libs/components/editor-blocks/src/blocks/grid/index.ts b/libs/components/editor-blocks/src/blocks/grid/index.ts index 582014376f..a74bb130f9 100644 --- a/libs/components/editor-blocks/src/blocks/grid/index.ts +++ b/libs/components/editor-blocks/src/blocks/grid/index.ts @@ -7,7 +7,7 @@ export { GRID_PROPERTY_KEY, removePercent } from './Grid'; export class GridBlock extends BaseView { public override selectable = false; - public override activatable = false; + public override editable = false; public override allowPendant = false; public override layoutOnly = true; diff --git a/libs/components/editor-blocks/src/blocks/group/Group.tsx b/libs/components/editor-blocks/src/blocks/group/Group.tsx index b5fdcac58c..ea27bbbd38 100644 --- a/libs/components/editor-blocks/src/blocks/group/Group.tsx +++ b/libs/components/editor-blocks/src/blocks/group/Group.tsx @@ -13,7 +13,7 @@ import { export class Group extends BaseView { public override selectable = true; - public override activatable = false; + public override editable = false; public override allowPendant = false; type = Protocol.Block.Type.group; diff --git a/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx b/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx index 6145bec114..739927626d 100644 --- a/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx +++ b/libs/components/editor-blocks/src/blocks/group/ScenePage.tsx @@ -2,5 +2,5 @@ import { RenderBlockChildren } from '@toeverything/components/editor-core'; import type { CreateView } from '@toeverything/framework/virgo'; export const ScenePage = ({ block }: CreateView) => { - return ; + return ; }; diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx index 76220d7ff0..8f79a6c208 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/CardItem.tsx @@ -1,11 +1,18 @@ -import type { KanbanCard } from '@toeverything/components/editor-core'; import { - RenderBlock, + KanbanCard, + useBlockRender, + useEditor, useKanban, - useRefPage, } from '@toeverything/components/editor-core'; -import { styled } from '@toeverything/components/ui'; +import { PenIcon } from '@toeverything/components/icons'; +import { + IconButton, + MuiClickAwayListener, + styled, +} from '@toeverything/components/ui'; import { useFlag } from '@toeverything/datasource/feature-flags'; +import { useState, type MouseEvent } from 'react'; +import { useRefPage } from './RefPage'; const CardContent = styled('div')({ margin: '20px', @@ -23,6 +30,7 @@ const CardActions = styled('div')({ fontWeight: '300', color: '#98ACBD', transition: 'all ease-in 0.2s', + zIndex: 1, ':hover': { background: '#F5F7F8', @@ -39,11 +47,13 @@ const PlusIcon = styled('div')({ }); const CardContainer = styled('div')({ + position: 'relative', display: 'flex', flexDirection: 'column', backgroundColor: '#fff', border: '1px solid #E2E7ED', borderRadius: '5px', + overflow: 'hidden', [CardActions.toString()]: { opacity: '0', @@ -55,6 +65,23 @@ const CardContainer = styled('div')({ }, }); +const Overlay = styled('div')({ + position: 'absolute', + width: '100%', + height: '100%', + background: 'transparent', + + '& > *': { + visibility: 'hidden', + position: 'absolute', + right: '24px', + top: '16px', + }, + '&:hover > *': { + visibility: 'visible', + }, +}); + export const CardItem = ({ id, block, @@ -64,24 +91,44 @@ export const CardItem = ({ }) => { const { addSubItem } = useKanban(); const { openSubPage } = useRefPage(); + const [editable, setEditable] = useState(false); const showKanbanRefPageFlag = useFlag('ShowKanbanRefPage', false); + const { editor } = useEditor(); + const { BlockRender } = useBlockRender(); + const onAddItem = async () => { + setEditable(true); await addSubItem(block); }; const onClickCard = async () => { - showKanbanRefPageFlag && openSubPage(id); + openSubPage(id); + }; + + const onClickPen = (e: MouseEvent) => { + e.stopPropagation(); + setEditable(true); + editor.selectionManager.activeNodeByNodeId(block.id); }; return ( - - - - - - - Add a sub-block - - + setEditable(false)}> + + + + + {showKanbanRefPageFlag && !editable && ( + + + + + + )} + + + Add a sub-block + + + ); }; diff --git a/libs/components/editor-core/src/ref-page/ModalPage.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx similarity index 80% rename from libs/components/editor-core/src/ref-page/ModalPage.tsx rename to libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx index 52e1d6554f..3d223d121d 100644 --- a/libs/components/editor-core/src/ref-page/ModalPage.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/RefPage.tsx @@ -1,7 +1,7 @@ +import { useEditor } from '@toeverything/components/editor-core'; import { MuiBackdrop, styled, useTheme } from '@toeverything/components/ui'; import { createContext, ReactNode, useContext, useState } from 'react'; import { createPortal } from 'react-dom'; -import { RenderBlock } from '../render-block'; const Dialog = styled('div')({ flex: 1, @@ -30,7 +30,7 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { onClick={closeSubPage} > { + onClick={(e: { stopPropagation: () => void }) => { e.stopPropagation(); }} > @@ -43,9 +43,21 @@ const Modal = ({ open, children }: { open: boolean; children?: ReactNode }) => { }; const ModalPage = ({ blockId }: { blockId: string | null }) => { + const { editor, editorElement } = useEditor(); + + const AffineEditor = editorElement as any; + return ( - {blockId && } + {blockId && ( + + )} ); }; diff --git a/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx b/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx index 27d5543177..73424708a7 100644 --- a/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx +++ b/libs/components/editor-blocks/src/blocks/group/scene-kanban/SceneKanban.tsx @@ -4,30 +4,33 @@ import { SceneKanbanContext } from './context'; import { CardContainerWrapper } from './dndable/wrapper/CardContainerWrapper'; import type { ComponentType } from 'react'; import type { CreateView } from '@toeverything/framework/virgo'; +import { RefPageProvider } from './RefPage'; export const SceneKanban: ComponentType = withKanban( ({ editor, block }) => { const { kanban } = useKanban(); return ( - - ( - - )} - /> - + + + ( + + )} + /> + + ); } ); diff --git a/libs/components/editor-blocks/src/blocks/image/index.ts b/libs/components/editor-blocks/src/blocks/image/index.ts index 844ae43cb2..2a649af50c 100644 --- a/libs/components/editor-blocks/src/blocks/image/index.ts +++ b/libs/components/editor-blocks/src/blocks/image/index.ts @@ -10,7 +10,7 @@ import { getRandomString } from '@toeverything/components/common'; export class ImageBlock extends BaseView { public override selectable = true; - public override activatable = false; + public override editable = false; type = Protocol.Block.Type.image; View = ImageView; diff --git a/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx b/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx index 38dac6fc75..74bcfab504 100644 --- a/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx +++ b/libs/components/editor-blocks/src/blocks/numbered/NumberedView.tsx @@ -1,8 +1,8 @@ import { TextProps } from '@toeverything/components/common'; import { ContentColumnValue, - services, Protocol, + services, } from '@toeverything/datasource/db-service'; import { type CreateView } from '@toeverything/framework/virgo'; import { useEffect, useRef, useState } from 'react'; @@ -11,18 +11,17 @@ import { type ExtendedTextUtils, } from '../../components/text-manage'; import { tabBlock } from '../../utils/indent'; -import { IndentWrapper } from '../../components/IndentWrapper'; import type { Numbered, NumberedAsyncBlock } from './types'; -import { getChildrenType, getNumber } from './data'; import { - supportChildren, - RenderBlockChildren, - useOnSelect, BlockPendantProvider, + RenderBlockChildren, + supportChildren, + useOnSelect, } from '@toeverything/components/editor-core'; -import { List } from '../../components/style-container'; import { BlockContainer } from '../../components/BlockContainer'; +import { List } from '../../components/style-container'; +import { getChildrenType, getNumber } from './data'; export const defaultTodoProps: Numbered = { text: { value: [{ text: '' }] }, @@ -204,9 +203,7 @@ export const NumberedView = ({ block, editor }: CreateView) => { - - - + ); }; diff --git a/libs/components/editor-blocks/src/blocks/numbered/index.ts b/libs/components/editor-blocks/src/blocks/numbered/index.ts index 5c3f8a4e4e..6fd9d66837 100644 --- a/libs/components/editor-blocks/src/blocks/numbered/index.ts +++ b/libs/components/editor-blocks/src/blocks/numbered/index.ts @@ -1,16 +1,16 @@ +import { Protocol } from '@toeverything/datasource/db-service'; import { AsyncBlock, BaseView, BlockEditor, HTML2BlockResult, } from '@toeverything/framework/virgo'; -import { Protocol } from '@toeverything/datasource/db-service'; -import { defaultTodoProps, NumberedView } from './NumberedView'; import { Block2HtmlProps, commonBlock2HtmlContent, commonHTML2block, } from '../../utils/commonBlockClip'; +import { defaultTodoProps, NumberedView } from './NumberedView'; export class NumberedBlock extends BaseView { public type = Protocol.Block.Type.numbered; diff --git a/libs/components/editor-blocks/src/blocks/text/TextView.tsx b/libs/components/editor-blocks/src/blocks/text/TextView.tsx index 5edf231217..fcb06238e2 100644 --- a/libs/components/editor-blocks/src/blocks/text/TextView.tsx +++ b/libs/components/editor-blocks/src/blocks/text/TextView.tsx @@ -12,7 +12,6 @@ import { styled } from '@toeverything/components/ui'; import { Protocol } from '@toeverything/datasource/db-service'; import { CreateView } from '@toeverything/framework/virgo'; import { BlockContainer } from '../../components/BlockContainer'; -import { IndentWrapper } from '../../components/IndentWrapper'; import { TextManage } from '../../components/text-manage'; import { dedentBlock, tabBlock } from '../../utils/indent'; interface CreateTextView extends CreateView { @@ -255,9 +254,7 @@ export const TextView = ({ handleTab={onTab} /> - - - + ); }; diff --git a/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx b/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx index 8982f4695e..9bf24a37ec 100644 --- a/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx +++ b/libs/components/editor-blocks/src/blocks/todo/TodoView.tsx @@ -1,11 +1,17 @@ import { TextProps } from '@toeverything/components/common'; +import { + AsyncBlock, + BlockPendantProvider, + CreateView, + useOnSelect, +} from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; import { ContentColumnValue, Protocol, } from '@toeverything/datasource/db-service'; -import { AsyncBlock, type CreateView } from '@toeverything/framework/virgo'; -import { useRef } from 'react'; +import { useRef, useState } from 'react'; +import { BlockContainer } from '../../components/BlockContainer'; import { TextManage, type ExtendedTextUtils, @@ -36,6 +42,10 @@ const todoIsEmpty = (contentValue: ContentColumnValue): boolean => { export const TodoView = ({ block, editor }: CreateView) => { const properties = { ...defaultTodoProps, ...block.getProperties() }; const text_ref = useRef(null); + const [isSelect, setIsSelect] = useState(false); + useOnSelect(block.id, (isSelect: boolean) => { + setIsSelect(isSelect); + }); const turn_into_text_block = async () => { // Convert to text block @@ -121,28 +131,34 @@ export const TodoView = ({ block, editor }: CreateView) => { }; return ( - -
- -
+ + + +
+ +
-
- -
-
+
+ +
+
+ + ); }; diff --git a/libs/components/editor-blocks/src/blocks/todo/index.ts b/libs/components/editor-blocks/src/blocks/todo/index.ts index 90b1dec4d9..7e973e68f3 100644 --- a/libs/components/editor-blocks/src/blocks/todo/index.ts +++ b/libs/components/editor-blocks/src/blocks/todo/index.ts @@ -1,17 +1,18 @@ +import { Protocol } from '@toeverything/datasource/db-service'; import { BaseView, BlockEditor, HTML2BlockResult, + withTreeViewChildren, } from '@toeverything/framework/virgo'; -import { Protocol } from '@toeverything/datasource/db-service'; -import { withTreeViewChildren } from '../../utils/WithTreeViewChildren'; -import { TodoView, defaultTodoProps } from './TodoView'; -import type { TodoAsyncBlock } from './types'; +import { defaultTodoProps, TodoView } from './TodoView'; + import { Block2HtmlProps, commonBlock2HtmlContent, commonHTML2block, } from '../../utils/commonBlockClip'; +import type { TodoAsyncBlock } from './types'; export class TodoBlock extends BaseView { type = Protocol.Block.Type.todo; diff --git a/libs/components/editor-blocks/src/blocks/youtube/index.ts b/libs/components/editor-blocks/src/blocks/youtube/index.ts index ef09dea459..080b3183e4 100644 --- a/libs/components/editor-blocks/src/blocks/youtube/index.ts +++ b/libs/components/editor-blocks/src/blocks/youtube/index.ts @@ -12,7 +12,7 @@ import { export class YoutubeBlock extends BaseView { public override selectable = true; - public override activatable = false; + public override editable = false; type = Protocol.Block.Type.youtube; View = YoutubeView; diff --git a/libs/components/editor-blocks/src/components/BlockContainer/BlockContainer.tsx b/libs/components/editor-blocks/src/components/BlockContainer/BlockContainer.tsx index 00da92a813..4c043ce163 100644 --- a/libs/components/editor-blocks/src/components/BlockContainer/BlockContainer.tsx +++ b/libs/components/editor-blocks/src/components/BlockContainer/BlockContainer.tsx @@ -23,7 +23,7 @@ export const BlockContainer = function ({ ); }; -export const Container = styled('div')<{ selected: boolean }>( +export const Container = styled('div')<{ selected?: boolean }>( ({ selected, theme }) => ({ backgroundColor: selected ? theme.affine.palette.textSelected : '', marginBottom: '2px', diff --git a/libs/components/editor-blocks/src/components/IndentWrapper/IndentWrapper.tsx b/libs/components/editor-blocks/src/components/IndentWrapper/IndentWrapper.tsx deleted file mode 100644 index 93aa9d34a1..0000000000 --- a/libs/components/editor-blocks/src/components/IndentWrapper/IndentWrapper.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { PropsWithChildren } from 'react'; -import { ChildrenView } from '@toeverything/framework/virgo'; -import { styled } from '@toeverything/components/ui'; - -/** - * Indent rendering child nodes - */ -export const IndentWrapper = (props: PropsWithChildren) => { - return {props.children}; -}; - -const StyledIdentWrapper = styled('div')({ - display: 'flex', - flexDirection: 'column', - // TODO: marginLeft should use theme provided by styled - marginLeft: '30px', -}); diff --git a/libs/components/editor-blocks/src/components/IndentWrapper/index.ts b/libs/components/editor-blocks/src/components/IndentWrapper/index.ts deleted file mode 100644 index e4234a916d..0000000000 --- a/libs/components/editor-blocks/src/components/IndentWrapper/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './IndentWrapper'; diff --git a/libs/components/editor-blocks/src/components/source-view/BlockView.tsx b/libs/components/editor-blocks/src/components/source-view/BlockView.tsx index 33c0f5d66c..0b2c953312 100644 --- a/libs/components/editor-blocks/src/components/source-view/BlockView.tsx +++ b/libs/components/editor-blocks/src/components/source-view/BlockView.tsx @@ -1,9 +1,9 @@ -import { memo, useEffect, useRef, useState } from 'react'; import { nanoid } from 'nanoid'; +import { memo, useEffect, useRef, useState } from 'react'; import { StyledBlockPreview } from '@toeverything/components/common'; +import { AsyncBlock, useEditor } from '@toeverything/components/editor-core'; import { services } from '@toeverything/datasource/db-service'; -import { AsyncBlock } from '@toeverything/framework/virgo'; import { debounce, sleep } from '@toeverything/utils'; const updateTitle = async ( @@ -73,15 +73,15 @@ const useBlockTitle = (block: AsyncBlock, blockId: string) => { type BlockPreviewProps = { block: AsyncBlock; blockId: string; - editorElement?: () => JSX.Element; }; const InternalBlockPreview = (props: BlockPreviewProps) => { const container = useRef(); const [preview, setPreview] = useState(true); const title = useBlockTitle(props.block, props.blockId); + const { editorElement } = useEditor(); - const AffineEditor = props.editorElement as any; + const AffineEditor = editorElement as any; useEffect(() => { if (container?.current) { diff --git a/libs/components/editor-blocks/src/components/source-view/SourceView.tsx b/libs/components/editor-blocks/src/components/source-view/SourceView.tsx index b03189d7d4..27d19433bf 100644 --- a/libs/components/editor-blocks/src/components/source-view/SourceView.tsx +++ b/libs/components/editor-blocks/src/components/source-view/SourceView.tsx @@ -1,17 +1,15 @@ import { AsyncBlock, useCurrentView, - useLazyIframe, } from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; -import { ReactElement, ReactNode, useEffect, useRef, useState } from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; import { SCENE_CONFIG } from '../../blocks/group/config'; import { BlockPreview } from './BlockView'; import { formatUrl } from './format-url'; export interface Props { block: AsyncBlock; - editorElement?: () => JSX.Element; viewType?: string; link: string; // onResizeEnd: (data: any) => void; @@ -150,7 +148,7 @@ const LoadingContiner = () => { }; export const SourceView = (props: Props) => { - const { link, isSelected, block, editorElement } = props; + const { link, isSelected, block } = props; const src = formatUrl(link); // let iframeShow = useLazyIframe(src, 3000, iframeContainer); const [currentView] = useCurrentView(); @@ -161,10 +159,7 @@ export const SourceView = (props: Props) => { - + ); @@ -175,11 +170,7 @@ export const SourceView = (props: Props) => { style={{ padding: '0' }} scene={type} > - + ); } diff --git a/libs/components/editor-blocks/src/components/source-view/format-url/youtube.ts b/libs/components/editor-blocks/src/components/source-view/format-url/youtube.ts index 56f165a515..7cd540b12e 100644 --- a/libs/components/editor-blocks/src/components/source-view/format-url/youtube.ts +++ b/libs/components/editor-blocks/src/components/source-view/format-url/youtube.ts @@ -1,4 +1,5 @@ export const isYoutubeUrl = (url?: string): boolean => { + if (!url) return false; const allowedHosts = ['www.youtu.be', 'www.youtube.com']; const host = new URL(url).host; return allowedHosts.includes(host); diff --git a/libs/components/editor-core/src/Contexts.tsx b/libs/components/editor-core/src/Contexts.tsx index 418fe0e8b4..0b7c7d2270 100644 --- a/libs/components/editor-core/src/Contexts.tsx +++ b/libs/components/editor-core/src/Contexts.tsx @@ -1,22 +1,34 @@ -import { createContext, useContext } from 'react'; -import type { BlockEditor, AsyncBlock } from './editor'; import { genErrorObj } from '@toeverything/utils'; +import { createContext, PropsWithChildren, useContext } from 'react'; +import type { AsyncBlock, BlockEditor } from './editor'; -const RootContext = createContext<{ +type EditorProps = { editor: BlockEditor; // TODO: Temporary fix, dependencies in the new architecture are bottom-up, editors do not need to be passed down from the top editorElement: () => JSX.Element; -}>( +}; + +const EditorContext = createContext( genErrorObj( - 'Failed to get context! The context only can use under the "render-root"' + 'Failed to get EditorContext! The context only can use under the "render-root"' // eslint-disable-next-line @typescript-eslint/no-explicit-any ) as any ); -export const EditorProvider = RootContext.Provider; - export const useEditor = () => { - return useContext(RootContext); + return useContext(EditorContext); +}; + +export const EditorProvider = ({ + editor, + editorElement, + children, +}: PropsWithChildren) => { + return ( + + {children} + + ); }; /** diff --git a/libs/components/editor-core/src/RenderRoot.tsx b/libs/components/editor-core/src/RenderRoot.tsx index cbafd3ea75..c1f1dd0d49 100644 --- a/libs/components/editor-core/src/RenderRoot.tsx +++ b/libs/components/editor-core/src/RenderRoot.tsx @@ -4,12 +4,12 @@ import { services, type ReturnUnobserve, } from '@toeverything/datasource/db-service'; -import type { PropsWithChildren } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { EditorProvider } from './Contexts'; import type { BlockEditor } from './editor'; import { useIsOnDrag } from './hooks'; import { addNewGroup, appendNewGroup } from './recast-block'; +import { BlockRenderProvider, RenderBlock } from './render-block'; import { SelectionRect, SelectionRef } from './Selection'; interface RenderRootProps { @@ -24,11 +24,7 @@ interface RenderRootProps { const MAX_PAGE_WIDTH = 5000; export const MIN_PAGE_WIDTH = 1480; -export const RenderRoot = ({ - editor, - editorElement, - children, -}: PropsWithChildren) => { +export const RenderRoot = ({ editor, editorElement }: RenderRootProps) => { const selectionRef = useRef(null); const triggeredBySelect = useRef(false); const [pageWidth, setPageWidth] = useState(MIN_PAGE_WIDTH); @@ -158,39 +154,43 @@ export const RenderRoot = ({ }; return ( - - { - if (ref != null && ref !== editor.container) { - editor.container = ref; - editor.getHooks().render(); - } - }} - onMouseMove={onMouseMove} - onMouseDown={onMouseDown} - onMouseUp={onMouseUp} - onMouseLeave={onMouseLeave} - onMouseOut={onMouseOut} - onContextMenu={onContextmenu} - onKeyDown={onKeyDown} - onKeyDownCapture={onKeyDownCapture} - onKeyUp={onKeyUp} - onDragOver={onDragOver} - onDragLeave={onDragLeave} - onDragOverCapture={onDragOverCapture} - onDragEnd={onDragEnd} - onDrop={onDrop} - isOnDrag={isOnDrag} - > - - {children} - - {/** TODO: remove selectionManager insert */} - {editor && } - {editor.isEdgeless ? null : } - {patchedNodes} - + + + { + if (ref != null && ref !== editor.container) { + editor.container = ref; + editor.getHooks().render(); + } + }} + onMouseMove={onMouseMove} + onMouseDown={onMouseDown} + onMouseUp={onMouseUp} + onMouseLeave={onMouseLeave} + onMouseOut={onMouseOut} + onContextMenu={onContextmenu} + onKeyDown={onKeyDown} + onKeyDownCapture={onKeyDownCapture} + onKeyUp={onKeyUp} + onDragOver={onDragOver} + onDragLeave={onDragLeave} + onDragOverCapture={onDragOverCapture} + onDragEnd={onDragEnd} + onDrop={onDrop} + isOnDrag={isOnDrag} + > + + + + {/** TODO: remove selectionManager insert */} + {editor && ( + + )} + {editor.isEdgeless ? null : } + {patchedNodes} + + ); }; @@ -251,7 +251,7 @@ function ScrollBlank({ editor }: { editor: BlockEditor }) { ); return ( - v.text).join('') !== + (item.children || []).map((v: any) => v.text).join('') + ) { + const newItem = { + ...item, + children: children, + }; + values.splice(i, 1, newItem); + } + } + } + return rawData; + } } diff --git a/libs/components/editor-core/src/editor/block/block-helper.ts b/libs/components/editor-core/src/editor/block/block-helper.ts index 947e7eafe6..9235c2a397 100644 --- a/libs/components/editor-core/src/editor/block/block-helper.ts +++ b/libs/components/editor-core/src/editor/block/block-helper.ts @@ -24,6 +24,11 @@ type TextUtilsFunctions = | 'setSearchSlash' | 'removeSearchSlash' | 'getSearchSlashText' + | 'setDoubleLinkSearchSlash' + | 'getDoubleLinkSearchSlashText' + | 'setSelectDoubleLinkSearchSlash' + | 'removeDoubleLinkSearchSlash' + | 'insertDoubleLink' | 'selectionToSlateRange' | 'transformPoint' | 'toggleTextFormatBySelection' @@ -37,7 +42,6 @@ type TextUtilsFunctions = | 'getCommentsIdsBySelection' | 'getCurrentSelection' | 'removeSelection' - | 'insertReference' | 'isCollapsed' | 'blur' | 'setSelection' @@ -257,27 +261,60 @@ export class BlockHelper { } } - public insertReference( - reference: string, + public setDoubleLinkSearchSlash(blockId: string, point: Point) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + textUtils.setDoubleLinkSearchSlash(point); + } else { + console.warn('Could find the block text utils'); + } + } + + public getDoubleLinkSearchSlashText(blockId: string) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + return textUtils.getDoubleLinkSearchSlashText(); + } + console.warn('Could find the block text utils'); + return ''; + } + public setSelectDoubleLinkSearchSlash(blockId: string) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + return textUtils.setSelectDoubleLinkSearchSlash(); + } + console.warn('Could find the block text utils'); + return ''; + } + + public removeDoubleLinkSearchSlash( blockId: string, - selection: Selection, - offset: number + isRemoveSlash?: boolean ) { - const text_utils = this._blockTextUtilsMap[blockId]; - if (text_utils) { - const offsetSelection = window.getSelection(); - offsetSelection.setBaseAndExtent( - selection.anchorNode, - selection.anchorOffset, - selection.focusNode, - selection.focusOffset + offset - ); + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + textUtils.removeDoubleLinkSearchSlash(isRemoveSlash); + } else { + console.warn('Could find the block text utils'); + } + } - text_utils.removeSelection(offsetSelection); - text_utils.insertReference(reference); - - // range. - // text_utils.toggleTextFormatBySelection(format, range); + public async insertDoubleLink( + workspaceId: string, + linkBlockId: string, + blockId: string + ) { + const textUtils = this._blockTextUtilsMap[blockId]; + if (textUtils) { + const linkBlock = await this._editor.getBlock({ + workspace: workspaceId, + id: linkBlockId, + }); + let children = linkBlock.getProperties().text?.value || []; + if (children.length === 1 && !children[0].text) { + children = [{ text: 'Untitled' }]; + } + textUtils.insertDoubleLink(workspaceId, linkBlockId, children); } console.warn('Could find the block text utils'); } diff --git a/libs/components/editor-core/src/editor/editor.ts b/libs/components/editor-core/src/editor/editor.ts index 0d1d71e6f7..b0ecc74836 100644 --- a/libs/components/editor-core/src/editor/editor.ts +++ b/libs/components/editor-core/src/editor/editor.ts @@ -1,17 +1,15 @@ /* eslint-disable max-lines */ -import HotKeys from 'hotkeys-js'; - import type { PatchNode } from '@toeverything/components/ui'; +import { Commands } from '@toeverything/datasource/commands'; import type { BlockFlavors, ReturnEditorBlock, UpdateEditorBlock, } from '@toeverything/datasource/db-service'; import { services } from '@toeverything/datasource/db-service'; - -import { Commands } from '@toeverything/datasource/commands'; import { domToRect, last, Point, sleep } from '@toeverything/utils'; import assert from 'assert'; +import HotKeys from 'hotkeys-js'; import type { WorkspaceAndBlockId } from './block'; import { AsyncBlock } from './block'; import { BlockHelper } from './block/block-helper'; @@ -295,7 +293,7 @@ export class Editor implements Virgo { return await blockView.onCreate(block); } - private async getBlock({ + public async getBlock({ workspace, id, }: WorkspaceAndBlockId): Promise { diff --git a/libs/components/editor-core/src/editor/selection/selection.ts b/libs/components/editor-core/src/editor/selection/selection.ts index e6c1f94c83..c33eb6d64c 100644 --- a/libs/components/editor-core/src/editor/selection/selection.ts +++ b/libs/components/editor-core/src/editor/selection/selection.ts @@ -408,13 +408,13 @@ export class SelectionManager implements VirgoSelection { /** * - * get previous activatable blockNode + * get previous editable blockNode * @private * @param {AsyncBlock} node * @return {*} {(Promise)} * @memberof SelectionManager */ - private async _getPreviousActivatableBlockNode( + private async _getPreviousEditableBlockNode( node: AsyncBlock ): Promise { const preNode = await node.physicallyPerviousSibling(); @@ -422,8 +422,8 @@ export class SelectionManager implements VirgoSelection { if (!preNode) { return null; } - const { activatable, selectable } = this._editor.getView(preNode.type); - if (activatable) { + const { editable, selectable } = this._editor.getView(preNode.type); + if (editable) { this.setSelectedNodesIds([]); return preNode; } @@ -432,18 +432,18 @@ export class SelectionManager implements VirgoSelection { (document.activeElement as HTMLInputElement)?.blur(); return null; } - return this._getPreviousActivatableBlockNode(preNode); + return this._getPreviousEditableBlockNode(preNode); } /** * - * get next activatable blockNode + * get next editable blockNode * @private * @param {AsyncBlock} node * @return {*} {(Promise)} * @memberof SelectionManager */ - private async _getNextActivatableBlockNode( + private async _getNextEditableBlockNode( node: AsyncBlock, ignoreSelf = true ): Promise { @@ -482,8 +482,8 @@ export class SelectionManager implements VirgoSelection { } } - const { activatable, selectable } = this._editor.getView(nextNode.type); - if (activatable) { + const { editable, selectable } = this._editor.getView(nextNode.type); + if (editable) { this.setSelectedNodesIds([]); return nextNode; } @@ -492,7 +492,7 @@ export class SelectionManager implements VirgoSelection { (document.activeElement as HTMLInputElement)?.blur(); return null; } - return this._getNextActivatableBlockNode(nextNode); + return this._getNextEditableBlockNode(nextNode); } /** @@ -513,7 +513,7 @@ export class SelectionManager implements VirgoSelection { return; } } - preNode = await this._getPreviousActivatableBlockNode(node); + preNode = await this._getPreviousEditableBlockNode(node); if (preNode) { this.activeNodeByNodeId(preNode.id, position); } @@ -622,7 +622,7 @@ export class SelectionManager implements VirgoSelection { } } - nextNode = await this._getNextActivatableBlockNode(node); + nextNode = await this._getNextEditableBlockNode(node); if (nextNode) { this.activeNodeByNodeId(nextNode.id, position); } diff --git a/libs/components/editor-core/src/editor/views/base-view.ts b/libs/components/editor-core/src/editor/views/base-view.ts index fbf37f6318..33dd6ef17a 100644 --- a/libs/components/editor-core/src/editor/views/base-view.ts +++ b/libs/components/editor-core/src/editor/views/base-view.ts @@ -13,7 +13,6 @@ import { HTML2BlockResult } from '../clipboard/types'; export interface CreateView { block: AsyncBlock; editor: Editor; - editorElement: () => JSX.Element; /** * @deprecated Use recast table instead */ @@ -32,10 +31,10 @@ export abstract class BaseView { abstract type: string; /** - * activatable means can be focused + * editable means can be focused * @memberof BaseView */ - public activatable = true; + public editable = true; public selectable = true; diff --git a/libs/components/editor-core/src/index.ts b/libs/components/editor-core/src/index.ts index bea2fed3a0..6059350bc9 100644 --- a/libs/components/editor-core/src/index.ts +++ b/libs/components/editor-core/src/index.ts @@ -1,19 +1,16 @@ -export { RenderRoot, MIN_PAGE_WIDTH } from './RenderRoot'; -export * from './render-block'; -export * from './hooks'; - -export { RenderBlock } from './render-block'; - -export * from './recast-block'; -export * from './recast-block/types'; - export * from './block-pendant'; - +export { useEditor } from './Contexts'; +export * from './editor'; +export * from './hooks'; export * from './kanban'; export * from './kanban/types'; - +export * from './recast-block'; +export * from './recast-block/types'; +export { + BlockRenderProvider, + RenderBlockChildren, + useBlockRender, + withTreeViewChildren, +} from './render-block'; +export { MIN_PAGE_WIDTH, RenderRoot } from './RenderRoot'; export * from './utils'; - -export * from './editor'; - -export { RefPageProvider, useRefPage } from './ref-page'; diff --git a/libs/components/editor-core/src/kanban/Context.tsx b/libs/components/editor-core/src/kanban/Context.tsx index 12cc0db2f1..d0fc58ed0a 100644 --- a/libs/components/editor-core/src/kanban/Context.tsx +++ b/libs/components/editor-core/src/kanban/Context.tsx @@ -10,6 +10,8 @@ import { RecastMetaProperty, RecastPropertyId, } from '../recast-block/types'; +import { BlockRenderProvider } from '../render-block'; +import { KanbanBlockRender } from '../render-block/RenderKanbanBlock'; import { useInitKanbanEffect, useRecastKanban } from './kanban'; import { KanbanGroup } from './types'; @@ -54,9 +56,11 @@ export const KanbanProvider = ({ }; return ( - - {children} - + + + {children} + + ); }; diff --git a/libs/components/editor-core/src/recast-block/Context.tsx b/libs/components/editor-core/src/recast-block/Context.tsx index 47ec6cfcdb..4236ede55a 100644 --- a/libs/components/editor-core/src/recast-block/Context.tsx +++ b/libs/components/editor-core/src/recast-block/Context.tsx @@ -2,7 +2,6 @@ import { Protocol } from '@toeverything/datasource/db-service'; import { AsyncBlock } from '../editor'; import { ComponentType, createContext, ReactNode, useContext } from 'react'; import { RecastBlock } from './types'; -import { RefPageProvider } from '../ref-page'; /** * Determine whether the block supports RecastBlock @@ -48,7 +47,7 @@ export const RecastBlockProvider = ({ return ( - {children} + {children} ); }; diff --git a/libs/components/editor-core/src/ref-page/index.ts b/libs/components/editor-core/src/ref-page/index.ts deleted file mode 100644 index 4b06310a5c..0000000000 --- a/libs/components/editor-core/src/ref-page/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { useRefPage, RefPageProvider } from './ModalPage'; diff --git a/libs/components/editor-core/src/render-block/Context.tsx b/libs/components/editor-core/src/render-block/Context.tsx new file mode 100644 index 0000000000..90c89eec0e --- /dev/null +++ b/libs/components/editor-core/src/render-block/Context.tsx @@ -0,0 +1,35 @@ +import { genErrorObj } from '@toeverything/utils'; +import { createContext, PropsWithChildren, useContext } from 'react'; +import { RenderBlockProps } from './RenderBlock'; + +type BlockRenderProps = { + blockRender: (args: RenderBlockProps) => JSX.Element | null; +}; + +export const BlockRenderContext = createContext( + genErrorObj( + 'Failed to get BlockChildrenContext! The context only can use under the "render-root"' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ) as any +); + +/** + * CAUTION! DO NOT PROVIDE A DYNAMIC BLOCK RENDER! + */ +export const BlockRenderProvider = ({ + blockRender, + children, +}: PropsWithChildren) => { + return ( + + {children} + + ); +}; + +export const useBlockRender = () => { + const { blockRender } = useContext(BlockRenderContext); + return { + BlockRender: blockRender, + }; +}; diff --git a/libs/components/editor-core/src/render-block/RenderBlock.tsx b/libs/components/editor-core/src/render-block/RenderBlock.tsx index 0627aba95b..c843274166 100644 --- a/libs/components/editor-core/src/render-block/RenderBlock.tsx +++ b/libs/components/editor-core/src/render-block/RenderBlock.tsx @@ -4,7 +4,12 @@ import { useCallback, useMemo } from 'react'; import { useEditor } from '../Contexts'; import { useBlock } from '../hooks'; -interface RenderBlockProps { +/** + * Render nothing + */ +export const NullBlockRender = (): null => null; + +export interface RenderBlockProps { blockId: string; hasContainer?: boolean; } @@ -13,7 +18,7 @@ export function RenderBlock({ blockId, hasContainer = true, }: RenderBlockProps) { - const { editor, editorElement } = useEditor(); + const { editor } = useEditor(); const { block } = useBlock(blockId); const setRef = useCallback( @@ -29,7 +34,7 @@ export function RenderBlock({ if (block?.type) { return editor.getView(block.type).View; } - return () => null; + return (): null => null; }, [editor, block?.type]); if (!block) { @@ -50,7 +55,6 @@ export function RenderBlock({ block={block} columns={columns.columns} columnsFromId={columns.fromId} - editorElement={editorElement} /> ); @@ -65,4 +69,5 @@ export function RenderBlock({ const BlockContainer = styled('div')(({ theme }) => ({ fontSize: theme.typography.body1.fontSize, + flex: 1, })); diff --git a/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx b/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx index 4c87ccc8e8..d79d0cd002 100644 --- a/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx +++ b/libs/components/editor-core/src/render-block/RenderBlockChildren.tsx @@ -1,16 +1,39 @@ +import { styled } from '@toeverything/components/ui'; import type { AsyncBlock } from '../editor'; -import { RenderBlock } from './RenderBlock'; +import { useBlockRender } from './Context'; +import { NullBlockRender } from './RenderBlock'; -interface RenderChildrenProps { +export interface RenderChildrenProps { block: AsyncBlock; + indent?: boolean; } -export const RenderBlockChildren = ({ block }: RenderChildrenProps) => { +export const RenderBlockChildren = ({ + block, + indent = true, +}: RenderChildrenProps) => { + const { BlockRender } = useBlockRender(); + if (BlockRender === NullBlockRender) { + return null; + } + return block.childrenIds.length ? ( - <> + {block.childrenIds.map(childId => { - return ; + return ; })} - + ) : null; }; + +/** + * Indent rendering child nodes + */ +const StyledIdentWrapper = styled('div')<{ indent?: boolean }>( + ({ indent }) => ({ + display: 'flex', + flexDirection: 'column', + // TODO: marginLeft should use theme provided by styled + ...(indent && { marginLeft: '30px' }), + }) +); diff --git a/libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx b/libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx new file mode 100644 index 0000000000..54f9e2276e --- /dev/null +++ b/libs/components/editor-core/src/render-block/RenderKanbanBlock.tsx @@ -0,0 +1,56 @@ +import { styled } from '@toeverything/components/ui'; +import { useBlock } from '../hooks'; +import { BlockRenderProvider } from './Context'; +import { NullBlockRender, RenderBlock, RenderBlockProps } from './RenderBlock'; + +/** + * Render block without children. + */ +const BlockWithoutChildrenRender = ({ blockId }: RenderBlockProps) => { + return ( + + + + ); +}; + +/** + * Render a block, but only one level of children. + */ +const OneLevelBlockRender = ({ blockId }: RenderBlockProps) => { + return ( + + + + ); +}; + +export const KanbanBlockRender = ({ blockId }: RenderBlockProps) => { + const { block } = useBlock(blockId); + + if (!block) { + return ( + + + + ); + } + + return ( + + + {block?.childrenIds.map(childId => ( + + + + ))} + + ); +}; + +const StyledBorder = styled('div')({ + border: '1px solid #E0E6EB', + borderRadius: '5px', + margin: '4px', + padding: '0 4px', +}); diff --git a/libs/components/editor-blocks/src/utils/WithTreeViewChildren.tsx b/libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx similarity index 61% rename from libs/components/editor-blocks/src/utils/WithTreeViewChildren.tsx rename to libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx index e7e497f2ef..8c3642ef6d 100644 --- a/libs/components/editor-blocks/src/utils/WithTreeViewChildren.tsx +++ b/libs/components/editor-core/src/render-block/WithTreeViewChildren.tsx @@ -1,10 +1,3 @@ -import { - BlockPendantProvider, - CreateView, - RenderBlock, - useCurrentView, - useOnSelect, -} from '@toeverything/components/editor-core'; import { styled } from '@toeverything/components/ui'; import type { ComponentPropsWithoutRef, @@ -12,9 +5,10 @@ import type { CSSProperties, ReactElement, } from 'react'; -import { forwardRef, useState } from 'react'; -import { SCENE_CONFIG } from '../blocks/group/config'; -import { BlockContainer } from '../components/BlockContainer'; +import { forwardRef } from 'react'; +import { CreateView } from '../editor'; +import { useBlockRender } from './Context'; +import { NullBlockRender } from './RenderBlock'; type WithChildrenConfig = { indent: CSSProperties['marginLeft']; @@ -41,45 +35,6 @@ const TreeView = forwardRef< ); }); -interface ChildrenViewProp { - childrenIds: string[]; - handleCollapse: () => void; - indent?: string | number; -} - -const ChildrenView = ({ - childrenIds, - handleCollapse, - indent, -}: ChildrenViewProp) => { - const [currentView] = useCurrentView(); - const isKanbanScene = currentView.type === SCENE_CONFIG.KANBAN; - - return ( - - {childrenIds.map((childId, idx) => { - if (isKanbanScene) { - return ( - - - - ); - } - - return ( - - - - ); - })} - - ); -}; - const CollapsedNode = forwardRef< HTMLDivElement, ComponentPropsWithoutRef<'div'> @@ -104,15 +59,15 @@ export const withTreeViewChildren = ( }; return (props: CreateView) => { - const { block, editor } = props; + const { block } = props; + const { BlockRender } = useBlockRender(); const collapsed = block.getProperty('collapsed')?.value; const childrenIds = block.childrenIds; - const showChildren = !collapsed && childrenIds.length > 0; + const showChildren = + !collapsed && + childrenIds.length > 0 && + BlockRender !== NullBlockRender; - const [isSelect, setIsSelect] = useState(false); - useOnSelect(block.id, (isSelect: boolean) => { - setIsSelect(isSelect); - }); const handleCollapse = () => { block.setProperty('collapsed', { value: true }); }; @@ -122,15 +77,8 @@ export const withTreeViewChildren = ( }; return ( - - -
{creator(props)}
-
+ <> + {creator(props)} {collapsed && ( )} - {showChildren && ( - - )} -
+ {showChildren && + childrenIds.map((childId, idx) => { + return ( + + + + ); + })} + ); }; }; -const Wrapper = styled('div')({ display: 'flex', flexDirection: 'column' }); - -const Children = Wrapper; - const TREE_COLOR = '#D5DFE6'; // adjust left and right margins of the the tree line const TREE_LINE_LEFT_OFFSET = '-16px'; @@ -163,6 +113,7 @@ const TREE_LINE_WIDTH = '12px'; const TreeWrapper = styled('div')({ position: 'relative', + display: 'flex', }); const StyledTreeView = styled('div')({ @@ -228,9 +179,3 @@ const LastItemRadius = styled('div')({ borderRadius: '0 0 0 3px', pointerEvents: 'none', }); - -const StyledBorder = styled('div')({ - border: '1px solid #E0E6EB', - borderRadius: '5px', - margin: '4px', -}); diff --git a/libs/components/editor-core/src/render-block/index.ts b/libs/components/editor-core/src/render-block/index.ts index 820bac8b57..0d5c809c4c 100644 --- a/libs/components/editor-core/src/render-block/index.ts +++ b/libs/components/editor-core/src/render-block/index.ts @@ -1,2 +1,4 @@ +export { BlockRenderProvider, useBlockRender } from './Context'; export * from './RenderBlock'; export * from './RenderBlockChildren'; +export { withTreeViewChildren } from './WithTreeViewChildren'; diff --git a/libs/components/editor-plugins/src/index.ts b/libs/components/editor-plugins/src/index.ts index be7bb9bd97..4c6ce9a1e1 100644 --- a/libs/components/editor-plugins/src/index.ts +++ b/libs/components/editor-plugins/src/index.ts @@ -1,15 +1,15 @@ import type { PluginCreator } from '@toeverything/framework/virgo'; -import { - LeftMenuPlugin, - InlineMenuPlugin, - CommandMenuPlugin, - ReferenceMenuPlugin, - SelectionGroupPlugin, - GroupMenuPlugin, -} from './menu'; -import { TemplatePlugin } from './template'; -import { FullTextSearchPlugin } from './search'; import { AddCommentPlugin } from './comment'; +import { + CommandMenuPlugin, + DoubleLinkMenuPlugin, + GroupMenuPlugin, + InlineMenuPlugin, + LeftMenuPlugin, + SelectionGroupPlugin, +} from './menu'; +import { FullTextSearchPlugin } from './search'; +import { TemplatePlugin } from './template'; // import { PlaceholderPlugin } from './placeholder'; // import { BlockPropertyPlugin } from './block-property'; @@ -19,9 +19,9 @@ export const plugins: PluginCreator[] = [ LeftMenuPlugin, InlineMenuPlugin, CommandMenuPlugin, - ReferenceMenuPlugin, + DoubleLinkMenuPlugin, TemplatePlugin, - SelectionGroupPlugin, + // SelectionGroupPlugin, AddCommentPlugin, GroupMenuPlugin, ]; diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx new file mode 100644 index 0000000000..ec5f52be08 --- /dev/null +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Container.tsx @@ -0,0 +1,121 @@ +import { CommonList, CommonListItem } from '@toeverything/components/common'; +import { styled } from '@toeverything/components/ui'; +import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; + +export type DoubleLinkMenuContainerProps = { + editor: Virgo; + hooks: PluginHooks; + style?: React.CSSProperties; + blockId: string; + onSelected?: (item: string) => void; + onClose?: () => void; + types?: Array; + items?: CommonListItem[]; +}; + +export const DoubleLinkMenuContainer = ( + props: DoubleLinkMenuContainerProps +) => { + const { hooks, onSelected, onClose, types, style, items } = props; + const menuRef = useRef(null); + const [currentItem, setCurrentItem] = useState(); + + useEffect(() => { + if (types && !currentItem) { + setCurrentItem(types[0]); + } + }, [currentItem, onClose, types]); + + const handleUpDownKey = useCallback( + (event: React.KeyboardEvent) => { + if (types && ['ArrowUp', 'ArrowDown'].includes(event.code)) { + event.preventDefault(); + const isUpkey = event.code === 'ArrowUp'; + if (!currentItem && types.length) { + setCurrentItem(types[isUpkey ? types.length - 1 : 0]); + } + if (currentItem) { + const idx = types.indexOf(currentItem); + if (isUpkey ? idx > 0 : idx < types.length - 1) { + setCurrentItem(types[isUpkey ? idx - 1 : idx + 1]); + } + } + } + }, + [currentItem, types] + ); + const handleClickUp = useCallback( + (event: React.KeyboardEvent) => { + handleUpDownKey(event); + }, + [handleUpDownKey] + ); + + const handleClickDown = useCallback( + (event: React.KeyboardEvent) => { + handleUpDownKey(event); + }, + [handleUpDownKey] + ); + + const handleClickEnter = useCallback( + async (event: React.KeyboardEvent) => { + if (event.code === 'Enter' && currentItem) { + event.preventDefault(); + onSelected && onSelected(currentItem); + } + }, + [currentItem, onSelected] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + handleClickUp(event); + handleClickDown(event); + handleClickEnter(event); + }, + [handleClickUp, handleClickDown, handleClickEnter] + ); + + useEffect(() => { + const sub = hooks + .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) + .subscribe(handleKeyDown); + + return () => { + sub.unsubscribe(); + }; + }, [hooks, handleKeyDown]); + + return ( + + + onSelected?.(type)} + currentItem={currentItem} + setCurrentItem={setCurrentItem} + /> + + + ); +}; + +const RootContainer = styled('div')(({ theme }) => ({ + zIndex: 1, + borderRadius: '10px', + boxShadow: theme.affine.shadows.shadow1, + backgroundColor: '#fff', + padding: '8px 4px', +})); + +const ContentContainer = styled('div')(({ theme }) => ({ + display: 'flex', + overflow: 'hidden', + maxHeight: '300px', +})); diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx new file mode 100644 index 0000000000..a81e8b3122 --- /dev/null +++ b/libs/components/editor-plugins/src/menu/double-link-menu/DoubleLinkMenu.tsx @@ -0,0 +1,376 @@ +import { CommonListItem } from '@toeverything/components/common'; +import { AddIcon } from '@toeverything/components/icons'; +import { + Input, + ListButton, + MuiClickAwayListener, + MuiGrow as Grow, + MuiPaper as Paper, + MuiPopper as Popper, + styled, +} from '@toeverything/components/ui'; +import { services } from '@toeverything/datasource/db-service'; +import { HookType, PluginHooks, Virgo } from '@toeverything/framework/virgo'; +import React, { + ChangeEvent, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { useParams } from 'react-router-dom'; +import { QueryBlocks, QueryResult } from '../../search'; +import { DoubleLinkMenuContainer } from './Container'; + +const ADD_NEW_SUB_PAGE = 'AddNewSubPage'; +const ADD_NEW_PAGE = 'AddNewPage'; +const ARRAY_CODES = ['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']; + +export type DoubleLinkMenuProps = { + editor: Virgo; + hooks: PluginHooks; + style?: { left: number; top: number }; +}; + +type DoubleLinkMenuStyle = { + left: number; + top: number; + height: number; +}; + +export const DoubleLinkMenu = ({ + editor, + hooks, + style, +}: DoubleLinkMenuProps) => { + const { page_id: curPageId } = useParams(); + const [isOpen, setIsOpen] = useState(false); + const [anchorEl, setAnchorEl] = useState(null); + const dialogRef = useRef(); + const newPageSearchRef = useRef(); + const [doubleLinkMenuStyle, setDoubleLinkMenuStyle] = + useState({ + left: 0, + top: 0, + height: 0, + }); + + const [curBlockId, setCurBlockId] = useState(); + const [searchText, setSearchText] = useState(); + const [inAddNewPage, setInAddNewPage] = useState(false); + const [filterText, setFilterText] = useState(''); + const [searchResultBlocks, setSearchResultBlocks] = useState( + [] + ); + + const menuTypes = useMemo(() => { + return Object.values(searchResultBlocks) + .map(({ id }) => id) + .concat([ADD_NEW_SUB_PAGE, ADD_NEW_PAGE]); + }, [searchResultBlocks]); + + const menuItems: CommonListItem[] = useMemo(() => { + const items: CommonListItem[] = []; + if (searchResultBlocks?.length > 0) { + items.push({ + renderCustom: () => { + return ( + + ); + }, + }); + items.push( + ...(searchResultBlocks?.map( + block => + ({ + block: { + ...block, + content: block.content || 'Untitled', + }, + } as CommonListItem) + ) || []) + ); + } + + if (items.length > 0) { + items.push({ divider: 'newPage' }); + } + items.push({ + content: { + id: ADD_NEW_SUB_PAGE, + content: 'Add new sub-page', + icon: AddIcon, + }, + }); + !inAddNewPage && + items.push({ + content: { + id: ADD_NEW_PAGE, + content: 'Add new page in...', + icon: AddIcon, + }, + }); + return items; + }, [searchResultBlocks, inAddNewPage]); + + useEffect(() => { + const text = inAddNewPage ? filterText : searchText; + QueryBlocks(editor, text, result => { + if (!inAddNewPage) { + result = result.filter(item => item.id !== curPageId); + } + setSearchResultBlocks(result); + }); + }, [editor, searchText, filterText, inAddNewPage, curPageId]); + + const hideMenu = useCallback(() => { + setIsOpen(false); + setInAddNewPage(false); + editor.blockHelper.removeDoubleLinkSearchSlash(curBlockId); + editor.scrollManager.unLock(); + }, [curBlockId, editor]); + + const resetState = useCallback( + (preNodeId: string, nextNodeId: string) => { + editor.blockHelper.removeDoubleLinkSearchSlash(preNodeId); + setCurBlockId(nextNodeId); + setSearchText(''); + setIsOpen(true); + editor.scrollManager.lock(); + const clientRect = + editor.selection.currentSelectInfo?.browserSelection + ?.getRangeAt(0) + ?.getBoundingClientRect(); + if (clientRect) { + const rectTop = clientRect.top; + const { top, left } = editor.container.getBoundingClientRect(); + setDoubleLinkMenuStyle({ + top: rectTop - top, + left: clientRect.left - left, + height: clientRect.height, + }); + setAnchorEl(dialogRef.current); + } + const textSelection = editor.blockHelper.selectionToSlateRange( + nextNodeId, + editor.selection.currentSelectInfo?.browserSelection + ); + if (textSelection) { + const { anchor } = textSelection; + editor.blockHelper.setDoubleLinkSearchSlash(nextNodeId, anchor); + } + }, + [editor] + ); + + const searchChange = useCallback( + async (event: React.KeyboardEvent) => { + if (ARRAY_CODES.includes(event.code)) { + return; + } + if (event.code === 'Backspace') { + const searchText = + editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId); + if (!searchText || searchText === '[[') { + hideMenu(); + event.preventDefault(); + event.stopPropagation(); + return; + } + } + const { type, anchorNode } = editor.selection.currentSelectInfo; + if ( + !isOpen || + (type === 'Range' && + anchorNode && + anchorNode.id !== curBlockId && + editor.blockHelper.isSelectionCollapsed(anchorNode.id)) + ) { + const text = editor.blockHelper.getBlockTextBeforeSelection( + anchorNode.id + ); + if (text.endsWith('[[')) { + resetState(curBlockId, anchorNode.id); + } + } + if (isOpen) { + const searchText = + editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId); + if (searchText && searchText.startsWith('[[')) { + setSearchText(searchText.slice(2).trim()); + } + } + }, + [editor, isOpen, curBlockId, hideMenu] + ); + + const handleKeyup = useCallback( + (event: React.KeyboardEvent) => searchChange(event), + [searchChange] + ); + + const handleKeyDown = useCallback( + (event: React.KeyboardEvent) => { + if (!isOpen) { + return; + } + if (event.code === 'Escape') { + hideMenu(); + } + + if (event.code === 'Backspace') { + const searchText = + editor.blockHelper.getDoubleLinkSearchSlashText(curBlockId); + if (!searchText || searchText === '[[') { + event.preventDefault(); + event.stopPropagation(); + return; + } + } + }, + [hideMenu, editor, isOpen, curBlockId] + ); + + useEffect(() => { + const sub = hooks + .get(HookType.ON_ROOT_NODE_KEYUP) + .subscribe(handleKeyup); + sub.add( + hooks + .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) + .subscribe(handleKeyDown) + ); + + return () => { + sub.unsubscribe(); + }; + }, [handleKeyup, handleKeyDown, hooks]); + + const insertDoubleLink = useCallback( + async (pageId: string) => { + editor.blockHelper.setSelectDoubleLinkSearchSlash(curBlockId); + await editor.blockHelper.insertDoubleLink( + editor.workspace, + pageId, + curBlockId + ); + hideMenu(); + }, + [editor, curBlockId, hideMenu] + ); + + const addSubPage = useCallback( + async (parentPageId: string) => { + const newPage = await services.api.editorBlock.create({ + workspace: editor.workspace, + type: 'page' as const, + }); + services.api.editorBlock.update({ + id: newPage.id, + workspace: editor.workspace, + properties: { + text: { value: [{ text: searchText }] }, + }, + }); + await services.api.pageTree.addChildPageToWorkspace( + editor.workspace, + parentPageId, + newPage.id + ); + return newPage.id; + }, + [searchText, editor] + ); + + const handleSelected = async (id: string) => { + if (curBlockId) { + if (id === ADD_NEW_PAGE) { + setInAddNewPage(true); + setTimeout(() => { + newPageSearchRef.current?.focus(); + }); + return; + } + if (id === ADD_NEW_SUB_PAGE) { + const pageId = await addSubPage(curPageId); + insertDoubleLink(pageId); + return; + } + if (inAddNewPage) { + const pageId = await addSubPage(id); + insertDoubleLink(pageId); + } else { + insertDoubleLink(id); + } + } + }; + + const handleFilterChange = useCallback( + async (e: ChangeEvent) => { + const text = e.target.value; + + await setFilterText(text); + }, + [] + ); + + return ( +
+ hideMenu()}> + + {({ TransitionProps }) => ( + + + {inAddNewPage && ( + + + + )} + + + + )} + + +
+ ); +}; +const NewPageSearchContainer = styled('div')({ + padding: '8px 8px 0px 8px', +}); diff --git a/libs/components/editor-plugins/src/menu/reference-menu/Plugin.tsx b/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx similarity index 79% rename from libs/components/editor-plugins/src/menu/reference-menu/Plugin.tsx rename to libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx index 688adfd147..3933bf5f1c 100644 --- a/libs/components/editor-plugins/src/menu/reference-menu/Plugin.tsx +++ b/libs/components/editor-plugins/src/menu/double-link-menu/Plugin.tsx @@ -1,12 +1,11 @@ import { StrictMode } from 'react'; - import { BasePlugin } from '../../base-plugin'; import { PluginRenderRoot } from '../../utils'; -import { ReferenceMenu } from './ReferenceMenu'; +import { DoubleLinkMenu } from './DoubleLinkMenu'; const PLUGIN_NAME = 'reference-menu'; -export class ReferenceMenuPlugin extends BasePlugin { +export class DoubleLinkMenuPlugin extends BasePlugin { private _root?: PluginRenderRoot; public static override get pluginName(): string { @@ -22,7 +21,7 @@ export class ReferenceMenuPlugin extends BasePlugin { this._root?.render( - + ); } diff --git a/libs/components/editor-plugins/src/menu/double-link-menu/index.ts b/libs/components/editor-plugins/src/menu/double-link-menu/index.ts new file mode 100644 index 0000000000..e287e74e03 --- /dev/null +++ b/libs/components/editor-plugins/src/menu/double-link-menu/index.ts @@ -0,0 +1 @@ +export { DoubleLinkMenuPlugin } from './Plugin'; diff --git a/libs/components/editor-plugins/src/menu/index.ts b/libs/components/editor-plugins/src/menu/index.ts index 50d4133651..190e3a5109 100644 --- a/libs/components/editor-plugins/src/menu/index.ts +++ b/libs/components/editor-plugins/src/menu/index.ts @@ -1,9 +1,7 @@ +export { CommandMenuPlugin } from './command-menu'; +export { DoubleLinkMenuPlugin } from './double-link-menu'; +export { GroupMenuPlugin } from './group-menu'; export { InlineMenuPlugin } from './inline-menu'; export { LeftMenuPlugin } from './left-menu/LeftMenuPlugin'; -export { CommandMenuPlugin } from './command-menu'; -export { ReferenceMenuPlugin } from './reference-menu'; -export { SelectionGroupPlugin } from './selection-group-menu'; - export { MENU_WIDTH as menuWidth } from './left-menu/menu-config'; - -export { GroupMenuPlugin } from './group-menu'; +export { SelectionGroupPlugin } from './selection-group-menu'; diff --git a/libs/components/editor-plugins/src/menu/reference-menu/Container.tsx b/libs/components/editor-plugins/src/menu/reference-menu/Container.tsx deleted file mode 100644 index 42ca2d679d..0000000000 --- a/libs/components/editor-plugins/src/menu/reference-menu/Container.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import React, { useEffect, useState, useCallback, useRef } from 'react'; - -import { Virgo, PluginHooks, HookType } from '@toeverything/framework/virgo'; -import { - CommonList, - CommonListItem, - commonListContainer, -} from '@toeverything/components/common'; -import { domToRect } from '@toeverything/utils'; -import { styled } from '@toeverything/components/ui'; - -import { QueryResult } from '../../search'; - -export type ReferenceMenuContainerProps = { - editor: Virgo; - hooks: PluginHooks; - style?: React.CSSProperties; - isShow?: boolean; - blockId: string; - onSelected?: (item: string) => void; - onClose?: () => void; - searchBlocks?: QueryResult; - types?: Array; -}; - -export const ReferenceMenuContainer = ({ - hooks, - isShow = false, - onSelected, - onClose, - types, - searchBlocks, - style, -}: ReferenceMenuContainerProps) => { - const menu_ref = useRef(null); - const [current_item, set_current_item] = useState(); - const [need_check_into_view, set_need_check_into_view] = - useState(false); - - useEffect(() => { - if (need_check_into_view) { - if (current_item && menu_ref.current) { - const item_ele = - menu_ref.current.querySelector( - `.item-${current_item}` - ); - const scroll_ele = - menu_ref.current.querySelector( - `.${commonListContainer}` - ); - if (item_ele) { - const itemRect = domToRect(item_ele); - const scrollRect = domToRect(scroll_ele); - if ( - itemRect.top < scrollRect.top || - itemRect.bottom > scrollRect.bottom - ) { - // IMP: may be do it with self function - item_ele.scrollIntoView({ - block: 'nearest', - }); - } - } - } - set_need_check_into_view(false); - } - }, [need_check_into_view, current_item]); - - useEffect(() => { - if (isShow && types && !current_item) set_current_item(types[0]); - if (!isShow) onClose?.(); - }, [current_item, isShow, onClose, types]); - - useEffect(() => { - if (isShow && types) { - if (!types.includes(current_item)) { - set_need_check_into_view(true); - if (types.length) { - set_current_item(types[0]); - } else { - set_current_item(undefined); - } - } - } - }, [isShow, types, current_item]); - - const handle_click_up = useCallback( - (event: React.KeyboardEvent) => { - if (isShow && types && event.code === 'ArrowUp') { - event.preventDefault(); - if (!current_item && types.length) { - set_current_item(types[types.length - 1]); - } - if (current_item) { - const idx = types.indexOf(current_item); - if (idx > 0) { - set_need_check_into_view(true); - set_current_item(types[idx - 1]); - } - } - } - }, - [isShow, types, current_item] - ); - - const handle_click_down = useCallback( - (event: React.KeyboardEvent) => { - if (isShow && types && event.code === 'ArrowDown') { - event.preventDefault(); - if (!current_item && types.length) { - set_current_item(types[0]); - } - if (current_item) { - const idx = types.indexOf(current_item); - if (idx < types.length - 1) { - set_need_check_into_view(true); - set_current_item(types[idx + 1]); - } - } - } - }, - [isShow, types, current_item] - ); - - const handle_click_enter = useCallback( - async (event: React.KeyboardEvent) => { - if (isShow && event.code === 'Enter' && current_item) { - event.preventDefault(); - onSelected && onSelected(current_item); - } - }, - [isShow, current_item, onSelected] - ); - - const handle_key_down = useCallback( - (event: React.KeyboardEvent) => { - handle_click_up(event); - handle_click_down(event); - handle_click_enter(event); - }, - [handle_click_up, handle_click_down, handle_click_enter] - ); - - useEffect(() => { - const sub = hooks - .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) - .subscribe(handle_key_down); - - return () => { - sub.unsubscribe(); - }; - }, [hooks, handle_key_down]); - - return isShow ? ( - - - ({ block } as CommonListItem) - ) || [] - } - onSelected={type => onSelected?.(type)} - currentItem={current_item} - setCurrentItem={set_current_item} - /> - - - ) : null; -}; - -const RootContainer = styled('div')(({ theme }) => ({ - position: 'fixed', - zIndex: 1, - maxHeight: '525px', - borderRadius: '10px', - boxShadow: theme.affine.shadows.shadow1, - backgroundColor: '#fff', - padding: '8px 4px', -})); - -const ContentContainer = styled('div')(({ theme }) => ({ - display: 'flex', - overflow: 'hidden', - maxHeight: '493px', -})); diff --git a/libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx b/libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx deleted file mode 100644 index af1cd96cea..0000000000 --- a/libs/components/editor-plugins/src/menu/reference-menu/ReferenceMenu.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; - -import { MuiClickAwayListener, styled } from '@toeverything/components/ui'; -import { Virgo, HookType, PluginHooks } from '@toeverything/framework/virgo'; -import { Point } from '@toeverything/utils'; - -import { ReferenceMenuContainer } from './Container'; -import { QueryBlocks, QueryResult } from '../../search'; - -export type ReferenceMenuProps = { - editor: Virgo; - hooks: PluginHooks; - style?: { left: number; top: number }; -}; - -export type RefLinkComponent = { - type: 'reflink'; - reference: string; -}; - -const BEFORE_REGEX = /\[\[(.*)$/; - -export const ReferenceMenu = ({ editor, hooks, style }: ReferenceMenuProps) => { - const [is_show, set_is_show] = useState(false); - const [block_id, set_block_id] = useState(); - const [position, set_position] = useState(new Point(0, 0)); - - const [search_text, set_search_text] = useState(''); - const [search_blocks, set_search_blocks] = useState([]); - - useEffect(() => { - QueryBlocks(editor, search_text, result => set_search_blocks(result)); - }, [editor, search_text]); - - const search_block_ids = useMemo( - () => Object.values(search_blocks).map(({ id }) => id), - [search_blocks] - ); - - const handle_search = useCallback( - async (event: React.KeyboardEvent) => { - const { type, anchorNode } = editor.selection.currentSelectInfo; - if ( - type === 'Range' && - anchorNode && - editor.blockHelper.isSelectionCollapsed(anchorNode.id) - ) { - const text = editor.blockHelper.getBlockTextBeforeSelection( - anchorNode.id - ); - const matched = BEFORE_REGEX.exec(text)?.[1]; - - if (typeof matched === 'string') { - if (event.key === '[') set_is_show(true); - - set_block_id(anchorNode.id); - set_search_text(matched); - - const rect = - editor.selection.currentSelectInfo?.browserSelection - ?.getRangeAt(0) - ?.getBoundingClientRect(); - if (rect) { - set_position(new Point(rect.left, rect.top + 24)); - } - } else if (is_show) { - set_is_show(false); - } - } - }, - [editor, is_show] - ); - - const handle_keyup = useCallback( - (event: React.KeyboardEvent) => handle_search(event), - [handle_search] - ); - - const handle_key_down = useCallback( - (event: React.KeyboardEvent) => { - if (event.code === 'Escape') { - set_is_show(false); - } - }, - [] - ); - - useEffect(() => { - const sub = hooks - .get(HookType.ON_ROOT_NODE_KEYUP) - .subscribe(handle_keyup); - sub.add( - hooks - .get(HookType.ON_ROOT_NODE_KEYDOWN_CAPTURE) - .subscribe(handle_key_down) - ); - - return () => { - sub.unsubscribe(); - }; - }, [handle_keyup, handle_key_down, hooks]); - - const handle_selected = async (reference: string) => { - if (block_id) { - const { anchorNode } = editor.selection.currentSelectInfo; - editor.blockHelper.insertReference( - reference, - anchorNode.id, - editor.selection.currentSelectInfo?.browserSelection, - -search_text.length - 2 - ); - } - - set_is_show(false); - }; - - const handle_close = () => { - block_id && editor.blockHelper.removeSearchSlash(block_id); - }; - - return ( - - set_is_show(false)}> -
- -
-
-
- ); -}; - -const ReferenceMenuWrapper = styled('div')({ - position: 'absolute', - zIndex: 1, -}); diff --git a/libs/components/editor-plugins/src/menu/reference-menu/index.ts b/libs/components/editor-plugins/src/menu/reference-menu/index.ts deleted file mode 100644 index 0a3879e398..0000000000 --- a/libs/components/editor-plugins/src/menu/reference-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ReferenceMenuPlugin } from './Plugin'; diff --git a/libs/components/editor-plugins/src/search/Search.tsx b/libs/components/editor-plugins/src/search/Search.tsx index 6b02b6334e..b689345f64 100644 --- a/libs/components/editor-plugins/src/search/Search.tsx +++ b/libs/components/editor-plugins/src/search/Search.tsx @@ -1,16 +1,15 @@ -import { useCallback, useEffect, useState } from 'react'; -import { useNavigate, useParams } from 'react-router'; -import style9 from 'style9'; - import { BlockPreview } from '@toeverything/components/common'; import { - TransitionsModal, MuiBox as Box, MuiBox, styled, + TransitionsModal, } from '@toeverything/components/ui'; -import { Virgo, BlockEditor } from '@toeverything/framework/virgo'; +import { BlockEditor, Virgo } from '@toeverything/framework/virgo'; import { throttle } from '@toeverything/utils'; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router'; +import style9 from 'style9'; const styles = style9.create({ wrapper: { @@ -37,9 +36,7 @@ const query_blocks = ( search: string, callback: (result: QueryResult) => void ) => { - (editor as BlockEditor) - .search(search) - .then(pages => callback(pages.filter(b => !!b.content))); + (editor as BlockEditor).search(search).then(pages => callback(pages)); }; export const QueryBlocks = throttle(query_blocks, 500); diff --git a/libs/components/ui/src/button/ListButton.tsx b/libs/components/ui/src/button/ListButton.tsx index 1d6cead375..30e5ca9a31 100644 --- a/libs/components/ui/src/button/ListButton.tsx +++ b/libs/components/ui/src/button/ListButton.tsx @@ -1,6 +1,5 @@ import clsx from 'clsx'; import style9 from 'style9'; - import { SvgIconProps } from '../svg-icon'; import { BaseButton } from './base-button'; @@ -35,7 +34,7 @@ const styles = style9.create({ type ListButtonProps = { className?: string; - onClick: () => void; + onClick?: () => void; onMouseOver?: () => void; content?: string; children?: () => JSX.Element; diff --git a/libs/components/ui/src/cascader/Cascader.tsx b/libs/components/ui/src/cascader/Cascader.tsx index 1ba44aab6b..7bf43413f2 100644 --- a/libs/components/ui/src/cascader/Cascader.tsx +++ b/libs/components/ui/src/cascader/Cascader.tsx @@ -1,5 +1,5 @@ import { ArrowRightIcon } from '@toeverything/components/icons'; -import { ReactElement, useRef, useState } from 'react'; +import { useRef, useState, type ReactElement } from 'react'; import { Divider } from '../divider'; import { MuiGrow as Grow, diff --git a/libs/components/ui/src/popper/Popper.tsx b/libs/components/ui/src/popper/Popper.tsx index 924e3be8fd..55d5692927 100644 --- a/libs/components/ui/src/popper/Popper.tsx +++ b/libs/components/ui/src/popper/Popper.tsx @@ -35,6 +35,8 @@ export const Popper = ({ offset = [0, 5], showArrow = false, popperHandlerRef, + onClick, + onClickAway, ...popperProps }: PopperProps) => { const [anchorEl, setAnchorEl] = useState(null); @@ -97,15 +99,20 @@ export const Popper = ({ return ( { - setVisible(false); + if (visibleControlledByParent) { + onClickAway?.(); + } else { + setVisible(false); + } }} > {isAnchorCustom ? null : (
setAnchorEl(dom)} - onClick={() => { + onClick={e => { if (!hasClickTrigger || visibleControlledByParent) { + onClick?.(e); return; } setVisible(!visible); diff --git a/libs/components/ui/src/popper/interface.ts b/libs/components/ui/src/popper/interface.ts index b449eff61f..1f7473ebf4 100644 --- a/libs/components/ui/src/popper/interface.ts +++ b/libs/components/ui/src/popper/interface.ts @@ -62,4 +62,6 @@ export type PopperProps = { showArrow?: boolean; popperHandlerRef?: Ref; + + onClickAway?: () => void; } & Omit; diff --git a/libs/components/ui/src/theme/theme.ts b/libs/components/ui/src/theme/theme.ts index 76d42e3a0a..ce05c7b54d 100644 --- a/libs/components/ui/src/theme/theme.ts +++ b/libs/components/ui/src/theme/theme.ts @@ -233,7 +233,10 @@ export const Theme = { }, shadows: { none: 'none', - shadow1: '0px 1px 5px rgba(152, 172, 189, 0.2)', + shadow1: + '0px 1px 10px -6px rgba(24, 39, 75, 0.08), 0px 3px 16px -6px rgba(24, 39, 75, 0.04)', + shadow2: + '0px 6px 16px -8px rgba(0,0,0,0.08), 0px 9px 14px 0px rgba(0,0,0,0.05), 0px 12px 24px 16px rgba(0,0,0,0.03)', }, border: ['none'], spacing: { diff --git a/libs/datasource/db-service/src/services/database/index.ts b/libs/datasource/db-service/src/services/database/index.ts index 4ea5bbbace..d6d013a3d3 100644 --- a/libs/datasource/db-service/src/services/database/index.ts +++ b/libs/datasource/db-service/src/services/database/index.ts @@ -168,6 +168,7 @@ export class Database { block.on('children', observer_name, listener); block.on('content', observer_name, listener); block.on('parent', observer_name, listener); + block.on('cascade', observer_name, listener); } this.#observers.setStatus(observer_name, 'observing'); @@ -188,6 +189,7 @@ export class Database { block.off('children', observer_name); block.off('content', observer_name); block.off('parent', observer_name); + block.off('cascade', observer_name); } } } diff --git a/libs/datasource/db-service/src/services/editor-block/templates/template-factory.ts b/libs/datasource/db-service/src/services/editor-block/templates/template-factory.ts index 6c8d5a5643..4cdf62676d 100644 --- a/libs/datasource/db-service/src/services/editor-block/templates/template-factory.ts +++ b/libs/datasource/db-service/src/services/editor-block/templates/template-factory.ts @@ -29,7 +29,7 @@ const groupTemplateMap = { const defaultTemplateList: Array = [ { name: 'New From Quick Start', - groupKeys: ['todolist'], + groupKeys: ['getStartedGroup0', 'getStartedGroup1', 'getStartedGroup2'], }, { name: 'New From Grid System', groupKeys: ['grid'] }, { name: 'New From Blog', groupKeys: ['blog'] }, diff --git a/libs/datasource/db-service/src/services/editor-block/utils/column/default-config.ts b/libs/datasource/db-service/src/services/editor-block/utils/column/default-config.ts index f2d1c861b6..ca51be0f8c 100644 --- a/libs/datasource/db-service/src/services/editor-block/utils/column/default-config.ts +++ b/libs/datasource/db-service/src/services/editor-block/utils/column/default-config.ts @@ -53,6 +53,7 @@ export type DefaultColumnsValue = { filterConstraint: FilterConstraint; filterWeakSqlConstraint: string; sorterConstraint: SorterConstraint; + bindings: StringColumnValue; }; export const DEFAULT_COLUMN_KEYS = { diff --git a/libs/datasource/jwt/package.json b/libs/datasource/jwt/package.json index dd8f6f6761..b43942b6ee 100644 --- a/libs/datasource/jwt/package.json +++ b/libs/datasource/jwt/package.json @@ -12,7 +12,7 @@ "file-selector": "^0.6.0", "flexsearch": "^0.7.21", "lib0": "^0.2.52", - "lru-cache": "^7.13.2", + "lru-cache": "^7.14.0", "ts-debounce": "^4.0.0" }, "dependencies": { diff --git a/libs/datasource/jwt/src/block/abstract.ts b/libs/datasource/jwt/src/block/abstract.ts index 38ee415e05..d67c92bb68 100644 --- a/libs/datasource/jwt/src/block/abstract.ts +++ b/libs/datasource/jwt/src/block/abstract.ts @@ -1,3 +1,10 @@ +import { + BlockFlavorKeys, + BlockFlavors, + BlockTypeKeys, + BlockTypes, +} from '../types'; +import { getLogger } from '../utils'; import { BlockInstance, BlockListener, @@ -6,14 +13,7 @@ import { ContentTypes, HistoryManager, MapOperation, -} from '../adapter'; -import { - BlockFlavorKeys, - BlockFlavors, - BlockTypeKeys, - BlockTypes, -} from '../types'; -import { getLogger } from '../utils'; +} from '../yjs/types'; declare const JWT_DEV: boolean; const logger = getLogger('BlockDB:block'); @@ -21,6 +21,7 @@ const logger_debug = getLogger('debug:BlockDB:block'); const _GET_BLOCK = Symbol('GET_BLOCK'); const _SET_PARENT = Symbol('SET_PARENT'); +const _EMIT_EVENT = Symbol('EMIT_EVENT'); export class AbstractBlock< B extends BlockInstance, @@ -29,10 +30,15 @@ export class AbstractBlock< private readonly _id: string; private readonly _block: BlockInstance; private readonly _history: HistoryManager; - private readonly _root?: AbstractBlock; - private readonly _parentListener: Map; + private readonly _root: AbstractBlock | undefined; + private readonly _listeners: { + cascade: Map; + children: Map; + content: Map; + parent: Map; + }; - private _parent?: AbstractBlock; + private _parent: AbstractBlock | undefined; private _changeParent?: () => void; constructor( @@ -45,10 +51,24 @@ export class AbstractBlock< this._history = this._block.scopedHistory([this._id]); this._root = root; - this._parentListener = new Map(); + this._listeners = { + cascade: new Map(), + children: new Map(), + content: new Map(), + parent: new Map(), + }; + + this._block.on('content', 'internal', states => + this[_EMIT_EVENT]('content', states) + ); + this._block.on('children', 'internal', states => + this[_EMIT_EVENT]('children', states) + ); JWT_DEV && logger_debug(`init: exists ${this._id}`); - if (parent) this._refreshParent(parent); + if (parent) { + this._refreshParent(parent); + } } public get root() { @@ -77,23 +97,18 @@ export class AbstractBlock< } public on( - event: 'content' | 'children' | 'parent', + event: 'cascade' | 'content' | 'children' | 'parent', name: string, callback: BlockListener ) { - if (event === 'parent') { - this._parentListener.set(name, callback); - } else { - this._block.on(event, name, callback); - } + this._listeners[event]?.set(name, callback); } - public off(event: 'content' | 'children' | 'parent', name: string) { - if (event === 'parent') { - this._parentListener.delete(name); - } else { - this._block.off(event, name); - } + public off( + event: 'cascade' | 'content' | 'children' | 'parent', + name: string + ) { + this._listeners[event]?.delete(name); } public addChildrenListener(name: string, listener: BlockListener) { @@ -146,7 +161,7 @@ export class AbstractBlock< return new Date(timestamp) .toISOString() .split('T')[0] - .replace(/-/g, ''); + ?.replace(/-/g, ''); } // eslint-disable-next-line no-empty } catch (e) {} @@ -187,8 +202,22 @@ export class AbstractBlock< const states: Map = new Map([ [parentId, type], ]); - for (const listener of this._parentListener.values()) { - listener(states); + this[_EMIT_EVENT]('parent', states); + } + + [_EMIT_EVENT]( + event: 'cascade' | 'content' | 'children' | 'parent', + states: Parameters[0] + ) { + const listeners = this._listeners[event]; + if (listeners) { + for (const listener of listeners.values()) { + listener(states); + } + } + + if (['children', 'content'].includes(event)) { + this._parent?.[_EMIT_EVENT]('cascade', states); } } @@ -259,7 +288,7 @@ export class AbstractBlock< } /** - * Insert sub-Block + * Insert children block * @param block Block instance * @param position Insertion position, if it is empty, it will be inserted at the end. If the block already exists, the position will be moved * @returns @@ -270,9 +299,12 @@ export class AbstractBlock< ) { JWT_DEV && logger(`insertChildren: start`); - if (block.id === this._id) return; // avoid self-reference + if (block.id === this._id) { + // avoid self-reference + return; + } if ( - this.type !== BlockTypes.block || // binary cannot insert subblocks + this.type !== BlockTypes.block || // binary cannot insert children blocks (block.type !== BlockTypes.block && this.flavor !== BlockFlavors.workspace) // binary can only be inserted into workspace ) { diff --git a/libs/datasource/jwt/src/block/base.ts b/libs/datasource/jwt/src/block/base.ts index 72183187b1..b5872980db 100644 --- a/libs/datasource/jwt/src/block/base.ts +++ b/libs/datasource/jwt/src/block/base.ts @@ -1,3 +1,5 @@ +import { BlockItem } from '../types'; +import { getLogger } from '../utils'; import { ArrayOperation, BlockInstance, @@ -5,9 +7,7 @@ import { ContentTypes, InternalPlainObject, MapOperation, -} from '../adapter'; -import { BlockItem } from '../types'; -import { getLogger } from '../utils'; +} from '../yjs/types'; import { AbstractBlock } from './abstract'; import { BlockCapability } from './capability'; @@ -23,8 +23,8 @@ export interface Decoration extends InternalPlainObject { type Validator = (value: T | undefined) => boolean | void; export type IndexMetadata = Readonly<{ - content?: string; - reference?: string; + content: string | undefined; + reference: string | undefined; tags: string[]; }>; export type QueryMetadata = Readonly< @@ -51,7 +51,7 @@ export class BaseBlock< B extends BlockInstance, C extends ContentOperation > extends AbstractBlock { - private readonly _exporters?: Exporters; + private readonly _exporters: Exporters | undefined; private readonly _contentExportersGetter: () => Map< string, ReadableContentExporter @@ -68,7 +68,7 @@ export class BaseBlock< ReadableContentExporter >; - validators: Map = new Map(); + private readonly _validators: Map = new Map(); constructor( block: B, @@ -157,14 +157,14 @@ export class BaseBlock< setValidator(key: string, validator?: Validator) { if (validator) { - this.validators.set(key, validator); + this._validators.set(key, validator); } else { - this.validators.delete(key); + this._validators.delete(key); } } private validate(key: string, value: unknown): boolean { - const validate = this.validators.get(key); + const validate = this._validators.get(key); if (validate) { return validate(value) === false ? false : true; } @@ -179,7 +179,7 @@ export class BaseBlock< } /** - * Get an instance of the child Block + * Get an instance of the child block * @param blockId block id */ private get_children_instance(blockId?: string): BaseBlock[] { @@ -201,9 +201,15 @@ export class BaseBlock< } try { const parent_page = this._getParentPage(false); - if (parent_page) metadata['page'] = parent_page; - if (this.group) metadata['group'] = this.group.id; - if (this.parent) metadata['parent'] = this.parent.id; + if (parent_page) { + metadata['page'] = parent_page; + } + if (this.group) { + metadata['group'] = this.group.id; + } + if (this.parent) { + metadata['parent'] = this.parent.id; + } } catch (e) { logger(`Failed to export default metadata`, e); } @@ -228,7 +234,9 @@ export class BaseBlock< for (const [name, exporter] of this._contentExportersGetter()) { try { const content = exporter(this.getContent()); - if (content) contents.push(content); + if (content) { + contents.push(content); + } } catch (err) { logger(`Failed to export content: ${name}`, err); } diff --git a/libs/datasource/jwt/src/block/indexer.ts b/libs/datasource/jwt/src/block/indexer.ts index dfee685b7e..cd430ce6d6 100644 --- a/libs/datasource/jwt/src/block/indexer.ts +++ b/libs/datasource/jwt/src/block/indexer.ts @@ -7,14 +7,14 @@ import produce from 'immer'; import LRUCache from 'lru-cache'; import sift, { Query } from 'sift'; +import { BlockFlavors } from '../types'; +import { BlockEventBus, getLogger } from '../utils'; import { AsyncDatabaseAdapter, BlockInstance, ChangedStates, ContentOperation, -} from '../adapter'; -import { BlockFlavors } from '../types'; -import { BlockEventBus, getLogger } from '../utils'; +} from '../yjs/types'; import { BaseBlock, IndexMetadata, QueryMetadata } from './base'; @@ -123,8 +123,8 @@ export class BlockIndexer< constructor( adapter: A, workspace: string, - block_builder: (block: BlockInstance) => Promise>, - event_bus: BlockEventBus + blockBuilder: (block: BlockInstance) => Promise>, + eventBus: BlockEventBus ) { this._adapter = adapter; this._idb = initIndexIdb(workspace); @@ -144,8 +144,8 @@ export class BlockIndexer< ttl: 1000 * 60 * 30, }); - this._blockBuilder = block_builder; - this._eventBus = event_bus; + this._blockBuilder = blockBuilder; + this._eventBus = eventBus; this._delayIndex = { documents: new Map() }; @@ -315,7 +315,9 @@ export class BlockIndexer< private _testMetaKey(key: string) { try { const metadata = this._blockMetadata.values().next().value; - if (!metadata || typeof metadata !== 'object') return false; + if (!metadata || typeof metadata !== 'object') { + return false; + } return !!(key in metadata); } catch (e) { return false; @@ -324,8 +326,11 @@ export class BlockIndexer< private _getSortedMetadata(sort: string, desc?: boolean) { const sorter = naturalSort(Array.from(this._blockMetadata.entries())); - if (desc) return sorter.desc(([, m]) => m[sort]); - else return sorter.asc(([, m]) => m[sort]); + if (desc) { + return sorter.desc(([, m]) => m[sort]); + } else { + return sorter.asc(([, m]) => m[sort]); + } } public query(query: QueryIndexMetadata) { @@ -337,15 +342,23 @@ export class BlockIndexer< if ($sort && this._testMetaKey($sort)) { const metadata = this._getSortedMetadata($sort, $desc); metadata.forEach(([key, value]) => { - if (matches.length > limit) return; - if (filter(value)) matches.push(key); + if (matches.length > limit) { + return; + } + if (filter(value)) { + matches.push(key); + } }); return matches; } else { this._blockMetadata.forEach((value, key) => { - if (matches.length > limit) return; - if (filter(value)) matches.push(key); + if (matches.length > limit) { + return; + } + if (filter(value)) { + matches.push(key); + } }); return matches; diff --git a/libs/datasource/jwt/src/index.ts b/libs/datasource/jwt/src/index.ts index 50fec96d5e..e8f3483e23 100644 --- a/libs/datasource/jwt/src/index.ts +++ b/libs/datasource/jwt/src/index.ts @@ -2,26 +2,6 @@ import { DocumentSearchOptions } from 'flexsearch'; import LRUCache from 'lru-cache'; -import { - AsyncDatabaseAdapter, - BlockInstance, - BlockListener, - ChangedStates, - Connectivity, - ContentOperation, - ContentTypes, - DataExporter, - getDataExporter, - HistoryManager, - YjsAdapter, - YjsContentOperation, - YjsInitOptions, -} from './adapter'; -import { - getYjsProviders, - YjsBlockInstance, - YjsProviderOptions, -} from './adapter/yjs'; import { BaseBlock, BlockIndexer, @@ -40,6 +20,26 @@ import { UUID, } from './types'; import { BlockEventBus, genUUID, getLogger } from './utils'; +import { + getYjsProviders, + YjsAdapter, + YjsBlockInstance, + YjsContentOperation, + YjsInitOptions, + YjsProviderOptions, +} from './yjs'; +import { + AsyncDatabaseAdapter, + BlockInstance, + BlockListener, + ChangedStates, + Connectivity, + ContentOperation, + ContentTypes, + DataExporter, + getDataExporter, + HistoryManager, +} from './yjs/types'; declare const JWT_DEV: boolean; @@ -158,8 +158,11 @@ export class BlockClient< public addBlockListener(tag: string, listener: BlockListener) { const bus = this._eventBus.topic('updated'); - if (tag !== 'index' || !bus.has(tag)) bus.on(tag, listener); - else console.error(`block listener ${tag} is reserved or exists`); + if (tag !== 'index' || !bus.has(tag)) { + bus.on(tag, listener); + } else { + console.error(`block listener ${tag} is reserved or exists`); + } } public removeBlockListener(tag: string) { @@ -171,8 +174,11 @@ export class BlockClient< listener: BlockListener> ) { const bus = this._eventBus.topic>>('editing'); - if (tag !== 'index' || !bus.has(tag)) bus.on(tag, listener); - else console.error(`editing listener ${tag} is reserved or exists`); + if (tag !== 'index' || !bus.has(tag)) { + bus.on(tag, listener); + } else { + console.error(`editing listener ${tag} is reserved or exists`); + } } public removeEditingListener(tag: string) { @@ -185,15 +191,18 @@ export class BlockClient< ) { const bus = this._eventBus.topic>('connectivity'); - if (tag !== 'index' || !bus.has(tag)) bus.on(tag, listener); - else + if (tag !== 'index' || !bus.has(tag)) { + bus.on(tag, listener); + } else { console.error(`connectivity listener ${tag} is reserved or exists`); + } } public removeConnectivityListener(tag: string) { this._eventBus.topic('connectivity').off(tag); } + // @ts-ignore private inspector() { return { ...this._adapter.inspector(), @@ -221,7 +230,9 @@ export class BlockClient< this.addBlockListener('index', async states => { await Promise.allSettled( Array.from(states.entries()).map(([id, state]) => { - if (state === 'delete') this._blockCaches.delete(id); + if (state === 'delete') { + this._blockCaches.delete(id); + } return this._blockIndexer.refreshIndex(id, state); }) ); @@ -256,7 +267,7 @@ export class BlockClient< } /** - * research all + * Full text search * @param part_of_title_or_content Title or content keyword, support Chinese * @param part_of_title_or_content.index search range, optional values: title, ttl, content, reference * @param part_of_title_or_content.tag tag, string or array of strings, supports multiple tags @@ -290,24 +301,30 @@ export class BlockClient< | string | Partial> ): Promise { - const promised_pages = await Promise.all( - this.search(part_of_title_or_content).flatMap(({ result }) => - result.map(async id => { - const page = this._pageMapping.get(id as string); - if (page) return page; - const block = await this.get(id as BlockTypeKeys); - return this.set_page(block); - }) - ) - ); - const pages = [ - ...new Set(promised_pages.filter((v): v is string => !!v)), - ]; + let pages = []; + if (part_of_title_or_content) { + const promisedPages = await Promise.all( + this.search(part_of_title_or_content).flatMap(({ result }) => + result.map(async id => { + const page = this._pageMapping.get(id as string); + if (page) { + return page; + } + const block = await this.get(id as BlockTypeKeys); + return this.set_page(block); + }) + ) + ); + pages = [...new Set(promisedPages.filter((v): v is string => !!v))]; + } else { + pages = await this.getBlockByFlavor('page'); + } return Promise.all( this._blockIndexer.getMetadata(pages).map(async page => ({ - content: this.get_decoded_content( - await this._adapter.getBlock(page.id) - ), + content: + this.get_decoded_content( + await this._adapter.getBlock(page.id) + ) || '', ...page, })) ); @@ -325,9 +342,10 @@ export class BlockClient< const ids = this.query(query); return Promise.all( this._blockIndexer.getMetadata(ids).map(async page => ({ - content: this.get_decoded_content( - await this._adapter.getBlock(page.id) - ), + content: + this.get_decoded_content( + await this._adapter.getBlock(page.id) + ) || '', ...page, })) ); @@ -376,7 +394,7 @@ export class BlockClient< private async get_parent(id: string) { const parents = this._parentMapping.get(id); - if (parents) { + if (parents && parents[0]) { const parent_block_id = parents[0]; if (!this._blockCaches.has(parent_block_id)) { this._blockCaches.set( @@ -403,7 +421,9 @@ export class BlockClient< private set_page(block: BaseBlock) { const page = this._pageMapping.get(block.id); - if (page) return page; + if (page) { + return page; + } const parent_page = block.parent_page; if (parent_page) { this._pageMapping.set(block.id, parent_page); @@ -470,8 +490,9 @@ export class BlockClient< matched += 1; } } - if (matched === conditions.length) + if (matched === conditions.length) { exporters.push([name, exporter] as const); + } } return exporters; @@ -485,7 +506,9 @@ export class BlockClient< ); if (exporter) { const op = block.content.asMap(); - if (op) return exporter[1](op); + if (op) { + return exporter[1](op); + } } } return undefined; @@ -564,7 +587,9 @@ export class BlockClient< this.set_page(abstract_block); abstract_block.on('parent', 'client_hook', state => { const [parent] = state.keys(); - this.set_parent(parent, abstract_block.id); + if (parent) { + this.set_parent(parent, abstract_block.id); + } this.set_page(abstract_block); }); this._blockCaches.set(abstract_block.id, abstract_block); @@ -648,19 +673,19 @@ export type BlockInitOptions = NonNullable< Parameters[1] >; -export type { - ArrayOperation, - ChangedStates, - Connectivity, - MapOperation, - TextOperation, -} from './adapter'; export type { BlockSearchItem, Decoration as BlockDecoration, ReadableContentExporter as BlockContentExporter, } from './block'; export { BlockTypes, BucketBackend as BlockBackend } from './types'; -export type { BlockTypeKeys } from './types'; +export type { BlockFlavorKeys, BlockTypeKeys } from './types'; export { isBlock } from './utils'; +export type { + ArrayOperation, + ChangedStates, + Connectivity, + MapOperation, + TextOperation, +} from './yjs/types'; export type { QueryIndexMetadata }; diff --git a/libs/datasource/jwt/src/types/block.ts b/libs/datasource/jwt/src/types/block.ts index 3b04dc7830..d1867f1ef9 100644 --- a/libs/datasource/jwt/src/types/block.ts +++ b/libs/datasource/jwt/src/types/block.ts @@ -1,4 +1,4 @@ -import { ContentOperation } from '../adapter'; +import { ContentOperation } from '../yjs/types'; import { RefMetadata } from './metadata'; import { UUID } from './uuid'; // base type of block @@ -60,8 +60,8 @@ export type BlockItem = { flavor: typeof BlockFlavors[BlockFlavorKeys]; children: string[]; readonly created: number; // creation time, UTC timestamp - readonly updated?: number; // update time, UTC timestamp - readonly creator?: string; // creator id + readonly updated?: number | undefined; // update time, UTC timestamp + readonly creator?: string | undefined; // creator id content: C; // Essentially what is stored here is either Uint8Array (binary resource) or YDoc (structured resource) }; diff --git a/libs/datasource/jwt/src/utils/event-bus.ts b/libs/datasource/jwt/src/utils/event-bus.ts index 50f5363390..85096dd6ed 100644 --- a/libs/datasource/jwt/src/utils/event-bus.ts +++ b/libs/datasource/jwt/src/utils/event-bus.ts @@ -81,7 +81,7 @@ class BlockScopedEventBus extends BlockEventBus { options?: ListenerOptions ) { if (options?.debounce) { - const { wait, maxWait } = options.debounce; + const { wait, maxWait = 500 } = options.debounce; const debounced = debounce(listener, wait, { maxWait }); this.on_listener(this._topic, name, e => { debounced((e as CustomEvent)?.detail); diff --git a/libs/datasource/jwt/src/utils/index.ts b/libs/datasource/jwt/src/utils/index.ts index f19e5633a1..3d0d3bb101 100644 --- a/libs/datasource/jwt/src/utils/index.ts +++ b/libs/datasource/jwt/src/utils/index.ts @@ -39,7 +39,9 @@ export function getLogger(namespace: string) { if (JWT_DEV) { const logger = debug(namespace); logger.log = console.log.bind(console); - if (JWT_DEV === ('testing' as any)) logger.enabled = true; + if (JWT_DEV === ('testing' as any)) { + logger.enabled = true; + } return logger; } else { return () => {}; diff --git a/libs/datasource/jwt/src/adapter/yjs/binary.ts b/libs/datasource/jwt/src/yjs/binary.ts similarity index 96% rename from libs/datasource/jwt/src/adapter/yjs/binary.ts rename to libs/datasource/jwt/src/yjs/binary.ts index 468710ac4a..c156452c78 100644 --- a/libs/datasource/jwt/src/adapter/yjs/binary.ts +++ b/libs/datasource/jwt/src/yjs/binary.ts @@ -25,7 +25,7 @@ export class YjsRemoteBinaries { } else { // TODO: Remote Load try { - const file = await this._remoteStorage?.instance.getBuffData( + const file = await this._remoteStorage?.instance?.getBuffData( name ); console.log(file); @@ -44,11 +44,11 @@ export class YjsRemoteBinaries { this._binaries.set(name, binary); if (this._remoteStorage) { // TODO: Remote Save, if there is an object with the same name remotely, the upload is skipped, because the file name is the hash of the file content - const has_file = this._remoteStorage.instance.exist(name); + const has_file = this._remoteStorage.instance?.exist(name); if (!has_file) { const upload_file = new File(binary.toArray(), name); await this._remoteStorage.instance - .upload(upload_file) + ?.upload(upload_file) .catch(err => { throw new Error(`${err} upload error`); }); diff --git a/libs/datasource/jwt/src/adapter/yjs/block.ts b/libs/datasource/jwt/src/yjs/block.ts similarity index 95% rename from libs/datasource/jwt/src/adapter/yjs/block.ts rename to libs/datasource/jwt/src/yjs/block.ts index 3f2b756799..513016f90e 100644 --- a/libs/datasource/jwt/src/adapter/yjs/block.ts +++ b/libs/datasource/jwt/src/yjs/block.ts @@ -5,8 +5,8 @@ import { transact, } from 'yjs'; -import { BlockItem, BlockTypes } from '../../types'; -import { BlockInstance, BlockListener, HistoryManager } from '../index'; +import { BlockItem, BlockTypes } from '../types'; +import { BlockInstance, BlockListener, HistoryManager } from './types'; import { YjsHistoryManager } from './history'; import { ChildrenListenerHandler, ContentListenerHandler } from './listener'; @@ -34,7 +34,7 @@ type YjsBlockInstanceProps = { export class YjsBlockInstance implements BlockInstance { private readonly _id: string; private readonly _block: YMap; - private readonly _binary?: YArray; + private readonly _binary: YArray | undefined; private readonly _children: YArray; private readonly _setBlock: ( id: string, @@ -48,8 +48,7 @@ export class YjsBlockInstance implements BlockInstance { private readonly _childrenListeners: Map; private readonly _contentListeners: Map; - // eslint-disable-next-line @typescript-eslint/naming-convention - _childrenMap: Map; + private _childrenMap: Map; constructor(props: YjsBlockInstanceProps) { this._id = props.id; @@ -184,7 +183,9 @@ export class YjsBlockInstance implements BlockInstance { } hasChildren(id: string): boolean { - if (this.children.includes(id)) return true; + if (this.children.includes(id)) { + return true; + } return this.getChildren().some(block => block.hasChildren(id)); } @@ -263,7 +264,9 @@ export class YjsBlockInstance implements BlockInstance { break; } } - if (id) failed.push(id); + if (id) { + failed.push(id); + } } this._childrenMap = getMapFromYArray(this._children); diff --git a/libs/datasource/jwt/src/adapter/yjs/gatekeeper.ts b/libs/datasource/jwt/src/yjs/gatekeeper.ts similarity index 95% rename from libs/datasource/jwt/src/adapter/yjs/gatekeeper.ts rename to libs/datasource/jwt/src/yjs/gatekeeper.ts index 7e02c5a09e..9acf1becf5 100644 --- a/libs/datasource/jwt/src/adapter/yjs/gatekeeper.ts +++ b/libs/datasource/jwt/src/yjs/gatekeeper.ts @@ -32,7 +32,7 @@ export class GateKeeper { return creator === this._userId || !!this._common.get(block_id); } - checkDeleteLists(block_ids: string[]) { + checkDeleteLists(block_ids: string[]): [string[], string[]] { const success = []; const fail = []; for (const block_id of block_ids) { diff --git a/libs/datasource/jwt/src/adapter/yjs/history.ts b/libs/datasource/jwt/src/yjs/history.ts similarity index 95% rename from libs/datasource/jwt/src/adapter/yjs/history.ts rename to libs/datasource/jwt/src/yjs/history.ts index 6739cf9a50..5c6f5c4001 100644 --- a/libs/datasource/jwt/src/adapter/yjs/history.ts +++ b/libs/datasource/jwt/src/yjs/history.ts @@ -1,10 +1,11 @@ import { Map as YMap, UndoManager } from 'yjs'; -import { HistoryCallback, HistoryManager } from '../../adapter'; +import { HistoryCallback, HistoryManager } from './types'; type StackItem = UndoManager['undoStack'][0]; export class YjsHistoryManager implements HistoryManager { + // @ts-ignore private readonly _blocks: YMap; private readonly _historyManager: UndoManager; private readonly _pushListeners: Map>; @@ -56,7 +57,7 @@ export class YjsHistoryManager implements HistoryManager { } break(): void { - // this.#history_manager. + // this._historyManager. } undo(): Map | undefined { diff --git a/libs/datasource/jwt/src/adapter/yjs/index.ts b/libs/datasource/jwt/src/yjs/index.ts similarity index 95% rename from libs/datasource/jwt/src/adapter/yjs/index.ts rename to libs/datasource/jwt/src/yjs/index.ts index ec201c2d84..7d0f30e4e3 100644 --- a/libs/datasource/jwt/src/adapter/yjs/index.ts +++ b/libs/datasource/jwt/src/yjs/index.ts @@ -18,15 +18,15 @@ import { transact, } from 'yjs'; +import { BlockItem, BlockTypes } from '../types'; +import { getLogger, sha3, sleep } from '../utils'; import { AsyncDatabaseAdapter, BlockListener, ChangedStateKeys, Connectivity, HistoryManager, -} from '../../adapter'; -import { BlockItem, BlockTypes } from '../../types'; -import { getLogger, sha3, sleep } from '../../utils'; +} from './types'; import { YjsRemoteBinaries } from './binary'; import { YjsBlockInstance } from './block'; @@ -40,6 +40,7 @@ import { import { YjsProvider } from './provider'; declare const JWT_DEV: boolean; +// @ts-ignore const logger = getLogger('BlockDB:yjs'); type ConnectivityListener = ( @@ -54,7 +55,7 @@ type YjsProviders = { gatekeeper: GateKeeper; connListener: { listeners?: ConnectivityListener }; userId: string; - remoteToken?: string; // remote storage token + remoteToken: string | undefined; // remote storage token }; const _yjsDatabaseInstance = new Map(); @@ -70,8 +71,8 @@ async function _initYjsDatabase( workspace: string, options: { userId: string; - token?: string; - provider?: Record; + token?: string | undefined; + provider?: Record | undefined; } ): Promise { if (_asyncInitLoading.has(workspace)) { @@ -243,9 +244,10 @@ export class YjsAdapter implements AsyncDatabaseAdapter { typeof updated === 'number' && updated + 1000 * 10 > Date.now() ) { - if (!editing_mapping[editing]) + if (!editing_mapping[editing]) { editing_mapping[editing] = []; - editing_mapping[editing].push(userId); + } + editing_mapping[editing]?.push(userId); } } listener( @@ -338,7 +340,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter { ], }); const [file] = (await fromEvent(handles)) as File[]; - const binary = await file.arrayBuffer(); + const binary = await file?.arrayBuffer(); // await this._provider.idb.clearData(); const doc = new Doc({ autoLoad: true, shouldLoad: true }); let updated = 0; @@ -348,7 +350,9 @@ export class YjsAdapter implements AsyncDatabaseAdapter { updated += 1; }); setInterval(() => { - if (updated > 0) updated -= 1; + if (updated > 0) { + updated -= 1; + } }, 500); const update_check = new Promise(resolve => { @@ -362,8 +366,10 @@ export class YjsAdapter implements AsyncDatabaseAdapter { }); // await new IndexedDBProvider(this._provider.idb.name, doc) // .whenSynced; - applyUpdate(doc, new Uint8Array(binary)); - await update_check; + if (binary) { + applyUpdate(doc, new Uint8Array(binary)); + await update_check; + } console.log('load success'); }, parse: () => this._doc.toJSON(), @@ -409,8 +415,8 @@ export class YjsAdapter implements AsyncDatabaseAdapter { async createBlock( options: Pick, 'type' | 'flavor'> & { - uuid?: string; - binary?: ArrayBufferLike; + uuid: string | undefined; + binary: ArrayBufferLike | undefined; } ): Promise { const uuid = options.uuid || `affine${nanoid(16)}`; @@ -481,7 +487,9 @@ export class YjsAdapter implements AsyncDatabaseAdapter { async getBlock(id: string): Promise { const block_instance = this.get_block_sync(id); - if (block_instance) return block_instance; + if (block_instance) { + return block_instance; + } const block = this._blocks.get(id); if (block && block.get('type') === BlockTypes.binary) { const binary = await this._binaries.get( @@ -540,7 +548,9 @@ export class YjsAdapter implements AsyncDatabaseAdapter { let uploaded: Promise | undefined; if (!block.size) { const content = item.content[INTO_INNER](); - if (!content) return reject(); + if (!content) { + return reject(); + } const children = new YArray(); children.push(item.children); diff --git a/libs/datasource/jwt/src/adapter/yjs/listener.ts b/libs/datasource/jwt/src/yjs/listener.ts similarity index 98% rename from libs/datasource/jwt/src/adapter/yjs/listener.ts rename to libs/datasource/jwt/src/yjs/listener.ts index 36c203bbce..a109862b20 100644 --- a/libs/datasource/jwt/src/adapter/yjs/listener.ts +++ b/libs/datasource/jwt/src/yjs/listener.ts @@ -1,7 +1,8 @@ import { produce } from 'immer'; import { debounce } from 'ts-debounce'; import { YEvent } from 'yjs'; -import { BlockListener, ChangedStateKeys } from '../index'; + +import { BlockListener, ChangedStateKeys } from './types'; let listener_suspend = false; diff --git a/libs/datasource/jwt/src/adapter/yjs/operation.ts b/libs/datasource/jwt/src/yjs/operation.ts similarity index 93% rename from libs/datasource/jwt/src/adapter/yjs/operation.ts rename to libs/datasource/jwt/src/yjs/operation.ts index a8dfdaffa4..8d1bac4723 100644 --- a/libs/datasource/jwt/src/adapter/yjs/operation.ts +++ b/libs/datasource/jwt/src/yjs/operation.ts @@ -6,6 +6,7 @@ import { Text as YText, } from 'yjs'; +import { ChildrenListenerHandler, ContentListenerHandler } from './listener'; import { ArrayOperation, BaseTypes, @@ -16,21 +17,29 @@ import { Operable, TextOperation, TextToken, -} from '../index'; -import { ChildrenListenerHandler, ContentListenerHandler } from './listener'; +} from './types'; const INTO_INNER = Symbol('INTO_INNER'); export const DO_NOT_USE_THIS_OR_YOU_WILL_BE_FIRED_SYMBOL_INTO_INNER: typeof INTO_INNER = INTO_INNER; -function auto_get(root: ContentOperation, key: string): unknown | undefined { +function auto_get( + root: ContentOperation, + key: string | undefined +): unknown | undefined { const array = root.asArray(); - if (array && !Number.isNaN(Number(key))) return array.get(Number(key)); + if (array && !Number.isNaN(Number(key))) { + return array.get(Number(key)); + } const map = root.asMap(); - if (map) return map.get(key); + if (map && key) { + return map.get(key); + } const text = root.asText(); - if (text) return text.toString(); + if (text) { + return text.toString(); + } console.error('auto_get unknown root', root, key); return undefined; } @@ -150,7 +159,9 @@ export class YjsContentOperation implements ContentOperation { if (root instanceof YjsContentOperation) { if (path.length === 1) { const [key] = path; - if (key) return auto_set(root, key, data); + if (key) { + return auto_set(root, key, data); + } console.error('autoSet unknown path', root, path, data); return; } @@ -191,6 +202,7 @@ export class YjsContentOperation implements ContentOperation { } // eslint-disable-next-line @typescript-eslint/naming-convention + // @ts-ignore private toJSON() { return this._content.toJSON(); } @@ -283,7 +295,9 @@ class YjsArrayOperation get(index: number): Operable | undefined { const content = this._arrayContent.get(index); - if (content) return this.to_operable(content); + if (content) { + return this.to_operable(content); + } return undefined; } @@ -368,7 +382,9 @@ class YjsMapOperation set(key: string, value: Operable): void { if (value instanceof YjsContentOperation) { const content = value[INTO_INNER](); - if (content) this._mapContent.set(key, content as unknown as T); + if (content) { + this._mapContent.set(key, content as unknown as T); + } } else { this._mapContent.set(key, value as T); } @@ -376,7 +392,9 @@ class YjsMapOperation get(key: string): Operable | undefined { const content = this._mapContent.get(key); - if (content) return this.to_operable(content); + if (content) { + return this.to_operable(content); + } return undefined; } diff --git a/libs/datasource/jwt/src/adapter/yjs/provider.ts b/libs/datasource/jwt/src/yjs/provider.ts similarity index 97% rename from libs/datasource/jwt/src/adapter/yjs/provider.ts rename to libs/datasource/jwt/src/yjs/provider.ts index ba1835cc52..99a09fd69c 100644 --- a/libs/datasource/jwt/src/adapter/yjs/provider.ts +++ b/libs/datasource/jwt/src/yjs/provider.ts @@ -7,13 +7,13 @@ import { WebsocketProvider, } from '@toeverything/datasource/jwt-rpc'; -import { Connectivity } from '../../adapter'; -import { BucketBackend } from '../../types'; +import { BucketBackend } from '../types'; +import { Connectivity } from './types'; type YjsDefaultInstances = { awareness: Awareness; doc: Doc; - token?: string; + token?: string | undefined; workspace: string; emitState: (connectivity: Connectivity) => void; }; diff --git a/libs/datasource/jwt/src/adapter/index.ts b/libs/datasource/jwt/src/yjs/types.ts similarity index 97% rename from libs/datasource/jwt/src/adapter/index.ts rename to libs/datasource/jwt/src/yjs/types.ts index 2d7176e6be..b23c685e5c 100644 --- a/libs/datasource/jwt/src/adapter/index.ts +++ b/libs/datasource/jwt/src/yjs/types.ts @@ -139,8 +139,8 @@ interface AsyncDatabaseAdapter { reload(): void; createBlock( options: Pick, 'type' | 'flavor'> & { - binary?: ArrayBuffer; - uuid?: string; + binary: ArrayBuffer | undefined; + uuid: string | undefined; } ): Promise>; getBlock(id: string): Promise | undefined>; @@ -184,8 +184,6 @@ export const getDataExporter = () => { return { importData, exportData, hasExporter, installExporter }; }; -export { YjsAdapter } from './yjs'; -export type { YjsContentOperation, YjsInitOptions } from './yjs'; export type { AsyncDatabaseAdapter, BlockPosition, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b3c62870c..2e59cde1a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,7 +184,7 @@ importers: yjs: ^13.5.41 dependencies: authing-js-sdk: 4.23.35 - firebase-admin: 11.0.1_@firebase+app-types@0.7.0 + firebase-admin: 11.0.1 lib0: 0.2.52 lru-cache: 7.13.2 nanoid: 4.0.0 @@ -220,17 +220,21 @@ importers: '@emotion/react': ^11.10.0 '@emotion/styled': ^11.10.0 '@mui/joy': ^5.0.0-alpha.39 + i18next: ^21.9.1 image-minimizer-webpack-plugin: ^3.2.3 imagemin: ^8.0.1 imagemin-optipng: ^8.0.0 lozad: ^1.16.0 mini-css-extract-plugin: ^2.6.1 + react-i18next: ^11.18.4 webpack: ^5.74.0 dependencies: '@emotion/react': 11.10.0 '@emotion/styled': 11.10.0_@emotion+react@11.10.0 '@mui/joy': 5.0.0-alpha.39_72v32ofbtgpmxm7mhvtx474vfu + i18next: 21.9.1 lozad: 1.16.0 + react-i18next: 11.18.4_i18next@21.9.1 devDependencies: image-minimizer-webpack-plugin: 3.2.3_5emixpjl54fjyhdvj76qjbw4py imagemin: 8.0.1 @@ -284,7 +288,7 @@ importers: '@tldraw/intersect': ^1.7.1 '@tldraw/vec': ^1.7.0 dependencies: - '@tldraw/core': 1.14.1_mobx@6.6.1 + '@tldraw/core': 1.14.1 '@tldraw/intersect': 1.7.1 '@tldraw/vec': 1.7.1 @@ -295,7 +299,7 @@ importers: '@tldraw/vec': ^1.7.0 perfect-freehand: ^1.0.16 dependencies: - '@tldraw/core': 1.14.1_mobx@6.6.1 + '@tldraw/core': 1.14.1 '@tldraw/intersect': 1.7.1 '@tldraw/vec': 1.7.1 perfect-freehand: 1.1.0 @@ -308,7 +312,7 @@ importers: idb-keyval: ^6.2.0 zustand: ^3.7.2 dependencies: - '@tldraw/core': 1.14.1_mobx@6.6.1 + '@tldraw/core': 1.14.1 '@tldraw/intersect': 1.7.1 '@tldraw/vec': 1.7.1 idb-keyval: 6.2.0 @@ -320,7 +324,7 @@ importers: '@tldraw/intersect': ^1.7.1 '@tldraw/vec': ^1.7.0 dependencies: - '@tldraw/core': 1.14.1_mobx@6.6.1 + '@tldraw/core': 1.14.1 '@tldraw/intersect': 1.7.1 '@tldraw/vec': 1.7.1 @@ -330,7 +334,7 @@ importers: '@tldraw/intersect': ^1.7.1 '@tldraw/vec': ^1.7.0 dependencies: - '@tldraw/core': 1.14.1_mobx@6.6.1 + '@tldraw/core': 1.14.1 '@tldraw/intersect': 1.7.1 '@tldraw/vec': 1.7.1 @@ -455,11 +459,9 @@ importers: eventemitter3: ^4.0.7 hotkeys-js: ^3.9.4 html-escaper: ^3.0.3 - i: ^0.3.7 lru-cache: ^7.10.1 marked: ^4.0.19 nanoid: ^4.0.0 - npm: ^8.18.0 slate: ^0.81.0 style9: ^0.13.3 dependencies: @@ -468,11 +470,9 @@ importers: eventemitter3: 4.0.7 hotkeys-js: 3.9.4 html-escaper: 3.0.3 - i: 0.3.7 lru-cache: 7.12.0 marked: 4.0.19 nanoid: 4.0.0 - npm: 8.18.0 slate: 0.81.1 style9: 0.13.3 @@ -570,6 +570,9 @@ importers: dependencies: ffc-js-client-side-sdk: 1.1.5 + libs/datasource/jwst/pkg: + specifiers: {} + libs/datasource/jwt: specifiers: '@types/debug': ^4.1.7 @@ -587,7 +590,7 @@ importers: idb-keyval: ^6.2.0 immer: ^9.0.15 lib0: ^0.2.52 - lru-cache: ^7.13.2 + lru-cache: ^7.14.0 nanoid: ^4.0.0 sha3: ^2.1.4 sift: ^16.0.0 @@ -618,7 +621,7 @@ importers: file-selector: 0.6.0 flexsearch: 0.7.21 lib0: 0.2.52 - lru-cache: 7.13.2 + lru-cache: 7.14.0 ts-debounce: 4.0.0 libs/datasource/jwt-rpc: @@ -3184,6 +3187,15 @@ packages: - utf-8-validate dev: true + /@firebase/auth-interop-types/0.1.6_@firebase+util@1.6.3: + resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==} + peerDependencies: + '@firebase/app-types': 0.x + '@firebase/util': 1.x + dependencies: + '@firebase/util': 1.6.3 + dev: false + /@firebase/auth-interop-types/0.1.6_pbfwexsq7uf6mrzcwnikj3g37m: resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==} peerDependencies: @@ -3192,6 +3204,7 @@ packages: dependencies: '@firebase/app-types': 0.7.0 '@firebase/util': 1.6.3 + dev: true /@firebase/auth-types/0.11.0_pbfwexsq7uf6mrzcwnikj3g37m: resolution: {integrity: sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==} @@ -3227,6 +3240,19 @@ packages: '@firebase/util': 1.6.3 tslib: 2.4.0 + /@firebase/database-compat/0.2.4: + resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==} + dependencies: + '@firebase/component': 0.5.17 + '@firebase/database': 0.13.4 + '@firebase/database-types': 0.9.12 + '@firebase/logger': 0.3.3 + '@firebase/util': 1.6.3 + tslib: 2.4.0 + transitivePeerDependencies: + - '@firebase/app-types' + dev: false + /@firebase/database-compat/0.2.4_@firebase+app-types@0.7.0: resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==} dependencies: @@ -3238,6 +3264,7 @@ packages: tslib: 2.4.0 transitivePeerDependencies: - '@firebase/app-types' + dev: true /@firebase/database-types/0.9.10: resolution: {integrity: sha512-2ji6nXRRsY+7hgU6zRhUtK0RmSjVWM71taI7Flgaw+BnopCo/lDF5HSwxp8z7LtiHlvQqeRA3Ozqx5VhlAbiKg==} @@ -3252,6 +3279,19 @@ packages: '@firebase/app-types': 0.7.0 '@firebase/util': 1.6.3 + /@firebase/database/0.13.4: + resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==} + dependencies: + '@firebase/auth-interop-types': 0.1.6_@firebase+util@1.6.3 + '@firebase/component': 0.5.17 + '@firebase/logger': 0.3.3 + '@firebase/util': 1.6.3 + faye-websocket: 0.11.4 + tslib: 2.4.0 + transitivePeerDependencies: + - '@firebase/app-types' + dev: false + /@firebase/database/0.13.4_@firebase+app-types@0.7.0: resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==} dependencies: @@ -3263,6 +3303,7 @@ packages: tslib: 2.4.0 transitivePeerDependencies: - '@firebase/app-types' + dev: true /@firebase/firestore-compat/0.1.23_53yvy43rwpg2c45kgeszsxtrca: resolution: {integrity: sha512-QfcuyMAavp//fQnjSfCEpnbWi7spIdKaXys1kOLu7395fLr+U6ykmto1HUMCSz8Yus9cEr/03Ujdi2SUl2GUAA==} @@ -6204,6 +6245,28 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: true + /@tldraw/core/1.14.1: + resolution: {integrity: sha512-FAiX/TD/tl5eMvTk0IHhFUSmldCe/UTTOnLL5aWQHqhicF/iTP5m3H12L6kyjN60yfssN8U9g4E/7y70TY7JxA==} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + dependencies: + '@tldraw/intersect': 1.7.1 + '@tldraw/vec': 1.7.1 + '@use-gesture/react': 10.2.16 + mobx-react-lite: 3.4.0 + perfect-freehand: 1.1.0 + resize-observer-polyfill: 1.5.1 + transitivePeerDependencies: + - mobx + - react-native + dev: false + /@tldraw/core/1.14.1_mobx@6.6.1: resolution: {integrity: sha512-FAiX/TD/tl5eMvTk0IHhFUSmldCe/UTTOnLL5aWQHqhicF/iTP5m3H12L6kyjN60yfssN8U9g4E/7y70TY7JxA==} peerDependencies: @@ -9571,20 +9634,30 @@ packages: '@typescript-eslint/parser': optional: true dependencies: + '@babel/runtime': 7.18.6 '@typescript-eslint/parser': 5.30.5_4x5o4skxv6sl53vpwefgt23khm + aria-query: 4.2.2 array-includes: 3.1.5 array.prototype.flat: 1.3.0 + ast-types-flow: 0.0.7 + axe-core: 4.4.2 + axobject-query: 2.2.0 + damerau-levenshtein: 1.0.8 debug: 2.6.9 doctrine: 2.1.0 + emoji-regex: 9.2.2 eslint: 8.19.0 eslint-import-resolver-node: 0.3.6 eslint-module-utils: 2.7.3_ea34krk32wbcqzxapvwr7rsjs4 has: 1.0.3 is-core-module: 2.9.0 is-glob: 4.0.3 + jsx-ast-utils: 3.3.1 + language-tags: 1.0.5 minimatch: 3.1.2 object.values: 1.1.5 resolve: 1.22.1 + semver: 6.3.0 tsconfig-paths: 3.14.1 transitivePeerDependencies: - eslint-import-resolver-typescript @@ -10304,12 +10377,12 @@ packages: semver-regex: 2.0.0 dev: true - /firebase-admin/11.0.1_@firebase+app-types@0.7.0: + /firebase-admin/11.0.1: resolution: {integrity: sha512-rL3wlZbi2Kb/KJgcmj1YHlD4ZhfmhfgRO2YJialxAllm0tj1IQea878hHuBLGmv4DpbW9t9nLvX9kddNR2Y65Q==} engines: {node: '>=14'} dependencies: '@fastify/busboy': 1.1.0 - '@firebase/database-compat': 0.2.4_@firebase+app-types@0.7.0 + '@firebase/database-compat': 0.2.4 '@firebase/database-types': 0.9.10 '@types/node': 18.0.1 jsonwebtoken: 8.5.1 @@ -11133,6 +11206,12 @@ packages: terser: 5.14.1 dev: true + /html-parse-stringify/3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + /html-webpack-plugin/5.5.0_webpack@5.74.0: resolution: {integrity: sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw==} engines: {node: '>=10.13.0'} @@ -11318,9 +11397,10 @@ packages: hasBin: true dev: true - /i/0.3.7: - resolution: {integrity: sha512-FYz4wlXgkQwIPqhzC5TdNMLSE5+GS1IIDJZY/1ZiEPCT2S3COUVZeT5OW4BmW4r5LHLQuOosSwsvnroG9GR59Q==} - engines: {node: '>=0.4'} + /i18next/21.9.1: + resolution: {integrity: sha512-ITbDrAjbRR73spZAiu6+ex5WNlHRr1mY+acDi2ioTHuUiviJqSz269Le1xHAf0QaQ6GgIHResUhQNcxGwa/PhA==} + dependencies: + '@babel/runtime': 7.18.6 dev: false /iconv-lite/0.4.24: @@ -13542,6 +13622,12 @@ packages: /lru-cache/7.13.2: resolution: {integrity: sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==} engines: {node: '>=12'} + dev: false + + /lru-cache/7.14.0: + resolution: {integrity: sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==} + engines: {node: '>=12'} + dev: true /lru-memoizer/2.1.4: resolution: {integrity: sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==} @@ -13807,6 +13893,22 @@ packages: hasBin: true dev: true + /mobx-react-lite/3.4.0: + resolution: {integrity: sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==} + peerDependencies: + mobx: ^6.1.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + react-native: + optional: true + dev: false + /mobx-react-lite/3.4.0_mobx@6.6.1: resolution: {integrity: sha512-bRuZp3C0itgLKHu/VNxi66DN/XVkQG7xtoBVWxpvC5FhAqbOCP21+nPhULjnzEqd7xBMybp6KwytdUpZKEgpIQ==} peerDependencies: @@ -14021,84 +14123,6 @@ packages: path-key: 4.0.0 dev: true - /npm/8.18.0: - resolution: {integrity: sha512-G07/yKvNUwhwxYhk8BxcuDPB/4s+y755i6CnH3lf9LQBHP5siUx66WbuNGWEnN3xaBER4+IR3OWApKX7eBO5Dw==} - engines: {node: ^12.13.0 || ^14.15.0 || >=16} - hasBin: true - dev: false - bundledDependencies: - - '@isaacs/string-locale-compare' - - '@npmcli/arborist' - - '@npmcli/ci-detect' - - '@npmcli/config' - - '@npmcli/fs' - - '@npmcli/map-workspaces' - - '@npmcli/package-json' - - '@npmcli/run-script' - - abbrev - - archy - - cacache - - chalk - - chownr - - cli-columns - - cli-table3 - - columnify - - fastest-levenshtein - - glob - - graceful-fs - - hosted-git-info - - ini - - init-package-json - - is-cidr - - json-parse-even-better-errors - - libnpmaccess - - libnpmdiff - - libnpmexec - - libnpmfund - - libnpmhook - - libnpmorg - - libnpmpack - - libnpmpublish - - libnpmsearch - - libnpmteam - - libnpmversion - - make-fetch-happen - - minipass - - minipass-pipeline - - mkdirp - - mkdirp-infer-owner - - ms - - node-gyp - - nopt - - npm-audit-report - - npm-install-checks - - npm-package-arg - - npm-pick-manifest - - npm-profile - - npm-registry-fetch - - npm-user-validate - - npmlog - - opener - - p-map - - pacote - - parse-conflict-json - - proc-log - - qrcode-terminal - - read - - read-package-json - - read-package-json-fast - - readdir-scoped-modules - - rimraf - - semver - - ssri - - tar - - text-table - - tiny-relative-date - - treeverse - - validate-npm-package-name - - which - - write-file-atomic - /npmlog/4.1.2: resolution: {integrity: sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==} dependencies: @@ -15250,7 +15274,7 @@ packages: dev: true /proxy-from-env/1.0.0: - resolution: {integrity: sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=} + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} dev: true /prr/1.0.1: @@ -15446,6 +15470,26 @@ packages: hotkeys-js: 3.9.3 dev: false + /react-i18next/11.18.4_i18next@21.9.1: + resolution: {integrity: sha512-gK/AylAQC5DvCD5YLNCHW4PNzpCfrWIyVAXbSMl+/5QXzlDP8VdBoqE2s2niGHB+zIXwBV9hRXbDrVuupbgHcg==} + peerDependencies: + i18next: '>= 19.0.0' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.18.6 + html-parse-stringify: 3.0.1 + i18next: 21.9.1 + dev: false + /react-is/16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -17693,7 +17737,7 @@ packages: dev: true /verror/1.10.0: - resolution: {integrity: sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=} + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} engines: {'0': node >=0.6.0} dependencies: assert-plus: 1.0.0 @@ -17701,6 +17745,11 @@ packages: extsprintf: 1.3.0 dev: true + /void-elements/3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} dependencies: diff --git a/tools/executors/svgOptimize/executor.json b/tools/executors/svgOptimize/executor.json deleted file mode 100644 index 1c19a2f9c5..0000000000 --- a/tools/executors/svgOptimize/executor.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "executors": { - "svgOptimize": { - "implementation": "./svgo.js", - "schema": "./schema.json", - "description": "Run `svgo` (to optimize svg)." - } - } -} diff --git a/tools/executors/svgOptimize/package.json b/tools/executors/svgOptimize/package.json deleted file mode 100644 index 721ff1df5d..0000000000 --- a/tools/executors/svgOptimize/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "executors": "./executor.json" -} diff --git a/tools/executors/svgOptimize/schema.json b/tools/executors/svgOptimize/schema.json deleted file mode 100644 index dc2fdc029b..0000000000 --- a/tools/executors/svgOptimize/schema.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "$schema": "http://json-schema.org/schema", - "type": "object", - "cli": "nx", - "properties": { - "svgOptimize": { - "type": "string", - "description": "optimize svg" - } - } -} diff --git a/tools/executors/svgOptimize/svgo.js b/tools/executors/svgOptimize/svgo.js deleted file mode 100644 index a0f6c21539..0000000000 --- a/tools/executors/svgOptimize/svgo.js +++ /dev/null @@ -1,133 +0,0 @@ -const path = require('path'); -const svgo = require('svgo'); -const { readdir, readFile, writeFile, exists } = require('fs/promises'); -const { pascalCase, paramCase } = require('change-case'); -const svgr = require('@svgr/core'); - -async function optimizeSvg(folder) { - try { - const icons = await readdir(folder); - const generateIcons = icons - .filter(n => n.endsWith('.svg')) - .map(async icon => { - let originSvg; - try { - originSvg = await readFile(path.resolve(folder, icon)); - } catch (err) { - console.error(err); - } - let optimizedSvg; - try { - const data = optimize(originSvg); - optimizedSvg = data.data; - } catch (err) { - console.error(err); - } - - const JSXContent = await getJSXContent( - pascalCase(icon), - optimizedSvg - ); - - const iconName = path.basename(icon, '.svg'); - await writeFile( - path.resolve(folder, `${iconName}.tsx`), - JSXContent, - { encoding: 'utf8', flag: '' } - ); - - console.log('Generated:', iconName); - }); - - await Promise.allSettled([ - ...generateIcons, - generateImportEntry(icons, folder), - ]); - } catch (err) { - console.error(err); - } -} - -function optimize(input) { - return svgo.optimize(input, { - plugins: [ - 'preset-default', - 'prefixIds', - { - name: 'sortAttrs', - params: { - xmlnsOrder: 'alphabetical', - }, - }, - ], - }); -} - -/** - * get icon component template - * - * @param {string} name - */ -async function getJSXContent(name, svgCode) { - let svgrContent = ''; - try { - svgrContent = await svgr.transform( - svgCode, - { - icon: true, - typescript: true, - }, - { componentName: `${name}Icon1` } - ); - } catch (err) { - console.error(err); - } - let matcher = svgrContent.match(/]+)>([\s\S]*?)<\/svg>/); - return ` -import { SvgIcon, SvgIconProps } from '@mui/material'; -export const ${name}Icon = (props: SvgIconProps) => ( - - ${matcher[2]} - -); -`; -} - -async function generateImportEntry(iconNodes, folder) { - const fileWithImportsPath = path.resolve(folder, 'index.ts'); - - const importsContent = iconNodes - .map(iconNode => { - const iconName = paramCase(iconNode.name); - if (!iconName) { - return `// Error: ${iconNode.name}`; - } - - return `export * from './${iconName}/${iconName}';`; - }) - .join('\n'); - - await fs.writeFile( - fileWithImportsPath, - `export const timestamp = ${Date.now()};\n${importsContent}`, - { encoding: 'utf8' } - ); -} - -/** - * @param {*} options - * @param {array} options.assets - * @param {string} options.assets.folder - * @param {*} context - * @returns - */ -exports['default'] = async function svgo(options, context) { - const libRoot = context.workspace.projects[context.projectName].root; - await Promise.allSettled( - (options.assets || []).map(async (asset, index) => { - await optimizeSvg(path.resolve(libRoot, asset.folder)); - }) - ); - - return { success: true }; -};