diff --git a/.github/actions/flutter_build/action.yml b/.github/actions/flutter_build/action.yml index cb7b325e82..81b2845949 100644 --- a/.github/actions/flutter_build/action.yml +++ b/.github/actions/flutter_build/action.yml @@ -37,13 +37,6 @@ runs: override: true profile: minimal - - name: Export pub environment variables and add to PATH - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV - fi - shell: bash - - name: Install flutter id: flutter uses: subosito/flutter-action@v2 diff --git a/.github/workflows/android_ci.yaml b/.github/workflows/android_ci.yaml.bak similarity index 100% rename from .github/workflows/android_ci.yaml rename to .github/workflows/android_ci.yaml.bak diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml index 5755fb7276..2c2143c1fe 100644 --- a/.github/workflows/docker_ci.yml +++ b/.github/workflows/docker_ci.yml @@ -7,19 +7,13 @@ on: - release/* paths: - frontend/** - pull_request: branches: - main - release/* paths: - frontend/** - types: - - opened - - synchronize - - reopened - - unlocked - - ready_for_review + types: [opened, synchronize, reopened, unlocked, ready_for_review] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -33,6 +27,15 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 + - name: Set up Docker Compose + run: | + docker-compose --version || { + echo "Docker Compose not found, installing..." + sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + docker-compose --version + } + - name: Build the app shell: bash run: | @@ -45,4 +48,4 @@ jobs: else \ echo "$line"; \ fi; \ - done \ + done diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index a5cb5f8f96..ffa1d309a5 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -7,6 +7,7 @@ on: - "release/*" paths: - ".github/workflows/flutter_ci.yaml" + - ".github/actions/flutter_build/**" - "frontend/rust-lib/**" - "frontend/appflowy_flutter/**" - "frontend/resources/**" @@ -17,6 +18,7 @@ on: - "release/*" paths: - ".github/workflows/flutter_ci.yaml" + - ".github/actions/flutter_build/**" - "frontend/rust-lib/**" - "frontend/appflowy_flutter/**" - "frontend/resources/**" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 237f81d125..f8fc9cac64 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,13 +54,6 @@ jobs: - name: Checkout source code uses: actions/checkout@v4 - - name: Export pub environment variable on Windows - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV - fi - shell: bash - - name: Install flutter uses: subosito/flutter-action@v2 with: @@ -336,6 +329,7 @@ jobs: LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage + LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz strategy: fail-fast: false @@ -412,7 +406,8 @@ jobs: continue-on-error: true run: | sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }} - cp -r ${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }} + cd .. + cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }} - name: Upload Asset id: upload-release-asset @@ -422,7 +417,7 @@ jobs: with: upload_url: ${{ needs.create-release.outputs.upload_url }} asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }} - asset_name: ${{ env.LINUX_ZIP_NAME }} + asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }} asset_content_type: application/octet-stream - name: Upload Debian package diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 4b30b0043a..7251939de6 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -93,7 +93,8 @@ jobs: af_cloud_test_base_url: http://localhost af_cloud_test_ws_url: ws://localhost/ws/v1 af_cloud_test_gotrue_url: http://localhost/gotrue - run: cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture + run: | + DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture - name: rustfmt rust-lib run: cargo fmt --all -- --check diff --git a/.github/workflows/tauri2_ci.yaml b/.github/workflows/tauri2_ci.yaml new file mode 100644 index 0000000000..06ce87d2fe --- /dev/null +++ b/.github/workflows/tauri2_ci.yaml @@ -0,0 +1,113 @@ +name: Tauri2-CI +on: + pull_request: + paths: + - ".github/workflows/tauri2_ci.yaml" + - "frontend/rust-lib/**" + - "frontend/appflowy_web_app/**" + - "frontend/resources/**" + +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" + RUST_TOOLCHAIN: "1.75" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + tauri-build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-20.04 ] + + runs-on: ${{ matrix.platform }} + + env: + CI: true + steps: + - uses: actions/checkout@v4 + + - name: Maximize build space (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Install Rust toolchain + id: rust_toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ env.RUST_TOOLCHAIN }} + override: true + profile: minimal + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: "./frontend/appflowy_web_app/src-tauri -> target" + + - name: Node_modules cache + uses: actions/cache@v2 + with: + path: frontend/appflowy_web_app/node_modules + key: node-modules-${{ runner.os }} + + - name: install dependencies (windows only) + if: matrix.platform == 'windows-latest' + working-directory: frontend + run: | + cargo install --force duckscript_cli + vcpkg integrate install + + - name: install dependencies (ubuntu only) + if: matrix.platform == 'ubuntu-20.04' + working-directory: frontend + run: | + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + + - name: install cargo-make + working-directory: frontend + run: | + cargo install --force cargo-make + cargo make appflowy-tauri-deps-tools + + - name: install frontend dependencies + working-directory: frontend/appflowy_web_app + run: | + mkdir dist + pnpm install + cd src-tauri && cargo build + + - name: test and lint + working-directory: frontend/appflowy_web_app + run: | + pnpm run lint:tauri + + - uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tauriScript: pnpm tauri + projectPath: frontend/appflowy_web_app + args: "--debug" \ No newline at end of file diff --git a/.github/workflows/tauri_ci.yaml b/.github/workflows/tauri_ci.yaml index 8d99091aab..462bebb8dd 100644 --- a/.github/workflows/tauri_ci.yaml +++ b/.github/workflows/tauri_ci.yaml @@ -22,7 +22,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ubuntu-20.04] runs-on: ${{ matrix.platform }} @@ -32,7 +32,7 @@ jobs: - uses: actions/checkout@v4 - name: Maximize build space (ubuntu only) - if: matrix.platform == 'ubuntu-latest' + if: matrix.platform == 'ubuntu-20.04' run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /opt/ghc @@ -80,7 +80,7 @@ jobs: vcpkg integrate install - name: install dependencies (ubuntu only) - if: matrix.platform == 'ubuntu-latest' + if: matrix.platform == 'ubuntu-20.04' working-directory: frontend run: | sudo apt-get update @@ -110,4 +110,5 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tauriScript: pnpm tauri - projectPath: frontend/appflowy_tauri \ No newline at end of file + projectPath: frontend/appflowy_tauri + args: "--debug" \ No newline at end of file diff --git a/.github/workflows/tauri_release.yml b/.github/workflows/tauri_release.yml index e031e65ccd..2e4be46dbe 100644 --- a/.github/workflows/tauri_release.yml +++ b/.github/workflows/tauri_release.yml @@ -31,7 +31,7 @@ jobs: - platform: macos-latest args: "--target x86_64-apple-darwin" target: "macos-x86_64" - - platform: ubuntu-latest + - platform: ubuntu-20.04 args: "--target x86_64-unknown-linux-gnu" target: "linux-x86_64" @@ -46,7 +46,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - name: Maximize build space (ubuntu only) - if: matrix.settings.platform == 'ubuntu-latest' + if: matrix.settings.platform == 'ubuntu-20.04' run: | sudo rm -rf /usr/share/dotnet sudo rm -rf /opt/ghc @@ -88,7 +88,7 @@ jobs: vcpkg integrate install - name: install dependencies (ubuntu only) - if: matrix.settings.platform == 'ubuntu-latest' + if: matrix.settings.platform == 'ubuntu-20.04' working-directory: frontend run: | sudo apt-get update @@ -140,14 +140,14 @@ jobs: - name: Upload Deb package(ubuntu only) uses: actions/upload-artifact@v4 - if: matrix.settings.platform == 'ubuntu-latest' + if: matrix.settings.platform == 'ubuntu-20.04' with: name: ${{ env.PACKAGE_PREFIX }}.deb path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb - name: Upload AppImage package(ubuntu only) uses: actions/upload-artifact@v4 - if: matrix.settings.platform == 'ubuntu-latest' + if: matrix.settings.platform == 'ubuntu-20.04' with: name: ${{ env.PACKAGE_PREFIX }}.AppImage path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage diff --git a/.github/workflows/web2_ci.yaml b/.github/workflows/web2_ci.yaml new file mode 100644 index 0000000000..31d6f5d8b3 --- /dev/null +++ b/.github/workflows/web2_ci.yaml @@ -0,0 +1,61 @@ +name: Web2-CI +on: + pull_request: + paths: + - ".github/workflows/web2_ci.yaml" + - "frontend/appflowy_web_app/**" + - "frontend/resources/**" +env: + NODE_VERSION: "18.16.0" + PNPM_VERSION: "8.5.0" +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true +jobs: + web-build: + if: github.event.pull_request.draft != true + strategy: + fail-fast: false + matrix: + platform: [ ubuntu-latest ] + + runs-on: ${{ matrix.platform }} + + steps: + - uses: actions/checkout@v4 + - name: Maximize build space (ubuntu only) + if: matrix.platform == 'ubuntu-latest' + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf "/usr/local/share/boost" + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + sudo docker image prune --all --force + sudo rm -rf /opt/hostedtoolcache/codeQL + sudo rm -rf ${GITHUB_WORKSPACE}/.git + sudo rm -rf $ANDROID_HOME/ndk + - name: setup node + uses: actions/setup-node@v3 + with: + node-version: ${{ env.NODE_VERSION }} + - name: setup pnpm + uses: pnpm/action-setup@v2 + with: + version: ${{ env.PNPM_VERSION }} + - name: Node_modules cache + uses: actions/cache@v2 + with: + path: frontend/appflowy_web_app/node_modules + key: node-modules-${{ runner.os }} + - name: install frontend dependencies + working-directory: frontend/appflowy_web_app + run: | + pnpm install + - name: test and lint + working-directory: frontend/appflowy_web_app + run: | + pnpm run lint + - name: build + working-directory: frontend/appflowy_web_app + run: | + pnpm run build \ No newline at end of file diff --git a/.github/workflows/web_ci.yaml b/.github/workflows/web_ci.yaml index 079a176772..2e92b33226 100644 --- a/.github/workflows/web_ci.yaml +++ b/.github/workflows/web_ci.yaml @@ -1,13 +1,12 @@ name: WEB-CI on: - pull_request: - branches: - - "main" - paths: - - ".github/workflows/web_ci.yaml" - - "frontend/rust-lib/**" - - "frontend/appflowy_web/**" + workflow_dispatch: + inputs: + build: + description: 'Build the web app' + required: true + default: 'true' env: CARGO_TERM_COLOR: always @@ -22,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - platform: [ubuntu-latest] + platform: [ ubuntu-latest ] runs-on: ${{ matrix.platform }} steps: diff --git a/.gitignore b/.gitignore index f1f08e6128..33de28002a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,5 @@ frontend/package frontend/*.deb **/Cargo.toml.bak + +**/.cargo/** \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e59da2ad1..5b040c4607 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,44 @@ # Release Notes +## Version 0.5.4 - 04/08/2024 +### New Features +- TBD +### Bug Fixes +- TBD + +## Version 0.5.3 - 03/21/2024 +### New Features +- Added build support for 32-bit Android devices +- Introduced filters for KanBan boards for enhanced organization +- Introduced the new "Relations" column type in Grids +- Expanded language support with the addition of Greek +- Enhanced toolbar design for Mobile devices +- Introduced a command palette feature with initial support for page search +### Bug Fixes +- Rectified the issue of incomplete row data in Grids when adding new rows with active filters +- Enhanced the logic governing the filtering of number and select/multi-select fields for improved accuracy +- Implemented UI refinements on both Desktop and Mobile platforms, enriching the overall user experience of AppFlowy + +## Version 0.5.2 - 03/13/2024 +### Bug Fixes +- Import csv file. + +## Version 0.5.1 - 03/11/2024 +### New Features +- Introduced support for performing generic calculations on databases. +- Implemented functionality for easily duplicating calendar events. +- Added the ability to duplicate fields with cell data, facilitating smoother data management. +- Now supports customizing font styles and colors prior to typing. +- Enhanced the checklist user experience with the integration of keyboard shortcuts. +- Improved the dark mode experience on mobile devices. +### Bug Fixes +- Fixed an issue with some pages failing to sync properly. +- Fixed an issue where links without the http(s) scheme could not be opened, ensuring consistent link functionality. +- Fixed an issue that prevented numbers from being inserted before heading blocks. +- Fixed the inline page reference update mechanism to accurately reflect workspace changes. +- Fixed an issue that made it difficult to resize images in certain cases. +- Enhanced image loading reliability by clearing the image cache when images fail to load. +- Resolved a problem preventing the launching of URLs on some Linux distributions. + ## Version 0.5.0 - 02/26/2024 ### New Features - Added support for scaling text on mobile platforms for better readability. diff --git a/README.md b/README.md index e08d894389..580fa98a48 100644 --- a/README.md +++ b/README.md @@ -31,15 +31,15 @@ You are in charge of your data and customizations. ## User Installation -* [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages) -* [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker) -* [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source) +- [Windows/Mac/Linux](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/mac-windows-linux-packages) +- [Docker](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/installing-with-docker) +- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source) ## Built With -* [Flutter](https://flutter.dev/) +- [Flutter](https://flutter.dev/) -* [Rust](https://www.rust-lang.org/) +- [Rust](https://www.rust-lang.org/) ## Stay Up-to-Date @@ -51,8 +51,8 @@ Please view the [documentation](https://docs.appflowy.io/docs/documentation/appf ## Roadmap -* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap) -* [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) +- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap) +- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12) If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+)
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+) @@ -63,19 +63,17 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab ## Contributing -Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details. +Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details. If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt! Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter. - ## Translations 🌎🗺 [![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge) To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations. - ## Join the community to build AppFlowy together @@ -92,14 +90,14 @@ When a customer's evolving core needs are not satisfied, they either switch to a All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well. -* To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience. -* To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability. +- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience. +- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability. We decided to achieve this mission by upholding the three most fundamental values: -* Data privacy first -* Reliable native experience -* Community-driven extensibility +- Data privacy first +- Reliable native experience +- Community-driven extensibility We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks. @@ -111,6 +109,6 @@ Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppF Special thanks to these amazing projects which help power AppFlowy.IO: -* [flutter-quill](https://github.com/singerdmx/flutter-quill) -* [cargo-make](https://github.com/sagiegurari/cargo-make) -* [contrib.rocks](https://contrib.rocks) +- [flutter-quill](https://github.com/singerdmx/flutter-quill) +- [cargo-make](https://github.com/sagiegurari/cargo-make) +- [contrib.rocks](https://contrib.rocks) diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json index 1bc6978a44..72d398e0fa 100644 --- a/frontend/.vscode/launch.json +++ b/frontend/.vscode/launch.json @@ -115,9 +115,12 @@ }, { "name": "AF-desktop: Debug Rust", - "request": "attach", "type": "lldb", + "request": "attach", "pid": "${command:pickMyProcess}" + // To launch the application directly, use the following configuration: + // "request": "launch", + // "program": "[YOUR_APPLICATION_PATH]", }, { // https://tauri.app/v1/guides/debugging/vs-code diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json index 4253215e27..d940eef0a8 100644 --- a/frontend/.vscode/tasks.json +++ b/frontend/.vscode/tasks.json @@ -257,7 +257,7 @@ "label": "AF: Tauri UI Dev", "type": "shell", "isBackground": true, - "command": "pnpm run sync:i18n && pnpm run dev", + "command": "pnpm sync:i18n && pnpm run dev", "options": { "cwd": "${workspaceFolder}/appflowy_tauri" } diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 3c8164c477..7157639104 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true CARGO_MAKE_CRATE_FS_NAME = "dart_ffi" CARGO_MAKE_CRATE_NAME = "dart-ffi" LIB_NAME = "dart_ffi" -APPFLOWY_VERSION = "0.5.1" +APPFLOWY_VERSION = "0.5.4" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" @@ -50,7 +50,8 @@ APP_ENVIRONMENT = "local" FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend" TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend" WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend" -WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm" +WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm" +TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend" # Test default config TEST_CRATE_TYPE = "cdylib" TEST_LIB_EXT = "dylib" @@ -226,9 +227,8 @@ script = [''' echo FEATURES: ${FLUTTER_DESKTOP_FEATURES} echo PRODUCT_EXT: ${PRODUCT_EXT} echo APP_ENVIRONMENT: ${APP_ENVIRONMENT} - echo ${platforms} - echo ${BUILD_ARCHS} - echo ${BUILD_VERSION} + echo BUILD_ARCHS: ${BUILD_ARCHS} + echo BUILD_VERSION: ${BUILD_VERSION} '''] script_runner = "@shell" diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle index be3e3dc25c..d14c7016c1 100644 --- a/frontend/appflowy_flutter/android/app/build.gradle +++ b/frontend/appflowy_flutter/android/app/build.gradle @@ -52,7 +52,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "io.appflowy.appflowy" - minSdkVersion 29 + minSdkVersion 23 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt b/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt index 38b0aa5ca7..455c5081b6 100644 --- a/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt +++ b/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt @@ -11,6 +11,12 @@ file(COPY DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/arm64-v8a ) +# armeabi-v7a +file(COPY + ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so + DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/armeabi-v7a +) + # x86_64 file(COPY ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/x86_64/libc++_shared.so diff --git a/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md b/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md new file mode 100644 index 0000000000..5998220774 --- /dev/null +++ b/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md @@ -0,0 +1,11 @@ +# AppFlowy Test Markdown import with table + +# Table + +| S.No. | Column 2 | +| --- | --- | +| 1. | row 1 | +| 2. | row 2 | +| 3. | row 3 | +| 4. | row 4 | +| 5. | row 5 | \ No newline at end of file diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart index 6f58ba6354..1e555b1667 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart @@ -1,6 +1,7 @@ // ignore_for_file: unused_import import 'dart:io'; +import 'dart:ui'; import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; @@ -14,8 +15,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/uuid.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:path/path.dart' as p; import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + import '../shared/dir.dart'; import '../shared/mock/mock_file_picker.dart'; import '../shared/util.dart'; diff --git a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart index 2435d15c3d..c66cdd5cc1 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart @@ -1,9 +1,12 @@ import 'anon_user_continue_test.dart' as anon_user_continue_test; import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test; -import 'collaborative_workspace_test.dart' as collaboration_workspace_test; import 'empty_test.dart' as preset_af_cloud_env_test; // import 'document_sync_test.dart' as document_sync_test; import 'user_setting_sync_test.dart' as user_sync_test; +import 'workspace/change_name_and_icon_test.dart' + as change_workspace_name_and_icon_test; +import 'workspace/collaborative_workspace_test.dart' + as collaboration_workspace_test; Future main() async { preset_af_cloud_env_test.main(); @@ -16,5 +19,7 @@ Future main() async { anon_user_continue_test.main(); + // workspace collaboration_workspace_test.main(); + change_workspace_name_and_icon_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart new file mode 100644 index 0000000000..5e0122c5ef --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart @@ -0,0 +1,81 @@ +// ignore_for_file: unused_import + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/feature_flags.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; +import '../../shared/workspace.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + const icon = '😄'; + const name = 'AppFlowy'; + final email = '${uuid()}@appflowy.io'; + + testWidgets('change name and icon', (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, // use the same email to check the next test + ); + + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + var workspaceIcon = tester.widget( + find.byType(WorkspaceIcon), + ); + expect(workspaceIcon.workspace.icon, ''); + + await tester.openWorkspaceMenu(); + await tester.changeWorkspaceIcon(icon); + await tester.changeWorkspaceName(name); + + workspaceIcon = tester.widget( + find.byType(WorkspaceIcon), + ); + expect(workspaceIcon.workspace.icon, icon); + expect(find.findTextInFlowyText(name), findsOneWidget); + }); + + testWidgets('verify the result again after relaunching', (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.appflowyCloudSelfHost, + email: email, // use the same email to check the next test + ); + + await tester.tapGoogleLoginInButton(); + await tester.expectToSeeHomePageWithGetStartedPage(); + + // check the result again + final workspaceIcon = tester.widget( + find.byType(WorkspaceIcon), + ); + expect(workspaceIcon.workspace.icon, icon); + expect(workspaceIcon.workspace.name, name); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart similarity index 90% rename from frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart rename to frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart index 56d9f2bb2c..d1c13eeb47 100644 --- a/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart +++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart @@ -23,11 +23,11 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:path/path.dart' as p; -import '../shared/database_test_op.dart'; -import '../shared/dir.dart'; -import '../shared/emoji.dart'; -import '../shared/mock/mock_file_picker.dart'; -import '../shared/util.dart'; +import '../../shared/database_test_op.dart'; +import '../../shared/dir.dart'; +import '../../shared/emoji.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -35,14 +35,14 @@ void main() { final email = '${uuid()}@appflowy.io'; group('collaborative workspace', () { - // only run the test when the feature flag is on - if (!FeatureFlag.collaborativeWorkspace.isOn) { - return; - } - // combine the create and delete workspace test to reduce the time testWidgets('create a new workspace, open it and then delete it', (tester) async { + // only run the test when the feature flag is on + if (!FeatureFlag.collaborativeWorkspace.isOn) { + return; + } + await tester.initializeAppFlowy( cloudType: AuthenticatorType.appflowyCloudSelfHost, email: email, @@ -68,8 +68,9 @@ void main() { ); // open the newly created workspace - await tester.tapButton(items.last); + await tester.tapButton(items.last, milliseconds: 1000); success = find.text(LocaleKeys.workspace_openSuccess.tr()); + await tester.pumpUntilFound(success); expect(success, findsOneWidget); await tester.pumpUntilNotFound(success); diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart index 30f79e1ac8..60d7ee8423 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; @@ -327,69 +327,70 @@ void main() { ); }); - testWidgets('last modified and created at field type options', - (tester) async { - await tester.initializeAppFlowy(); - await tester.tapGoButton(); + // Disable this test because it fails on CI randomly + // testWidgets('last modified and created at field type options', + // (tester) async { + // await tester.initializeAppFlowy(); + // await tester.tapGoButton(); - await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); - final created = DateTime.now(); + // await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + // final created = DateTime.now(); - // create a created at field - await tester.tapNewPropertyButton(); - await tester.renameField(FieldType.CreatedTime.i18n); - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.CreatedTime); - await tester.dismissFieldEditor(); + // // create a created at field + // await tester.tapNewPropertyButton(); + // await tester.renameField(FieldType.CreatedTime.i18n); + // await tester.tapSwitchFieldTypeButton(); + // await tester.selectFieldType(FieldType.CreatedTime); + // await tester.dismissFieldEditor(); - // create a last modified field - await tester.tapNewPropertyButton(); - await tester.renameField(FieldType.LastEditedTime.i18n); - await tester.tapSwitchFieldTypeButton(); + // // create a last modified field + // await tester.tapNewPropertyButton(); + // await tester.renameField(FieldType.LastEditedTime.i18n); + // await tester.tapSwitchFieldTypeButton(); - // get time just before modifying - final modified = DateTime.now(); + // // get time just before modifying + // final modified = DateTime.now(); - // create a last modified field (cont'd) - await tester.selectFieldType(FieldType.LastEditedTime); - await tester.dismissFieldEditor(); + // // create a last modified field (cont'd) + // await tester.selectFieldType(FieldType.LastEditedTime); + // await tester.dismissFieldEditor(); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.CreatedTime, - content: DateFormat('MMM dd, y HH:mm').format(created), - ); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.LastEditedTime, - content: DateFormat('MMM dd, y HH:mm').format(modified), - ); + // tester.assertCellContent( + // rowIndex: 0, + // fieldType: FieldType.CreatedTime, + // content: DateFormat('MMM dd, y HH:mm').format(created), + // ); + // tester.assertCellContent( + // rowIndex: 0, + // fieldType: FieldType.LastEditedTime, + // content: DateFormat('MMM dd, y HH:mm').format(modified), + // ); - // open field editor and change date & time format - await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n); - await tester.tapEditFieldButton(); - await tester.changeDateFormat(); - await tester.changeTimeFormat(); - await tester.dismissFieldEditor(); + // // open field editor and change date & time format + // await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n); + // await tester.tapEditFieldButton(); + // await tester.changeDateFormat(); + // await tester.changeTimeFormat(); + // await tester.dismissFieldEditor(); - // open field editor and change date & time format - await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n); - await tester.tapEditFieldButton(); - await tester.changeDateFormat(); - await tester.changeTimeFormat(); - await tester.dismissFieldEditor(); + // // open field editor and change date & time format + // await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n); + // await tester.tapEditFieldButton(); + // await tester.changeDateFormat(); + // await tester.changeTimeFormat(); + // await tester.dismissFieldEditor(); - // assert format has been changed - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.CreatedTime, - content: DateFormat('dd/MM/y hh:mm a').format(created), - ); - tester.assertCellContent( - rowIndex: 0, - fieldType: FieldType.LastEditedTime, - content: DateFormat('dd/MM/y hh:mm a').format(modified), - ); - }); + // // assert format has been changed + // tester.assertCellContent( + // rowIndex: 0, + // fieldType: FieldType.CreatedTime, + // content: DateFormat('dd/MM/y hh:mm a').format(created), + // ); + // tester.assertCellContent( + // rowIndex: 0, + // fieldType: FieldType.LastEditedTime, + // content: DateFormat('dd/MM/y hh:mm a').format(modified), + // ); + // }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart index 4dfc64ff62..b6db3e1a62 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart @@ -103,8 +103,8 @@ void main() { // select the option 's4' await tester.tapOptionFilterWithName('s4'); - // The row with 's4' or 's5' should be shown. - await tester.assertNumberOfRowsInGridPage(2); + // The row with 's4' should be shown. + await tester.assertNumberOfRowsInGridPage(1); await tester.pumpAndSettle(); }); diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart index 7568a81def..9ff563604d 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart @@ -1,6 +1,6 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,10 +13,10 @@ void main() { group('sidebar expand test', () { bool isExpanded({required FolderCategoryType type}) { - if (type == FolderCategoryType.personal) { + if (type == FolderCategoryType.private) { return find .descendant( - of: find.byType(PersonalFolder), + of: find.byType(PrivateSectionFolder), matching: find.byType(ViewItem), ) .evaluate() @@ -30,19 +30,19 @@ void main() { await tester.tapGoButton(); // first time is expanded - expect(isExpanded(type: FolderCategoryType.personal), true); + expect(isExpanded(type: FolderCategoryType.private), true); // collapse the personal folder await tester.tapButton( - find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()), + find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.personal), false); + expect(isExpanded(type: FolderCategoryType.private), false); // expand the personal folder await tester.tapButton( - find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()), + find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()), ); - expect(isExpanded(type: FolderCategoryType.personal), true); + expect(isExpanded(type: FolderCategoryType.private), true); }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart index aa4f151ab8..9ccd06d526 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart @@ -1,5 +1,5 @@ import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart index bf199036a8..35bcf599ab 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart @@ -1,6 +1,5 @@ import 'package:integration_test/integration_test.dart'; -import 'sidebar_expand_test.dart' as sidebar_expanded_test; import 'sidebar_favorites_test.dart' as sidebar_favorite_test; import 'sidebar_icon_test.dart' as sidebar_icon_test; import 'sidebar_test.dart' as sidebar_test; @@ -10,7 +9,7 @@ void startTesting() { // Sidebar integration tests sidebar_test.main(); - sidebar_expanded_test.main(); + // sidebar_expanded_test.main(); sidebar_favorite_test.main(); sidebar_icon_test.main(); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart index 58d8cc75be..8f356b8406 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -44,5 +45,44 @@ void main() { tester.expectToSeePageName('test1'); tester.expectToSeePageName('test2'); }); + + testWidgets('import markdown file with table', (tester) async { + final context = await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + // expect to see a getting started page + tester.expectToSeePageName(gettingStarted); + + await tester.tapAddViewButton(); + await tester.tapImportButton(); + + const testFileName = 'markdown_with_table.md'; + final paths = []; + final str = await rootBundle.loadString( + 'assets/test/workspaces/markdowns/$testFileName', + ); + final path = p.join(context.applicationDataDirectory, testFileName); + paths.add(path); + File(path).writeAsStringSync(str); + // mock get files + mockPickFilePaths( + paths: paths, + ); + + await tester.tapTextAndMarkdownButton(); + + tester.expectToSeePageName('markdown_with_table'); + + // expect to see all content of markdown file along with table + await tester.openPage('markdown_with_table'); + + final importedPageEditorState = tester.editor.getCurrentEditorState(); + expect(importedPageEditorState.getNodeAtPath([0])!.type, + HeadingBlockKeys.type,); + expect(importedPageEditorState.getNodeAtPath([2])!.type, + HeadingBlockKeys.type,); + expect(importedPageEditorState.getNodeAtPath([4])!.type, + TableBlockKeys.type,); + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart index e2a343d4f1..137b6e5701 100644 --- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart +++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart @@ -5,9 +5,7 @@ import 'package:appflowy/startup/tasks/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; -import 'package:path/path.dart' as p; -import '../../shared/mock/mock_file_picker.dart'; import '../../shared/util.dart'; void main() { @@ -18,80 +16,80 @@ void main() { return; } - testWidgets('switch to B from A, then switch to A again', (tester) async { - const userA = 'UserA'; - const userB = 'UserB'; + // testWidgets('switch to B from A, then switch to A again', (tester) async { + // const userA = 'UserA'; + // const userB = 'UserB'; - final initialPath = p.join(userA, appFlowyDataFolder); - final context = await tester.initializeAppFlowy( - pathExtension: initialPath, - ); - // remove the last extension - final rootPath = context.applicationDataDirectory.replaceFirst( - initialPath, - '', - ); + // final initialPath = p.join(userA, appFlowyDataFolder); + // final context = await tester.initializeAppFlowy( + // pathExtension: initialPath, + // ); + // // remove the last extension + // final rootPath = context.applicationDataDirectory.replaceFirst( + // initialPath, + // '', + // ); - await tester.tapGoButton(); - await tester.expectToSeeHomePageWithGetStartedPage(); + // await tester.tapGoButton(); + // await tester.expectToSeeHomePageWithGetStartedPage(); - // switch to user B - { - // set user name for userA - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - await tester.enterUserName(userA); + // // switch to user B + // { + // // set user name for userA + // await tester.openSettings(); + // await tester.openSettingsPage(SettingsPage.user); + // await tester.enterUserName(userA); - await tester.openSettingsPage(SettingsPage.files); - await tester.pumpAndSettle(); + // await tester.openSettingsPage(SettingsPage.files); + // await tester.pumpAndSettle(); - // mock the file_picker result - await mockGetDirectoryPath( - p.join(rootPath, userB), - ); - await tester.tapCustomLocationButton(); - await tester.pumpAndSettle(); - await tester.expectToSeeHomePageWithGetStartedPage(); + // // mock the file_picker result + // await mockGetDirectoryPath( + // p.join(rootPath, userB), + // ); + // await tester.tapCustomLocationButton(); + // await tester.pumpAndSettle(); + // await tester.expectToSeeHomePageWithGetStartedPage(); - // set user name for userB - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.user); - await tester.enterUserName(userB); - } + // // set user name for userB + // await tester.openSettings(); + // await tester.openSettingsPage(SettingsPage.user); + // await tester.enterUserName(userB); + // } - // switch to the userA - { - await tester.openSettingsPage(SettingsPage.files); - await tester.pumpAndSettle(); + // // switch to the userA + // { + // await tester.openSettingsPage(SettingsPage.files); + // await tester.pumpAndSettle(); - // mock the file_picker result - await mockGetDirectoryPath( - p.join(rootPath, userA), - ); - await tester.tapCustomLocationButton(); + // // mock the file_picker result + // await mockGetDirectoryPath( + // p.join(rootPath, userA), + // ); + // await tester.tapCustomLocationButton(); - await tester.pumpAndSettle(); - await tester.expectToSeeHomePageWithGetStartedPage(); - tester.expectToSeeUserName(userA); - } + // await tester.pumpAndSettle(); + // await tester.expectToSeeHomePageWithGetStartedPage(); + // tester.expectToSeeUserName(userA); + // } - // switch to the userB again - { - await tester.openSettings(); - await tester.openSettingsPage(SettingsPage.files); - await tester.pumpAndSettle(); + // // switch to the userB again + // { + // await tester.openSettings(); + // await tester.openSettingsPage(SettingsPage.files); + // await tester.pumpAndSettle(); - // mock the file_picker result - await mockGetDirectoryPath( - p.join(rootPath, userB), - ); - await tester.tapCustomLocationButton(); + // // mock the file_picker result + // await mockGetDirectoryPath( + // p.join(rootPath, userB), + // ); + // await tester.tapCustomLocationButton(); - await tester.pumpAndSettle(); - await tester.expectToSeeHomePageWithGetStartedPage(); - tester.expectToSeeUserName(userB); - } - }); + // await tester.pumpAndSettle(); + // await tester.expectToSeeHomePageWithGetStartedPage(); + // tester.expectToSeeUserName(userB); + // } + // }); testWidgets('reset to default location', (tester) async { await tester.initializeAppFlowy(); diff --git a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart new file mode 100644 index 0000000000..e75031c955 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart @@ -0,0 +1,49 @@ +// ignore_for_file: unused_import + +import 'dart:io'; + +import 'package:appflowy/env/cloud_env.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart'; +import 'package:appflowy/mobile/presentation/home/home.dart'; +import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; +import 'package:appflowy/plugins/document/document_page.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart'; +import 'package:appflowy/workspace/application/settings/prelude.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/uuid.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path/path.dart' as p; + +import '../../shared/dir.dart'; +import '../../shared/mock/mock_file_picker.dart'; +import '../../shared/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('create new page', () { + testWidgets('create document', (tester) async { + await tester.initializeAppFlowy( + cloudType: AuthenticatorType.local, + ); + + // click the anonymousSignInButton + final anonymousSignInButton = find.byType(SignInAnonymousButton); + expect(anonymousSignInButton, findsOneWidget); + await tester.tapButton(anonymousSignInButton); + + // tap the create page button + final createPageButton = find.byKey(mobileCreateNewPageButtonKey); + await tester.tapButton(createPageButton); + expect(find.byType(MobileDocumentScreen), findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart index f90b151372..9039c843aa 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart @@ -28,9 +28,7 @@ void main() { group('anonymous sign in on mobile', () { testWidgets('anon user and then sign in', (tester) async { - await tester.initializeAppFlowy( - cloudType: AuthenticatorType.local, - ); + await tester.initializeAppFlowy(); // click the anonymousSignInButton final anonymousSignInButton = find.byType(SignInAnonymousButton); diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart index 50eb062c08..00255f7b3e 100644 --- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart @@ -1,5 +1,7 @@ import 'dart:io'; +import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; +import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -29,10 +31,8 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart'; @@ -54,7 +54,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart'; import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart new file mode 100644 index 0000000000..5137944364 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart @@ -0,0 +1,58 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/base/icon/icon_picker.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'base.dart'; + +extension AppFlowyWorkspace on WidgetTester { + /// Open workspace menu + Future openWorkspaceMenu() async { + final workspaceWrapper = find.byType(SidebarSwitchWorkspaceButton); + expect(workspaceWrapper, findsOneWidget); + await tapButton(workspaceWrapper); + final workspaceMenu = find.byType(WorkspacesMenu); + expect(workspaceMenu, findsOneWidget); + } + + /// Open a workspace + Future openWorkspace(String name) async { + final workspace = find.descendant( + of: find.byType(WorkspaceMenuItem), + matching: find.findTextInFlowyText(name), + ); + expect(workspace, findsOneWidget); + await tapButton(workspace); + } + + Future changeWorkspaceName(String name) async { + final moreButton = find.descendant( + of: find.byType(WorkspaceMenuItem), + matching: find.byType(WorkspaceMoreActionList), + ); + expect(moreButton, findsOneWidget); + await tapButton(moreButton); + await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr())); + final input = find.byType(TextFormField); + expect(input, findsOneWidget); + await enterText(input, name); + await tapButton(find.text(LocaleKeys.button_ok.tr())); + } + + Future changeWorkspaceIcon(String icon) async { + final iconButton = find.descendant( + of: find.byType(WorkspaceMenuItem), + matching: find.byType(WorkspaceIcon), + ); + expect(iconButton, findsOneWidget); + await tapButton(iconButton); + final iconPicker = find.byType(FlowyIconPicker); + expect(iconPicker, findsOneWidget); + await tapButton(find.findTextInFlowyText(icon)); + } +} diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart index 94103ffef8..50fdebd203 100644 --- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart +++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart @@ -64,4 +64,9 @@ class KVKeys { /// The value is a json string with the following format: /// {'feature_flag_1': true, 'feature_flag_2': false} static const String featureFlag = 'featureFlag'; + + /// The key for saving the last opened workspace id + /// + /// The workspace id is a string. + static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId'; } diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart index c01d72a7fd..e8c9be51d5 100644 --- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart +++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart @@ -27,8 +27,7 @@ Future afLaunchUrl( ); } on PlatformException catch (e) { Log.error('Failed to open uri: $e'); - } finally { - result = false; + return false; } // if the uri is not a valid url, try to launch it with http scheme diff --git a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart index 4dcaf3fa23..259ab09745 100644 --- a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart @@ -5,6 +5,9 @@ import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; +// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value +const String _source = 'Document'; + typedef DocumentNotificationCallback = void Function( DocumentNotification, FlowyResult, @@ -16,7 +19,8 @@ class DocumentNotificationParser super.id, required super.callback, }) : super( - tyParser: (ty) => DocumentNotification.valueOf(ty), + tyParser: (ty, source) => + source == _source ? DocumentNotification.valueOf(ty) : null, errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } diff --git a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart index 46cba8cbfe..e7304ac14b 100644 --- a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart @@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; +// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value +const String _source = 'Workspace'; + // Folder typedef FolderNotificationCallback = void Function( FolderNotification, @@ -21,7 +24,8 @@ class FolderNotificationParser super.id, required super.callback, }) : super( - tyParser: (ty) => FolderNotification.valueOf(ty), + tyParser: (ty, source) => + source == _source ? FolderNotification.valueOf(ty) : null, errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } diff --git a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart index 4d67f0bbb0..38676e384c 100644 --- a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart @@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; +// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value +const String _source = 'Database'; + // DatabasePB typedef DatabaseNotificationCallback = void Function( DatabaseNotification, @@ -21,7 +24,8 @@ class DatabaseNotificationParser super.id, required super.callback, }) : super( - tyParser: (ty) => DatabaseNotification.valueOf(ty), + tyParser: (ty, source) => + source == _source ? DatabaseNotification.valueOf(ty) : null, errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } diff --git a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart index 9aba14cd27..e6ed20fab0 100644 --- a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart +++ b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart @@ -14,7 +14,7 @@ class NotificationParser { String? id; void Function(T, FlowyResult) callback; E Function(Uint8List) errorParser; - T? Function(int) tyParser; + T? Function(int, String) tyParser; void parse(SubscribeObject subject) { if (id != null) { @@ -23,7 +23,7 @@ class NotificationParser { } } - final ty = tyParser(subject.ty); + final ty = tyParser(subject.ty, subject.source); if (ty == null) { return; } diff --git a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart index 741f26967c..36c7638df5 100644 --- a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart +++ b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart @@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart'; import 'notification_helper.dart'; +// This value should be the same as the USER_OBSERVABLE_SOURCE value +const String _source = 'User'; + // User typedef UserNotificationCallback = void Function( UserNotification, @@ -21,7 +24,8 @@ class UserNotificationParser required String super.id, required super.callback, }) : super( - tyParser: (ty) => UserNotification.valueOf(ty), + tyParser: (ty, source) => + source == _source ? UserNotification.valueOf(ty) : null, errorParser: (bytes) => FlowyError.fromBuffer(bytes), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart index db4540f3c0..3a2c9a83fa 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart @@ -26,7 +26,7 @@ extension on ViewPB { String get routeName { switch (layout) { case ViewLayoutPB.Document: - return MobileEditorScreen.routeName; + return MobileDocumentScreen.routeName; case ViewLayoutPB.Grid: return MobileGridScreen.routeName; case ViewLayoutPB.Calendar: @@ -42,8 +42,8 @@ extension on ViewPB { switch (layout) { case ViewLayoutPB.Document: return { - MobileEditorScreen.viewId: id, - MobileEditorScreen.viewTitle: name, + MobileDocumentScreen.viewId: id, + MobileDocumentScreen.viewTitle: name, }; case ViewLayoutPB.Grid: return { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart index 2a35ab24cc..2341513c25 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart @@ -77,7 +77,7 @@ class AppBarDoneButton extends StatelessWidget { Widget build(BuildContext context) { return AppBarButton( onTap: onTap, - padding: const EdgeInsets.fromLTRB(12, 12, 8, 12), + padding: const EdgeInsets.all(12), child: FlowyText( LocaleKeys.button_done.tr(), color: Theme.of(context).colorScheme.primary, @@ -93,7 +93,7 @@ class AppBarSaveButton extends StatelessWidget { super.key, required this.onTap, this.enable = true, - this.padding = const EdgeInsets.fromLTRB(12, 12, 8, 12), + this.padding = const EdgeInsets.all(12), }); final VoidCallback onTap; @@ -165,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget { @override Widget build(BuildContext context) { return AppBarButton( - padding: const EdgeInsets.fromLTRB(12, 12, 8, 12), + padding: const EdgeInsets.all(12), onTap: () => onTap(context), child: const FlowySvg(FlowySvgs.three_dots_s), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart index 5ed50a2f1a..701b356283 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart @@ -4,7 +4,10 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; -import 'package:appflowy/plugins/document/document_page.dart'; +import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; +import 'package:appflowy/plugins/shared/sync_indicator.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; @@ -70,7 +73,21 @@ class _MobileViewPageState extends State { } else { body = state.data!.fold((view) { viewPB = view; - actions.add(_buildAppBarMoreButton(view)); + actions.addAll([ + if (FeatureFlag.syncDocument.isOn) ...[ + DocumentCollaborators( + width: 60, + height: 44, + fontSize: 14, + padding: const EdgeInsets.symmetric(vertical: 8), + view: view, + ), + const HSpace(16.0), + DocumentSyncIndicator(view: view), + const HSpace(8.0), + ], + _buildAppBarMoreButton(view), + ]); final plugin = view.plugin(arguments: widget.arguments ?? const {}) ..init(); return plugin.widgetBuilder.buildWidget(shrinkWrap: false); @@ -144,6 +161,8 @@ class _MobileViewPageState extends State { Widget _buildAppBarMoreButton(ViewPB view) { return AppBarMoreButton( onTap: (context) { + EditorNotification.exitEditing().post(); + showMobileBottomSheet( context, showDragHandle: true, @@ -183,14 +202,12 @@ class _MobileViewPageState extends State { context.read().add(FavoriteEvent.toggle(view)); break; case MobileViewBottomSheetBodyAction.undo: - context.dispatchNotification( - const EditorNotification(type: EditorNotificationType.redo), - ); + EditorNotification.undo().post(); context.pop(); break; case MobileViewBottomSheetBodyAction.redo: + EditorNotification.redo().post(); context.pop(); - context.dispatchNotification(EditorNotification.redo()); break; case MobileViewBottomSheetBodyAction.helpCenter: // unimplemented diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart new file mode 100644 index 0000000000..c9d8a48e6b --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart @@ -0,0 +1,82 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class BottomSheetCloseButton extends StatelessWidget { + const BottomSheetCloseButton({ + super.key, + this.onTap, + }); + + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap ?? () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: 18, + height: 18, + child: FlowySvg( + FlowySvgs.m_bottom_sheet_close_m, + ), + ), + ), + ); + } +} + +class BottomSheetDoneButton extends StatelessWidget { + const BottomSheetDoneButton({ + super.key, + this.onDone, + }); + + final VoidCallback? onDone; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onDone ?? () => Navigator.pop(context), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0), + child: FlowyText( + LocaleKeys.button_done.tr(), + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w500, + textAlign: TextAlign.right, + ), + ), + ); + } +} + +class BottomSheetBackButton extends StatelessWidget { + const BottomSheetBackButton({ + super.key, + this.onTap, + }); + + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap ?? () => Navigator.pop(context), + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: 18, + height: 18, + child: FlowySvg( + FlowySvgs.m_app_bar_back_s, + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart index e09b13268c..e1bc32a6f0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart @@ -1,6 +1,4 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -26,7 +24,7 @@ class BottomSheetHeader extends StatelessWidget { left: 0, child: Align( alignment: Alignment.centerLeft, - child: AppBarCloseButton( + child: BottomSheetCloseButton( onTap: onClose, ), ), @@ -41,19 +39,8 @@ class BottomSheetHeader extends StatelessWidget { if (onDone != null) Align( alignment: Alignment.centerRight, - child: FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), - decoration: const BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10)), - color: Color(0xFF00BCF0), - ), - text: FlowyText.medium( - LocaleKeys.button_done.tr(), - color: Colors.white, - fontSize: 16.0, - ), - onTap: onDone, + child: BottomSheetDoneButton( + onDone: onDone, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart index 0b0ce92b34..d4f49cb9a9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart @@ -52,7 +52,7 @@ class _MobileBottomSheetRenameWidgetState height: 42.0, child: FlowyTextField( controller: controller, - textInputAction: TextInputAction.done, + keyboardType: TextInputType.text, onSubmitted: (text) => widget.onRename(text), ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart index a240f32178..8a51a08176 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder; import 'package:flutter/material.dart'; @@ -195,12 +195,12 @@ class BottomSheetHeader extends StatelessWidget { if (showBackButton) const Align( alignment: Alignment.centerLeft, - child: AppBarBackButton(), + child: BottomSheetBackButton(), ), if (showCloseButton) const Align( alignment: Alignment.centerLeft, - child: AppBarCloseButton(), + child: BottomSheetCloseButton(), ), Align( child: FlowyText( @@ -212,8 +212,8 @@ class BottomSheetHeader extends StatelessWidget { if (showDoneButton) Align( alignment: Alignment.centerRight, - child: AppBarDoneButton( - onTap: () => Navigator.pop(context), + child: BottomSheetDoneButton( + onDone: () => Navigator.pop(context), ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart index 078da42b9d..549a7f4a8a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart @@ -199,7 +199,7 @@ class MobileHiddenGroup extends StatelessWidget { children: [ Expanded( child: Text( - group.groupName, + context.read().generateGroupNameFromGroup(group), style: Theme.of(context).textTheme.bodyMedium, maxLines: 2, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart index 95c9fd0ee0..26c9d462a6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart @@ -11,7 +11,7 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart index fe482f57c4..14c4e022ae 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart @@ -2,8 +2,8 @@ import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter/material.dart'; -class MobileEditorScreen extends StatelessWidget { - const MobileEditorScreen({ +class MobileDocumentScreen extends StatelessWidget { + const MobileDocumentScreen({ super.key, required this.id, this.title, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart index d7d9b7993f..9d8cbf608e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart @@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; @@ -16,22 +16,22 @@ class MobileFavoritePageFolder extends StatelessWidget { const MobileFavoritePageFolder({ super.key, required this.userProfile, - required this.workspaceSetting, + required this.workspaceId, }); final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final String workspaceId; @override Widget build(BuildContext context) { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( + SidebarSectionsEvent.initial( userProfile, - workspaceSetting.workspaceId, + workspaceId, ), ), ), @@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget { create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), ), ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => - context.pushView(state.lastCreatedRootView!), - ), - ], - child: Builder( - builder: (context) { - final favoriteState = context.watch().state; - if (favoriteState.views.isEmpty) { - return FlowyMobileStateContainer.info( - emoji: '😁', - title: LocaleKeys.favorite_noFavorite.tr(), - description: LocaleKeys.favorite_noFavoriteHintText.tr(), + child: BlocListener( + listener: (context, state) { + context.read().add( + const FavoriteEvent.initial(), ); - } - return Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: SlidableAutoCloseBehavior( - child: Column( - children: [ - MobileFavoriteFolder( - showHeader: false, - forceExpanded: true, - views: favoriteState.views, - ), - const VSpace(100.0), - ], + }, + child: MultiBlocListener( + listeners: [ + BlocListener( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) => + context.pushView(state.lastCreatedRootView!), + ), + ], + child: Builder( + builder: (context) { + final favoriteState = context.watch().state; + if (favoriteState.views.isEmpty) { + return FlowyMobileStateContainer.info( + emoji: '😁', + title: LocaleKeys.favorite_noFavorite.tr(), + description: LocaleKeys.favorite_noFavoriteHintText.tr(), + ); + } + return Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SlidableAutoCloseBehavior( + child: Column( + children: [ + MobileFavoriteFolder( + showHeader: false, + forceExpanded: true, + views: favoriteState.views, + ), + const VSpace(100.0), + ], + ), ), ), ), - ), - ); - }, + ); + }, + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart index d6e9a18272..7afc740b45 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart @@ -4,11 +4,13 @@ import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dar import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/user/prelude.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class MobileFavoriteScreen extends StatelessWidget { const MobileFavoriteScreen({ @@ -50,9 +52,23 @@ class MobileFavoriteScreen extends StatelessWidget { return Scaffold( body: SafeArea( - child: MobileFavoritePage( - userProfile: userProfile, - workspaceSetting: workspaceSetting, + child: BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), + ), + child: BlocBuilder( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + return MobileFavoritePage( + userProfile: userProfile, + workspaceId: state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ); + }, + ), ), ), ); @@ -65,11 +81,11 @@ class MobileFavoritePage extends StatelessWidget { const MobileFavoritePage({ super.key, required this.userProfile, - required this.workspaceSetting, + required this.workspaceId, }); final UserProfilePB userProfile; - final WorkspaceSettingPB workspaceSetting; + final String workspaceId; @override Widget build(BuildContext context) { @@ -92,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget { Expanded( child: MobileFavoritePageFolder( userProfile: userProfile, - workspaceSetting: workspaceSetting, + workspaceId: workspaceId, ), ), ], diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart index cc42b3c9b9..f062bccffd 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart @@ -1,24 +1,28 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; -import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart'; +import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; +// Contains Public And Private Sections class MobileFolders extends StatelessWidget { const MobileFolders({ super.key, required this.user, - required this.workspaceSetting, + required this.workspaceId, required this.showFavorite, }); final UserProfilePB user; - final WorkspaceSettingPB workspaceSetting; + final String workspaceId; final bool showFavorite; @override @@ -26,11 +30,11 @@ class MobileFolders extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( + SidebarSectionsEvent.initial( user, - workspaceSetting.workspaceId, + workspaceId, ), ), ), @@ -38,24 +42,51 @@ class MobileFolders extends StatelessWidget { create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), ), ], - child: MultiBlocListener( - listeners: [ - BlocListener( - listenWhen: (p, c) => - p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, - listener: (context, state) => - context.pushView(state.lastCreatedRootView!), - ), - ], - child: Builder( - builder: (context) { - final menuState = context.watch().state; + child: BlocListener( + listener: (context, state) { + context.read().add( + SidebarSectionsEvent.initial( + user, + state.currentWorkspace?.workspaceId ?? workspaceId, + ), + ); + }, + child: BlocConsumer( + listenWhen: (p, c) => + p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, + listener: (context, state) { + final lastCreatedRootView = state.lastCreatedRootView; + if (lastCreatedRootView != null) { + context.pushView(lastCreatedRootView); + } + }, + builder: (context, state) { + final isCollaborativeWorkspace = + context.read().state.isCollabWorkspaceOn; return SlidableAutoCloseBehavior( child: Column( children: [ - MobilePersonalFolder( - views: menuState.views, - ), + ...isCollaborativeWorkspace + ? [ + MobileSectionFolder( + title: LocaleKeys.sideBar_public.tr(), + categoryType: FolderCategoryType.public, + views: state.section.publicViews, + ), + const VSpace(8.0), + MobileSectionFolder( + title: LocaleKeys.sideBar_private.tr(), + categoryType: FolderCategoryType.private, + views: state.section.privateViews, + ), + ] + : [ + MobileSectionFolder( + title: LocaleKeys.sideBar_personal.tr(), + categoryType: FolderCategoryType.public, + views: state.section.publicViews, + ), + ], const VSpace(8.0), ], ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart index cab7da1fc7..69759fc508 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart @@ -8,6 +8,7 @@ import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart'; import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; @@ -15,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; @@ -82,55 +84,73 @@ class MobileHomePage extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - children: [ - // Header - Padding( - padding: EdgeInsets.only( - left: 16, - right: 16, - top: Platform.isAndroid ? 8.0 : 0.0, - ), - child: MobileHomePageHeader( - userProfile: userProfile, - ), + return BlocProvider( + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), ), - const Divider(), - - // Folder - Expanded( - child: Scrollbar( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Recent files - const MobileRecentFolder(), - - // Folders - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: MobileFolders( - user: userProfile, - workspaceSetting: workspaceSetting, - showFavorite: false, - ), - ), - const SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: _TrashButton(), - ), - ], + child: BlocBuilder( + buildWhen: (previous, current) => + previous.currentWorkspace?.workspaceId != + current.currentWorkspace?.workspaceId, + builder: (context, state) { + if (state.currentWorkspace == null) { + return const SizedBox.shrink(); + } + return Column( + children: [ + // Header + Padding( + padding: EdgeInsets.only( + left: 16, + right: 16, + top: Platform.isAndroid ? 8.0 : 0.0, + ), + child: MobileHomePageHeader( + userProfile: userProfile, ), ), - ), - ), - ), - ], + const Divider(), + + // Folder + Expanded( + child: Scrollbar( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Recent files + const MobileRecentFolder(), + + // Folders + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: MobileFolders( + user: userProfile, + workspaceId: + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + showFavorite: false, + ), + ), + const SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: _TrashButton(), + ), + ], + ), + ), + ), + ), + ), + ], + ); + }, + ), ); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart index 157f878b75..c6782c0ce1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart @@ -1,12 +1,16 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart'; +import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart'; import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/user/settings_user_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -14,7 +18,10 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; class MobileHomePageHeader extends StatelessWidget { - const MobileHomePageHeader({super.key, required this.userProfile}); + const MobileHomePageHeader({ + super.key, + required this.userProfile, + }); final UserProfilePB userProfile; @@ -25,33 +32,22 @@ class MobileHomePageHeader extends StatelessWidget { ..add(const SettingsUserEvent.initial()), child: BlocBuilder( builder: (context, state) { - final userIcon = state.userProfile.iconUrl; + final isCollaborativeWorkspace = + context.read().state.isCollabWorkspaceOn; return ConstrainedBox( constraints: const BoxConstraints(minHeight: 52), child: Row( + mainAxisSize: MainAxisSize.min, children: [ - _UserIcon(userIcon: userIcon), - const HSpace(12), Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const FlowyText.medium('AppFlowy', fontSize: 18), - const VSpace(4), - FlowyText.regular( - userProfile.email.isNotEmpty - ? state.userProfile.email - : state.userProfile.name, - fontSize: 12, - color: Theme.of(context).colorScheme.onSurface, - overflow: TextOverflow.ellipsis, - ), - ], - ), + child: isCollaborativeWorkspace + ? _MobileWorkspace(userProfile: userProfile) + : _MobileUser(userProfile: userProfile), ), IconButton( - onPressed: () => - context.push(MobileHomeSettingPage.routeName), + onPressed: () => context.push( + MobileHomeSettingPage.routeName, + ), icon: const FlowySvg(FlowySvgs.m_setting_m), ), ], @@ -63,6 +59,154 @@ class MobileHomePageHeader extends StatelessWidget { } } +class _MobileUser extends StatelessWidget { + const _MobileUser({ + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + final userIcon = userProfile.iconUrl; + return Row( + children: [ + _UserIcon(userIcon: userIcon), + const HSpace(12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const FlowyText.medium('AppFlowy', fontSize: 18), + const VSpace(4), + FlowyText.regular( + userProfile.email.isNotEmpty + ? userProfile.email + : userProfile.name, + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } +} + +class _MobileWorkspace extends StatelessWidget { + const _MobileWorkspace({ + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final currentWorkspace = state.currentWorkspace; + if (currentWorkspace == null) { + return const SizedBox.shrink(); + } + return GestureDetector( + onTap: () { + context.read().add( + const UserWorkspaceEvent.fetchWorkspaces(), + ); + _showSwitchWorkspacesBottomSheet(context); + }, + child: Row( + children: [ + const HSpace(2.0), + SizedBox.square( + dimension: 34.0, + child: WorkspaceIcon( + workspace: currentWorkspace, + iconSize: 26, + enableEdit: false, + ), + ), + const HSpace(8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + FlowyText.medium( + currentWorkspace.name, + fontSize: 16.0, + overflow: TextOverflow.ellipsis, + ), + const HSpace(4.0), + const FlowySvg(FlowySvgs.list_dropdown_s), + ], + ), + FlowyText.medium( + userProfile.email.isNotEmpty + ? userProfile.email + : userProfile.name, + overflow: TextOverflow.ellipsis, + fontSize: 12, + color: Theme.of(context).colorScheme.onSurface, + ), + ], + ), + ), + ], + ), + ); + }, + ); + } + + void _showSwitchWorkspacesBottomSheet( + BuildContext context, + ) { + showMobileBottomSheet( + context, + showDivider: false, + showHeader: true, + showDragHandle: true, + title: LocaleKeys.workspace_menuTitle.tr(), + builder: (_) { + return BlocProvider.value( + value: context.read(), + child: BlocBuilder( + builder: (context, state) { + final currentWorkspace = state.currentWorkspace; + final workspaces = state.workspaces; + if (currentWorkspace == null || workspaces.isEmpty) { + return const SizedBox.shrink(); + } + return MobileWorkspaceMenu( + userProfile: userProfile, + currentWorkspace: currentWorkspace, + workspaces: workspaces, + onWorkspaceSelected: (workspace) { + context.pop(); + + if (workspace == currentWorkspace) { + return; + } + + context.read().add( + UserWorkspaceEvent.openWorkspace( + workspace.workspaceId, + ), + ); + }, + ); + }, + ), + ); + }, + ); + } +} + class _UserIcon extends StatelessWidget { const _UserIcon({ required this.userIcon, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart index 95e4f6e11a..1cf3b5515b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart @@ -1,11 +1,16 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/workspace/application/recent/prelude.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; class MobileRecentFolder extends StatefulWidget { const MobileRecentFolder({super.key}); @@ -22,31 +27,38 @@ class _MobileRecentFolderState extends State { ..add( const RecentViewsEvent.initial(), ), - child: BlocBuilder( - builder: (context, state) { - final ids = {}; - - List recentViews = state.views.reversed.toList(); - recentViews.retainWhere((element) => ids.add(element.id)); - - // only keep the first 20 items. - recentViews = recentViews.take(20).toList(); - - if (recentViews.isEmpty) { - return const SizedBox.shrink(); - } - - return Column( - children: [ - _RecentViews( - key: ValueKey(recentViews), - // the recent views are in reverse order - recentViews: recentViews, - ), - const VSpace(12.0), - ], - ); + child: BlocListener( + listener: (context, state) { + context.read().add( + const RecentViewsEvent.fetchRecentViews(), + ); }, + child: BlocBuilder( + builder: (context, state) { + final ids = {}; + + List recentViews = state.views.reversed.toList(); + recentViews.retainWhere((element) => ids.add(element.id)); + + // only keep the first 20 items. + recentViews = recentViews.take(20).toList(); + + if (recentViews.isEmpty) { + return const SizedBox.shrink(); + } + + return Column( + children: [ + _RecentViews( + key: ValueKey(recentViews), + // the recent views are in reverse order + recentViews: recentViews, + ), + const VSpace(12.0), + ], + ); + }, + ), ), ); } @@ -68,11 +80,70 @@ class _RecentViews extends StatelessWidget { children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 24), - child: FlowyText.semibold( - LocaleKeys.sideBar_recent.tr(), - fontSize: 20.0, + child: GestureDetector( + child: FlowyText.semibold( + LocaleKeys.sideBar_recent.tr(), + fontSize: 20.0, + ), + onTap: () { + showMobileBottomSheet( + context, + showDivider: false, + showDragHandle: true, + backgroundColor: Theme.of(context).colorScheme.background, + builder: (_) { + return Column( + children: [ + FlowyOptionTile.text( + text: LocaleKeys.button_clear.tr(), + leftIcon: FlowySvg( + FlowySvgs.m_delete_s, + color: Theme.of(context).colorScheme.error, + ), + textColor: Theme.of(context).colorScheme.error, + onTap: () { + context.read().add( + RecentViewsEvent.removeRecentViews( + recentViews.map((e) => e.id).toList(), + ), + ); + context.pop(); + }, + ), + ], + ); + }, + ); + }, ), ), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 24), + // child: FlowyText.semibold( + // LocaleKeys.sideBar_recent.tr(), + // fontSize: 20.0, + // ), + // ), + // if (kDebugMode) + // Padding( + // padding: const EdgeInsets.only(right: 16.0), + // child: FlowyButton( + // useIntrinsicWidth: true, + // text: FlowyText(LocaleKeys.button_clear.tr()), + // onTap: () { + // context.read().add( + // RecentViewsEvent.removeRecentViews( + // recentViews.map((e) => e.id).toList(), + // ), + // ); + // }, + // ), + // ), + // ], + // ), SingleChildScrollView( key: const PageStorageKey('recent_views_page_storage_key'), scrollDirection: Axis.horizontal, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart index b315cb5e52..aa938160ae 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart @@ -2,10 +2,10 @@ import 'dart:io'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; +import 'package:appflowy/plugins/document/application/doc_listener.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/shared/appflowy_network_image.dart'; -import 'package:appflowy/workspace/application/doc/doc_listener.dart'; import 'package:appflowy/workspace/application/view/prelude.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -53,7 +53,7 @@ class _MobileRecentViewState extends State { documentListener = DocumentListener(id: view.id) ..start( - didReceiveUpdate: (document) { + onDocEventUpdate: (document) { setState(() { view = view; }); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart similarity index 64% rename from frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart index a5b04a7093..58e34c63d1 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart @@ -1,26 +1,33 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart'; -import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart'; +import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart'; import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class MobilePersonalFolder extends StatelessWidget { - const MobilePersonalFolder({ +class MobileSectionFolder extends StatelessWidget { + const MobileSectionFolder({ super.key, + required this.title, required this.views, + required this.categoryType, }); + final String title; final List views; + final FolderCategoryType categoryType; @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.personal) + create: (context) => FolderBloc(type: categoryType) ..add( const FolderEvent.initial(), ), @@ -28,14 +35,25 @@ class MobilePersonalFolder extends StatelessWidget { builder: (context, state) { return Column( children: [ - MobilePersonalFolderHeader( + MobileSectionFolderHeader( + title: title, isExpanded: context.read().state.isExpanded, onPressed: () => context .read() .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context.read().add( - const FolderEvent.expandOrUnExpand(isExpanded: true), - ), + onAdded: () { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: + LocaleKeys.menuAppHeader_defaultNewPageName.tr(), + index: 0, + viewSection: categoryType.toViewSectionPB, + ), + ); + context.read().add( + const FolderEvent.expandOrUnExpand(isExpanded: true), + ); + }, ), const VSpace(8.0), const Divider( @@ -45,9 +63,9 @@ class MobilePersonalFolder extends StatelessWidget { ...views.map( (view) => MobileViewItem( key: ValueKey( - '${FolderCategoryType.personal.name} ${view.id}', + '${FolderCategoryType.private.name} ${view.id}', ), - categoryType: FolderCategoryType.personal, + categoryType: categoryType, isFirstChild: view.id == views.first.id, view: view, level: 0, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart similarity index 65% rename from frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart rename to frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart index 6e77f86454..3ba15df25d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart @@ -1,30 +1,30 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -class MobilePersonalFolderHeader extends StatefulWidget { - const MobilePersonalFolderHeader({ +@visibleForTesting +const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey'); + +class MobileSectionFolderHeader extends StatefulWidget { + const MobileSectionFolderHeader({ super.key, + required this.title, required this.onPressed, required this.onAdded, required this.isExpanded, }); + final String title; final VoidCallback onPressed; final VoidCallback onAdded; final bool isExpanded; @override - State createState() => - _MobilePersonalFolderHeaderState(); + State createState() => + _MobileSectionFolderHeaderState(); } -class _MobilePersonalFolderHeaderState - extends State { +class _MobileSectionFolderHeaderState extends State { double _turns = 0; @override @@ -35,7 +35,7 @@ class _MobilePersonalFolderHeaderState Expanded( child: FlowyButton( text: FlowyText.semibold( - LocaleKeys.sideBar_personal.tr(), + widget.title, fontSize: 20.0, ), margin: const EdgeInsets.symmetric(vertical: 8), @@ -58,6 +58,7 @@ class _MobilePersonalFolderHeaderState ), ), FlowyIconButton( + key: mobileCreateNewPageButtonKey, hoverColor: Theme.of(context).colorScheme.secondaryContainer, iconPadding: const EdgeInsets.all(2), height: iconSize, @@ -66,14 +67,7 @@ class _MobilePersonalFolderHeaderState FlowySvgs.add_s, size: Size.square(iconSize), ), - onPressed: () { - context.read().add( - SidebarRootViewsEvent.createRootView( - LocaleKeys.menuAppHeader_defaultNewPageName.tr(), - index: 0, - ), - ); - }, + onPressed: widget.onAdded, ), ], ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart new file mode 100644 index 0000000000..a41e8aba97 --- /dev/null +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart @@ -0,0 +1,119 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +// Only works on mobile. +class MobileWorkspaceMenu extends StatelessWidget { + const MobileWorkspaceMenu({ + super.key, + required this.userProfile, + required this.currentWorkspace, + required this.workspaces, + required this.onWorkspaceSelected, + }); + + final UserProfilePB userProfile; + final UserWorkspacePB currentWorkspace; + final List workspaces; + final void Function(UserWorkspacePB workspace) onWorkspaceSelected; + + @override + Widget build(BuildContext context) { + final List children = []; + for (var i = 0; i < workspaces.length; i++) { + final workspace = workspaces[i]; + children.add( + _WorkspaceMenuItem( + userProfile: userProfile, + workspace: workspace, + showTopBorder: i == 0, + currentWorkspace: currentWorkspace, + onWorkspaceSelected: onWorkspaceSelected, + ), + ); + } + return Column( + children: children, + ); + } +} + +class _WorkspaceMenuItem extends StatelessWidget { + const _WorkspaceMenuItem({ + required this.userProfile, + required this.workspace, + required this.showTopBorder, + required this.currentWorkspace, + required this.onWorkspaceSelected, + }); + + final UserProfilePB userProfile; + final UserWorkspacePB workspace; + final bool showTopBorder; + final UserWorkspacePB currentWorkspace; + final void Function(UserWorkspacePB workspace) onWorkspaceSelected; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => WorkspaceMemberBloc( + userProfile: userProfile, + workspace: workspace, + )..add(const WorkspaceMemberEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final members = state.members; + return FlowyOptionTile.text( + content: Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText( + workspace.name, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + FlowyText( + state.isLoading + ? '' + : LocaleKeys.settings_appearance_members_membersCount + .plural( + members.length, + ), + fontSize: 10.0, + color: Theme.of(context).hintColor, + ), + ], + ), + ), + ), + height: 60, + showTopBorder: showTopBorder, + leftIcon: WorkspaceIcon( + enableEdit: false, + iconSize: 26, + workspace: workspace, + ), + trailing: workspace.workspaceId == currentWorkspace.workspaceId + ? const FlowySvg( + FlowySvgs.m_blue_check_s, + blendMode: null, + ) + : null, + onTap: () => onWorkspaceSelected(workspace), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart index f87ef8645f..64e3e8824d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart @@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart'; @@ -80,15 +80,15 @@ class _NotificationScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( + SidebarSectionsEvent.initial( userProfile, workspaceSetting.workspaceId, ), ), - child: BlocBuilder( - builder: (context, menuState) => + child: BlocBuilder( + builder: (context, sectionState) => BlocBuilder( builder: (context, filterState) => BlocBuilder( @@ -122,7 +122,7 @@ class _NotificationScreenContent extends StatelessWidget { NotificationsView( shownReminders: pastReminders, reminderBloc: reminderBloc, - views: menuState.views, + views: sectionState.section.publicViews, onAction: _onAction, onDelete: _onDelete, onReadChanged: _onReadChanged, @@ -134,7 +134,7 @@ class _NotificationScreenContent extends StatelessWidget { NotificationsView( shownReminders: upcomingReminders, reminderBloc: reminderBloc, - views: menuState.views, + views: sectionState.section.publicViews, isUpcoming: true, onAction: _onAction, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart index 6f6c3adf49..44eba47bcb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart @@ -406,6 +406,10 @@ class _SingleMobileInnerViewItemState extends State { ViewEvent.createView( LocaleKeys.menuAppHeader_defaultNewPageName.tr(), layout, + section: + widget.categoryType != FolderCategoryType.favorite + ? widget.categoryType.toViewSectionPB + : null, ), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart index 66d25c58c6..337ce2549d 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart @@ -1,10 +1,12 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import '../widgets/widgets.dart'; @@ -32,10 +34,20 @@ class AboutSettingGroup extends StatelessWidget { ), onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'), ), + if (kDebugMode) + MobileSettingItem( + name: 'Feature Flags', + trailing: const Icon( + Icons.chevron_right, + ), + onTap: () { + context.push(FeatureFlagScreen.routeName); + }, + ), MobileSettingItem( name: LocaleKeys.settings_mobile_version.tr(), trailing: FlowyText( - '${DeviceOrApplicationInfoTask.applicationVersion} (${DeviceOrApplicationInfoTask.buildNumber})', + '${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})', color: Theme.of(context).colorScheme.onSurface, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart index 840306f34a..ebc58290b9 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart @@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc import 'package:appflowy_backend/log.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; class SelfHostUrlBottomSheet extends StatefulWidget { const SelfHostUrlBottomSheet({ @@ -38,32 +37,9 @@ class _SelfHostUrlBottomSheetState extends State { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Column( mainAxisSize: MainAxisSize.min, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - LocaleKeys.editor_urlHint.tr(), - style: theme.textTheme.labelSmall, - ), - IconButton( - icon: Icon( - Icons.close, - color: theme.hintColor, - ), - onPressed: () { - context.pop(); - }, - ), - ], - ), - const SizedBox( - height: 16, - ), Form( key: _formKey, child: TextFormField( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart index 060dacedf0..095214d6ef 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart @@ -36,6 +36,14 @@ class _SelfHostSettingGroupState extends State { onTap: () { showMobileBottomSheet( context, + showHeader: true, + title: LocaleKeys.editor_urlHint.tr(), + showCloseButton: true, + showDivider: false, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), builder: (_) { return SelfHostUrlBottomSheet( url: url, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart index cdb54ec122..6dc45c1c40 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart @@ -43,6 +43,7 @@ class MobileSettingItem extends StatelessWidget { trailing: trailing, onTap: onTap, visualDensity: VisualDensity.compact, + contentPadding: const EdgeInsets.only(left: 8.0), ), ); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart index ceca40d019..4f76003e23 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart @@ -36,26 +36,31 @@ class FlowyOptionTile extends StatelessWidget { this.content, this.backgroundColor, this.fontFamily, + this.height, }); factory FlowyOptionTile.text({ - required String text, + String? text, + Widget? content, Color? textColor, bool showTopBorder = true, bool showBottomBorder = true, Widget? leftIcon, Widget? trailing, VoidCallback? onTap, + double? height, }) { return FlowyOptionTile._( type: FlowyOptionTileType.text, text: text, + content: content, textColor: textColor, onTap: onTap, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, leading: leftIcon, trailing: trailing, + height: height, ); } @@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget { final Color? backgroundColor; final String? fontFamily; + final double? height; + @override Widget build(BuildContext context) { final leadingWidget = _buildLeading(); @@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget { color: backgroundColor, showTopBorder: showTopBorder, showBottomBorder: showBottomBorder, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Row( - children: [ - if (leadingWidget != null) leadingWidget, - if (content != null) content!, - if (content == null) _buildText(), - if (content == null) _buildTextField(), - if (trailing != null) trailing!, - ], + child: SizedBox( + height: height, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + if (leadingWidget != null) leadingWidget, + if (content != null) content!, + if (content == null) _buildText(), + if (content == null) _buildTextField(), + if (trailing != null) trailing!, + ], + ), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart index 2e56e1691a..39528a97c2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart @@ -1,10 +1,14 @@ import 'dart:async'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; +import 'package:appflowy/plugins/database/domain/field_service.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -35,12 +39,14 @@ class RelationCellBloc extends Bloc { (event, emit) async { await event.when( didUpdateCell: (RelationCellDataPB? cellData) async { - if (cellData == null || cellData.rowIds.isEmpty) { + if (cellData == null || + cellData.rowIds.isEmpty || + state.relatedDatabaseMeta == null) { emit(state.copyWith(rows: const [])); return; } final payload = RepeatedRowIdPB( - databaseId: state.relatedDatabaseId, + databaseId: state.relatedDatabaseMeta!.databaseId, rowIds: cellData.rowIds, ); final result = @@ -54,8 +60,16 @@ class RelationCellBloc extends Bloc { ); emit(state.copyWith(rows: rows)); }, - didUpdateRelationDatabaseId: (databaseId) { - emit(state.copyWith(relatedDatabaseId: databaseId)); + didUpdateRelationTypeOption: (typeOption) async { + if (typeOption.databaseId.isEmpty) { + return; + } + final meta = await _loadDatabaseMeta(typeOption.databaseId); + emit(state.copyWith(relatedDatabaseMeta: meta)); + _loadCellData(); + }, + selectDatabaseId: (databaseId) async { + await _updateTypeOption(databaseId); }, selectRow: (rowId) async { await _handleSelectRow(rowId); @@ -73,29 +87,30 @@ class RelationCellBloc extends Bloc { } }, onCellFieldChanged: (field) { - if (!isClosed) { - // hack: SingleFieldListener receives notification before - // FieldController's copy is updated. - Future.delayed(const Duration(milliseconds: 50), () { + // hack: SingleFieldListener receives notification before + // FieldController's copy is updated. + Future.delayed(const Duration(milliseconds: 50), () { + if (!isClosed) { final RelationTypeOptionPB typeOption = cellController.getTypeOption(RelationTypeOptionDataParser()); - add( - RelationCellEvent.didUpdateRelationDatabaseId( - typeOption.databaseId, - ), - ); - }); - } + add(RelationCellEvent.didUpdateRelationTypeOption(typeOption)); + } + }); }, ); } void _init() { - final RelationTypeOptionPB typeOption = + final typeOption = cellController.getTypeOption(RelationTypeOptionDataParser()); - add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId)); + add(RelationCellEvent.didUpdateRelationTypeOption(typeOption)); + } + + void _loadCellData() { final cellData = cellController.getCellData(); - add(RelationCellEvent.didUpdateCell(cellData)); + if (!isClosed) { + add(RelationCellEvent.didUpdateCell(cellData)); + } } Future _handleSelectRow(String rowId) async { @@ -115,25 +130,66 @@ class RelationCellBloc extends Bloc { final result = await DatabaseEventUpdateRelationCell(payload).send(); result.fold((l) => null, (err) => Log.error(err)); } + + Future _loadDatabaseMeta(String databaseId) async { + final getDatabaseResult = await DatabaseEventGetDatabases().send(); + final databaseMeta = getDatabaseResult.fold( + (s) => s.items.firstWhereOrNull( + (metaPB) => metaPB.databaseId == databaseId, + ), + (f) => null, + ); + if (databaseMeta != null) { + final result = + await ViewBackendService.getView(databaseMeta.inlineViewId); + return result.fold( + (s) => DatabaseMeta( + databaseId: databaseId, + inlineViewId: databaseMeta.inlineViewId, + databaseName: s.name, + ), + (f) => null, + ); + } + return null; + } + + Future _updateTypeOption(String databaseId) async { + final newDateTypeOption = RelationTypeOptionPB( + databaseId: databaseId, + ); + + final result = await FieldBackendService.updateFieldTypeOption( + viewId: cellController.viewId, + fieldId: cellController.fieldInfo.id, + typeOptionData: newDateTypeOption.writeToBuffer(), + ); + result.fold((s) => null, (err) => Log.error(err)); + } } @freezed class RelationCellEvent with _$RelationCellEvent { - const factory RelationCellEvent.didUpdateRelationDatabaseId( - String databaseId, - ) = _DidUpdateRelationDatabaseId; + const factory RelationCellEvent.didUpdateRelationTypeOption( + RelationTypeOptionPB typeOption, + ) = _DidUpdateRelationTypeOption; const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) = _DidUpdateCell; + const factory RelationCellEvent.selectDatabaseId( + String databaseId, + ) = _SelectDatabaseId; const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId; } @freezed class RelationCellState with _$RelationCellState { const factory RelationCellState({ - required String relatedDatabaseId, + required DatabaseMeta? relatedDatabaseMeta, required List rows, }) = _RelationCellState; - factory RelationCellState.initial() => - const RelationCellState(relatedDatabaseId: "", rows: []); + factory RelationCellState.initial() => const RelationCellState( + relatedDatabaseMeta: null, + rows: [], + ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart new file mode 100644 index 0000000000..8e9e068920 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart @@ -0,0 +1,433 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart'; +import 'package:appflowy/plugins/database/domain/field_service.dart'; +import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'select_option_cell_editor_bloc.freezed.dart'; + +const String createSelectOptionSuggestionId = + "create_select_option_suggestion_id"; + +class SelectOptionCellEditorBloc + extends Bloc { + SelectOptionCellEditorBloc({required this.cellController}) + : _selectOptionService = SelectOptionCellBackendService( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + rowId: cellController.rowId, + ), + _typeOptionAction = cellController.fieldType == FieldType.SingleSelect + ? SingleSelectAction( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + onTypeOptionUpdated: (typeOptionData) => + FieldBackendService.updateFieldTypeOption( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + typeOptionData: typeOptionData, + ), + ) + : MultiSelectAction( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + onTypeOptionUpdated: (typeOptionData) => + FieldBackendService.updateFieldTypeOption( + viewId: cellController.viewId, + fieldId: cellController.fieldId, + typeOptionData: typeOptionData, + ), + ), + super(SelectOptionCellEditorState.initial(cellController)) { + _dispatch(); + _startListening(); + _loadOptions(); + } + + final SelectOptionCellBackendService _selectOptionService; + final ISelectOptionAction _typeOptionAction; + final SelectOptionCellController cellController; + + VoidCallback? _onCellChangedFn; + + final List allOptions = []; + String filter = ""; + + void _dispatch() { + on( + (event, emit) async { + await event.when( + didReceiveOptions: (options, selectedOptions) { + final result = _getVisibleOptions(options); + allOptions + ..clear() + ..addAll(options); + emit( + state.copyWith( + options: result.options, + createSelectOptionSuggestion: + result.createSelectOptionSuggestion, + selectedOptions: selectedOptions, + ), + ); + }, + createOption: () async { + if (state.createSelectOptionSuggestion == null) { + return; + } + filter = ""; + await _createOption( + name: state.createSelectOptionSuggestion!.name, + color: state.createSelectOptionSuggestion!.color, + ); + emit(state.copyWith(clearFilter: true)); + }, + deleteOption: (option) async { + await _deleteOption([option]); + }, + deleteAllOptions: () async { + if (allOptions.isNotEmpty) { + await _deleteOption(allOptions); + } + }, + updateOption: (option) async { + await _updateOption(option); + }, + selectOption: (optionId) async { + await _selectOptionService.select(optionIds: [optionId]); + }, + unSelectOption: (optionId) async { + await _selectOptionService.unSelect(optionIds: [optionId]); + }, + unSelectLastOption: () async { + if (state.selectedOptions.isEmpty) { + return; + } + final lastSelectedOptionId = state.selectedOptions.last.id; + await _selectOptionService + .unSelect(optionIds: [lastSelectedOptionId]); + }, + submitTextField: () { + _submitTextFieldValue(emit); + }, + selectMultipleOptions: (optionNames, remainder) { + if (optionNames.isNotEmpty) { + _selectMultipleOptions(optionNames); + } + _filterOption(remainder, emit); + }, + reorderOption: (fromOptionId, toOptionId) { + final options = _typeOptionAction.reorderOption( + allOptions, + fromOptionId, + toOptionId, + ); + allOptions + ..clear() + ..addAll(options); + final result = _getVisibleOptions(options); + emit(state.copyWith(options: result.options)); + }, + filterOption: (filterText) { + _filterOption(filterText, emit); + }, + focusPreviousOption: () { + _focusOption(true, emit); + }, + focusNextOption: () { + _focusOption(false, emit); + }, + updateFocusedOption: (optionId) { + emit(state.copyWith(focusedOptionId: optionId)); + }, + resetClearFilterFlag: () { + emit(state.copyWith(clearFilter: false)); + }, + ); + }, + ); + } + + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + return super.close(); + } + + Future _createOption({ + required String name, + required SelectOptionColorPB color, + }) async { + final result = await _selectOptionService.create( + name: name, + color: color, + ); + result.fold((l) => {}, (err) => Log.error(err)); + } + + Future _deleteOption(List options) async { + final result = await _selectOptionService.delete(options: options); + result.fold((l) => null, (err) => Log.error(err)); + } + + Future _updateOption(SelectOptionPB option) async { + final result = await _selectOptionService.update( + option: option, + ); + + result.fold((l) => null, (err) => Log.error(err)); + } + + void _submitTextFieldValue(Emitter emit) { + if (state.focusedOptionId == null) { + return; + } + + final focusedOptionId = state.focusedOptionId!; + + if (focusedOptionId == createSelectOptionSuggestionId) { + filter = ""; + _createOption( + name: state.createSelectOptionSuggestion!.name, + color: state.createSelectOptionSuggestion!.color, + ); + emit( + state.copyWith( + createSelectOptionSuggestion: null, + clearFilter: true, + ), + ); + } else if (!state.selectedOptions + .any((option) => option.id == focusedOptionId)) { + _selectOptionService.select(optionIds: [focusedOptionId]); + } + } + + void _selectMultipleOptions(List optionNames) { + final optionIds = optionNames + .map( + (name) => allOptions.firstWhereOrNull( + (option) => option.name.toLowerCase() == name.toLowerCase(), + ), + ) + .nonNulls + .map((option) => option.id) + .toList(); + + _selectOptionService.select(optionIds: optionIds); + } + + void _filterOption( + String filterText, + Emitter emit, + ) { + filter = filterText; + final _MakeOptionResult result = _getVisibleOptions( + allOptions, + ); + final focusedOptionId = result.options.isEmpty + ? result.createSelectOptionSuggestion == null + ? null + : createSelectOptionSuggestionId + : result.options.any((option) => option.id == state.focusedOptionId) + ? state.focusedOptionId + : result.options.first.id; + emit( + state.copyWith( + options: result.options, + createSelectOptionSuggestion: result.createSelectOptionSuggestion, + focusedOptionId: focusedOptionId, + ), + ); + } + + Future _loadOptions() async { + final result = await _selectOptionService.getCellData(); + if (isClosed) { + Log.warn("Unexpecteded closing the bloc"); + return; + } + + return result.fold( + (data) => add( + SelectOptionCellEditorEvent.didReceiveOptions( + data.options, + data.selectOptions, + ), + ), + (err) { + Log.error(err); + return null; + }, + ); + } + + _MakeOptionResult _getVisibleOptions( + List allOptions, + ) { + final List options = List.from(allOptions); + String newOptionName = filter; + + if (filter.isNotEmpty) { + options.retainWhere((option) { + final name = option.name.toLowerCase(); + final lFilter = filter.toLowerCase(); + + if (name == lFilter) { + newOptionName = ""; + } + + return name.contains(lFilter); + }); + } + + return _MakeOptionResult( + options: options, + createSelectOptionSuggestion: newOptionName.isEmpty + ? null + : CreateSelectOptionSuggestion( + name: newOptionName, + color: newSelectOptionColor(allOptions), + ), + ); + } + + void _focusOption(bool previous, Emitter emit) { + if (state.options.isEmpty && state.createSelectOptionSuggestion == null) { + return; + } + + final optionIds = [ + ...state.options.map((e) => e.id), + if (state.createSelectOptionSuggestion != null) + createSelectOptionSuggestionId, + ]; + + if (state.focusedOptionId == null) { + emit( + state.copyWith( + focusedOptionId: previous ? optionIds.last : optionIds.first, + ), + ); + return; + } + + final currentIndex = + optionIds.indexWhere((id) => id == state.focusedOptionId); + + final newIndex = currentIndex == -1 + ? 0 + : (currentIndex + (previous ? -1 : 1)) % optionIds.length; + + emit(state.copyWith(focusedOptionId: optionIds[newIndex])); + } + + void _startListening() { + _onCellChangedFn = cellController.addListener( + onCellChanged: (selectOptionContext) { + _loadOptions(); + }, + onCellFieldChanged: (field) { + _loadOptions(); + }, + ); + } +} + +@freezed +class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent { + const factory SelectOptionCellEditorEvent.didReceiveOptions( + List options, + List selectedOptions, + ) = _DidReceiveOptions; + const factory SelectOptionCellEditorEvent.createOption() = _CreateOption; + const factory SelectOptionCellEditorEvent.selectOption(String optionId) = + _SelectOption; + const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) = + _UnSelectOption; + const factory SelectOptionCellEditorEvent.unSelectLastOption() = + _UnSelectLastOption; + const factory SelectOptionCellEditorEvent.updateOption( + SelectOptionPB option, + ) = _UpdateOption; + const factory SelectOptionCellEditorEvent.deleteOption( + SelectOptionPB option, + ) = _DeleteOption; + const factory SelectOptionCellEditorEvent.deleteAllOptions() = + _DeleteAllOptions; + const factory SelectOptionCellEditorEvent.reorderOption( + String fromOptionId, + String toOptionId, + ) = _ReorderOption; + const factory SelectOptionCellEditorEvent.filterOption(String filterText) = + _SelectOptionFilter; + const factory SelectOptionCellEditorEvent.submitTextField() = + _SubmitTextField; + const factory SelectOptionCellEditorEvent.selectMultipleOptions( + List optionNames, + String remainder, + ) = _SelectMultipleOptions; + const factory SelectOptionCellEditorEvent.focusPreviousOption() = + _FocusPreviousOption; + const factory SelectOptionCellEditorEvent.focusNextOption() = + _FocusNextOption; + const factory SelectOptionCellEditorEvent.updateFocusedOption( + String? optionId, + ) = _UpdateFocusedOption; + const factory SelectOptionCellEditorEvent.resetClearFilterFlag() = + _ResetClearFilterFlag; +} + +@freezed +class SelectOptionCellEditorState with _$SelectOptionCellEditorState { + const factory SelectOptionCellEditorState({ + required List options, + required List selectedOptions, + required CreateSelectOptionSuggestion? createSelectOptionSuggestion, + required String? focusedOptionId, + required bool clearFilter, + }) = _SelectOptionEditorState; + + factory SelectOptionCellEditorState.initial( + SelectOptionCellController context, + ) { + final data = context.getCellData(loadIfNotExist: false); + return SelectOptionCellEditorState( + options: data?.options ?? [], + selectedOptions: data?.selectOptions ?? [], + createSelectOptionSuggestion: null, + focusedOptionId: null, + clearFilter: false, + ); + } +} + +class _MakeOptionResult { + _MakeOptionResult({ + required this.options, + required this.createSelectOptionSuggestion, + }); + + List options; + CreateSelectOptionSuggestion? createSelectOptionSuggestion; +} + +class CreateSelectOptionSuggestion { + CreateSelectOptionSuggestion({ + required this.name, + required this.color, + }); + + final String name; + final SelectOptionColorPB color; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_editor_bloc.dart deleted file mode 100644 index 224d916719..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_editor_bloc.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'select_option_editor_bloc.freezed.dart'; - -class SelectOptionCellEditorBloc - extends Bloc { - SelectOptionCellEditorBloc({required this.cellController}) - : _selectOptionService = SelectOptionCellBackendService( - viewId: cellController.viewId, - fieldId: cellController.fieldId, - rowId: cellController.rowId, - ), - super(SelectOptionEditorState.initial(cellController)) { - _dispatch(); - } - - final SelectOptionCellBackendService _selectOptionService; - final SelectOptionCellController cellController; - - VoidCallback? _onCellChangedFn; - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: () async { - _startListening(); - await _loadOptions(); - }, - didReceiveOptions: (options, selectedOptions) { - final result = _makeOptions(state.filter, options); - emit( - state.copyWith( - allOptions: options, - options: result.options, - createOption: result.createOption, - selectedOptions: selectedOptions, - ), - ); - }, - newOption: (optionName) async { - await _createOption(optionName); - emit( - state.copyWith( - filter: null, - ), - ); - }, - deleteOption: (option) async { - await _deleteOption([option]); - }, - deleteAllOptions: () async { - if (state.allOptions.isNotEmpty) { - await _deleteOption(state.allOptions); - } - }, - updateOption: (option) async { - await _updateOption(option); - }, - selectOption: (optionId) async { - await _selectOptionService.select(optionIds: [optionId]); - final selectedOption = [ - ...state.selectedOptions, - state.options.firstWhere( - (element) => element.id == optionId, - ), - ]; - emit( - state.copyWith( - selectedOptions: selectedOption, - ), - ); - }, - unSelectOption: (optionId) async { - await _selectOptionService.unSelect(optionIds: [optionId]); - final selectedOptions = [...state.selectedOptions] - ..removeWhere((e) => e.id == optionId); - emit( - state.copyWith( - selectedOptions: selectedOptions, - ), - ); - }, - trySelectOption: (optionName) { - _trySelectOption(optionName, emit); - }, - selectMultipleOptions: (optionNames, remainder) { - if (optionNames.isNotEmpty) { - _selectMultipleOptions(optionNames); - } - _filterOption(remainder, emit); - }, - filterOption: (optionName) { - _filterOption(optionName, emit); - }, - ); - }, - ); - } - - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - return super.close(); - } - - Future _createOption(String name) async { - final result = await _selectOptionService.create(name: name); - result.fold((l) => {}, (err) => Log.error(err)); - } - - Future _deleteOption(List options) async { - final result = await _selectOptionService.delete(options: options); - result.fold((l) => null, (err) => Log.error(err)); - } - - Future _updateOption(SelectOptionPB option) async { - final result = await _selectOptionService.update( - option: option, - ); - - result.fold((l) => null, (err) => Log.error(err)); - } - - void _trySelectOption( - String optionName, - Emitter emit, - ) { - SelectOptionPB? matchingOption; - bool optionExistsButSelected = false; - - for (final option in state.options) { - if (option.name.toLowerCase() == optionName.toLowerCase()) { - if (!state.selectedOptions.contains(option)) { - matchingOption = option; - break; - } else { - optionExistsButSelected = true; - } - } - } - - // if there isn't a matching option at all, then create it - if (matchingOption == null && !optionExistsButSelected) { - _createOption(optionName); - } - - // if there is an unselected matching option, select it - if (matchingOption != null) { - _selectOptionService.select(optionIds: [matchingOption.id]); - } - - // clear the filter - emit(state.copyWith(filter: null)); - } - - void _selectMultipleOptions(List optionNames) { - // The options are unordered. So in order to keep the inserted [optionNames] - // order, it needs to get the option id in the [optionNames] order. - final lowerCaseNames = optionNames.map((e) => e.toLowerCase()); - final Map optionIdsMap = {}; - for (final option in state.options) { - optionIdsMap[option.name.toLowerCase()] = option.id; - } - - final optionIds = lowerCaseNames - .where((name) => optionIdsMap[name] != null) - .map((name) => optionIdsMap[name]!) - .toList(); - - _selectOptionService.select(optionIds: optionIds); - } - - void _filterOption(String optionName, Emitter emit) { - final _MakeOptionResult result = _makeOptions( - optionName, - state.allOptions, - ); - emit( - state.copyWith( - filter: optionName, - options: result.options, - createOption: result.createOption, - ), - ); - } - - Future _loadOptions() async { - final result = await _selectOptionService.getCellData(); - if (isClosed) { - Log.warn("Unexpecteded closing the bloc"); - return; - } - - return result.fold( - (data) => add( - SelectOptionEditorEvent.didReceiveOptions( - data.options, - data.selectOptions, - ), - ), - (err) { - Log.error(err); - return null; - }, - ); - } - - _MakeOptionResult _makeOptions( - String? filter, - List allOptions, - ) { - final List options = List.from(allOptions); - String? createOption = filter; - - if (filter != null && filter.isNotEmpty) { - options.retainWhere((option) { - final name = option.name.toLowerCase(); - final lFilter = filter.toLowerCase(); - - if (name == lFilter) { - createOption = null; - } - - return name.contains(lFilter); - }); - } else { - createOption = null; - } - - return _MakeOptionResult( - options: options, - createOption: createOption, - ); - } - - void _startListening() { - _onCellChangedFn = cellController.addListener( - onCellChanged: (selectOptionContext) { - _loadOptions(); - }, - onCellFieldChanged: (field) { - _loadOptions(); - }, - ); - } -} - -@freezed -class SelectOptionEditorEvent with _$SelectOptionEditorEvent { - const factory SelectOptionEditorEvent.initial() = _Initial; - const factory SelectOptionEditorEvent.didReceiveOptions( - List options, - List selectedOptions, - ) = _DidReceiveOptions; - const factory SelectOptionEditorEvent.newOption(String optionName) = - _NewOption; - const factory SelectOptionEditorEvent.selectOption(String optionId) = - _SelectOption; - const factory SelectOptionEditorEvent.unSelectOption(String optionId) = - _UnSelectOption; - const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) = - _UpdateOption; - const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) = - _DeleteOption; - const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions; - const factory SelectOptionEditorEvent.filterOption(String optionName) = - _SelectOptionFilter; - const factory SelectOptionEditorEvent.trySelectOption(String optionName) = - _TrySelectOption; - const factory SelectOptionEditorEvent.selectMultipleOptions( - List optionNames, - String remainder, - ) = _SelectMultipleOptions; -} - -@freezed -class SelectOptionEditorState with _$SelectOptionEditorState { - const factory SelectOptionEditorState({ - required List options, - required List allOptions, - required List selectedOptions, - required String? createOption, - required String? filter, - }) = _SelectOptionEditorState; - - factory SelectOptionEditorState.initial(SelectOptionCellController context) { - final data = context.getCellData(loadIfNotExist: false); - return SelectOptionEditorState( - options: data?.options ?? [], - allOptions: data?.options ?? [], - selectedOptions: data?.selectOptions ?? [], - createOption: null, - filter: null, - ); - } -} - -class _MakeOptionResult { - _MakeOptionResult({ - required this.options, - required this.createOption, - }); - - List options; - String? createOption; -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart index dbd2258cc1..fe52a73aac 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart'; @@ -29,15 +30,17 @@ class URLCellBloc extends Bloc { void _dispatch() { on( (event, emit) async { - event.when( + await event.when( initial: () { _startListening(); }, - didReceiveCellUpdate: (cellData) { + didReceiveCellUpdate: (cellData) async { + final content = cellData?.content ?? ""; + final isValid = await isUrlValid(content); emit( state.copyWith( - content: cellData?.content ?? "", - url: cellData?.url ?? "", + content: content, + isValid: isValid, ), ); }, @@ -58,6 +61,31 @@ class URLCellBloc extends Bloc { }, ); } + + Future isUrlValid(String content) async { + if (content.isEmpty) { + return true; + } + + try { + // check protocol is provided + const linkPrefix = [ + 'http://', + 'https://', + ]; + final shouldAddScheme = + !linkPrefix.any((pattern) => content.startsWith(pattern)); + final url = shouldAddScheme ? 'http://$content' : content; + + // get hostname and check validity + final uri = Uri.parse(url); + final hostName = uri.host; + await InternetAddress.lookup(hostName); + } catch (_) { + return false; + } + return true; + } } @freezed @@ -72,14 +100,14 @@ class URLCellEvent with _$URLCellEvent { class URLCellState with _$URLCellState { const factory URLCellState({ required String content, - required String url, + required bool isValid, }) = _URLCellState; factory URLCellState.initial(URLCellController context) { final cellData = context.getCellData(); return URLCellState( content: cellData?.content ?? "", - url: cellData?.url ?? "", + isValid: true, ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart index 5beb2bba88..db1a56071e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart @@ -162,75 +162,6 @@ class FieldController { /// Listen for filter changes in the backend. void _listenOnFilterChanges() { - void deleteFilterFromChangeset( - List filters, - FilterChangesetNotificationPB changeset, - ) { - final deleteFilterIds = changeset.deleteFilters.map((e) => e.id).toList(); - if (deleteFilterIds.isNotEmpty) { - filters.retainWhere( - (element) => !deleteFilterIds.contains(element.filter.id), - ); - } - } - - void insertFilterFromChangeset( - List filters, - FilterChangesetNotificationPB changeset, - ) { - for (final newFilter in changeset.insertFilters) { - final filterIndex = - filters.indexWhere((element) => element.filter.id == newFilter.id); - if (filterIndex == -1) { - final fieldInfo = _findFieldInfo( - fieldInfos: fieldInfos, - fieldId: newFilter.fieldId, - fieldType: newFilter.fieldType, - ); - if (fieldInfo != null) { - filters.add(FilterInfo(viewId, newFilter, fieldInfo)); - } - } - } - } - - void updateFilterFromChangeset( - List filters, - FilterChangesetNotificationPB changeset, - ) { - for (final updatedFilter in changeset.updateFilters) { - final filterIndex = filters.indexWhere( - (element) => element.filter.id == updatedFilter.filterId, - ); - // Remove the old filter - if (filterIndex != -1) { - filters.removeAt(filterIndex); - } - - // Insert the filter if there is a filter and its field info is - // not null - if (updatedFilter.hasFilter()) { - final fieldInfo = _findFieldInfo( - fieldInfos: fieldInfos, - fieldId: updatedFilter.filter.fieldId, - fieldType: updatedFilter.filter.fieldType, - ); - - if (fieldInfo != null) { - // Insert the filter with the position: filterIndex, otherwise, - // append it to the end of the list. - final filterInfo = - FilterInfo(viewId, updatedFilter.filter, fieldInfo); - if (filterIndex != -1) { - filters.insert(filterIndex, filterInfo); - } else { - filters.add(filterInfo); - } - } - } - } - } - _filtersListener.start( onFilterChanged: (result) { if (_isDisposed) { @@ -239,15 +170,19 @@ class FieldController { result.fold( (FilterChangesetNotificationPB changeset) { - final List filters = filterInfos; - // delete removed filters - deleteFilterFromChangeset(filters, changeset); + final List filters = []; + for (final filter in changeset.filters.items) { + final fieldInfo = _findFieldInfo( + fieldInfos: fieldInfos, + fieldId: filter.data.fieldId, + fieldType: filter.data.fieldType, + ); - // insert new filters - insertFilterFromChangeset(filters, changeset); - - // edit modified filters - updateFilterFromChangeset(filters, changeset); + if (fieldInfo != null) { + final filterInfo = FilterInfo(viewId, filter, fieldInfo); + filters.add(filterInfo); + } + } _filterNotifier?.filters = filters; _updateFieldInfos(); @@ -665,8 +600,8 @@ class FieldController { FilterInfo? getFilterInfo(FilterPB filterPB) { final fieldInfo = _findFieldInfo( fieldInfos: fieldInfos, - fieldId: filterPB.fieldId, - fieldType: filterPB.fieldType, + fieldId: filterPB.data.fieldId, + fieldType: filterPB.data.fieldType, ); return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart index 64d5a398be..1612ab6a23 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart @@ -47,7 +47,7 @@ class FieldInfo with _$FieldInfo { } bool get canCreateFilter { - if (hasFilter) { + if (isGroupField) { return false; } @@ -58,6 +58,7 @@ class FieldInfo with _$FieldInfo { case FieldType.RichText: case FieldType.SingleSelect: case FieldType.Checklist: + case FieldType.URL: return true; default: return false; diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart new file mode 100644 index 0000000000..df8e0d46fb --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/dispatch/dispatch.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'relation_type_option_cubit.freezed.dart'; + +class RelationDatabaseListCubit extends Cubit { + RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) { + _loadDatabaseMetas(); + } + + void _loadDatabaseMetas() async { + final getDatabaseResult = await DatabaseEventGetDatabases().send(); + final metaPBs = getDatabaseResult.fold>( + (s) => s.items, + (f) => [], + ); + final futures = metaPBs.map((meta) { + return ViewBackendService.getView(meta.inlineViewId).then( + (result) => result.fold( + (s) => DatabaseMeta( + databaseId: meta.databaseId, + inlineViewId: meta.inlineViewId, + databaseName: s.name, + ), + (f) => null, + ), + ); + }); + final databaseMetas = await Future.wait(futures); + emit( + RelationDatabaseListState( + databaseMetas: databaseMetas.nonNulls.toList(), + ), + ); + } +} + +@freezed +class DatabaseMeta with _$DatabaseMeta { + factory DatabaseMeta({ + /// id of the database + required String databaseId, + + /// id of the inline view + required String inlineViewId, + + /// name of the database, currently identical to the name of the inline view + required String databaseName, + }) = _DatabaseMeta; +} + +@freezed +class RelationDatabaseListState with _$RelationDatabaseListState { + factory RelationDatabaseListState({ + required List databaseMetas, + }) = _RelationDatabaseListState; + + factory RelationDatabaseListState.initial() => + RelationDatabaseListState(databaseMetas: []); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart index cd1db30fc6..72f49dd084 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart @@ -20,10 +20,10 @@ class SelectOptionTypeOptionBloc void _dispatch() { on( (event, emit) async { - await event.when( - createOption: (optionName) async { + event.when( + createOption: (optionName) { final List options = - await typeOptionAction.insertOption(state.options, optionName); + typeOptionAction.insertOption(state.options, optionName); emit(state.copyWith(options: options)); }, addingOption: () { @@ -33,15 +33,23 @@ class SelectOptionTypeOptionBloc emit(state.copyWith(isEditingOption: false, newOptionName: null)); }, updateOption: (option) { - final List options = + final options = typeOptionAction.updateOption(state.options, option); emit(state.copyWith(options: options)); }, deleteOption: (option) { - final List options = + final options = typeOptionAction.deleteOption(state.options, option); emit(state.copyWith(options: options)); }, + reorderOption: (fromOptionId, toOptionId) { + final options = typeOptionAction.reorderOption( + state.options, + fromOptionId, + toOptionId, + ); + emit(state.copyWith(options: options)); + }, ); }, ); @@ -61,6 +69,10 @@ class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent { const factory SelectOptionTypeOptionEvent.deleteOption( SelectOptionPB option, ) = _DeleteOption; + const factory SelectOptionTypeOptionEvent.reorderOption( + String fromOptionId, + String toOptionId, + ) = _ReorderOption; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart index 8d46b994bb..235bdb60eb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart @@ -1,9 +1,7 @@ -import 'dart:async'; - import 'package:appflowy/plugins/database/domain/type_option_service.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/builder.dart'; -import 'package:appflowy_backend/log.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:nanoid/nanoid.dart'; abstract class ISelectOptionAction { ISelectOptionAction({ @@ -20,29 +18,25 @@ abstract class ISelectOptionAction { onTypeOptionUpdated(newTypeOption.writeToBuffer()); } - Future> insertOption( + List insertOption( List options, String optionName, ) { - final newOptions = List.from(options); - return service.newOption(name: optionName).then((result) { - return result.fold( - (option) { - final exists = - newOptions.any((element) => element.name == option.name); - if (!exists) { - newOptions.insert(0, option); - } + if (options.any((element) => element.name == optionName)) { + return options; + } - updateTypeOption(newOptions); - return newOptions; - }, - (err) { - Log.error(err); - return newOptions; - }, - ); - }); + final newOptions = List.from(options); + + final newSelectOption = SelectOptionPB() + ..id = nanoid(4) + ..color = newSelectOptionColor(options) + ..name = optionName; + + newOptions.insert(0, newSelectOption); + + updateTypeOption(newOptions); + return newOptions; } List deleteOption( @@ -73,6 +67,25 @@ abstract class ISelectOptionAction { updateTypeOption(newOptions); return newOptions; } + + List reorderOption( + List options, + String fromOptionId, + String toOptionId, + ) { + final newOptions = List.from(options); + final fromIndex = + newOptions.indexWhere((element) => element.id == fromOptionId); + final toIndex = + newOptions.indexWhere((element) => element.id == toOptionId); + + if (fromIndex != -1 && toIndex != -1) { + newOptions.insert(toIndex, newOptions.removeAt(fromIndex)); + } + + updateTypeOption(newOptions); + return newOptions; + } } class MultiSelectAction extends ISelectOptionAction { @@ -102,3 +115,19 @@ class SingleSelectAction extends ISelectOptionAction { onTypeOptionUpdated(newTypeOption.writeToBuffer()); } } + +SelectOptionColorPB newSelectOptionColor(List options) { + final colorFrequency = List.filled(SelectOptionColorPB.values.length, 0); + + for (final option in options) { + colorFrequency[option.color.value]++; + } + + final minIndex = colorFrequency + .asMap() + .entries + .reduce((a, b) => a.value <= b.value ? a : b) + .key; + + return SelectOptionColorPB.valueOf(minIndex) ?? SelectOptionColorPB.Purple; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart index 8479120d7b..1b0d73de8f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart @@ -28,16 +28,10 @@ class RowBackendService { ), ); - Map? cellDataByFieldId; - if (withCells != null) { final rowBuilder = RowDataBuilder(); withCells(rowBuilder); - cellDataByFieldId = rowBuilder.build(); - } - - if (cellDataByFieldId != null) { - payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId); + payload.data.addAll(rowBuilder.build()); } return DatabaseEventCreateRow(payload).send(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart new file mode 100644 index 0000000000..740fadddc1 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart @@ -0,0 +1,112 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/sync/database_sync_state_listener.dart'; +import 'package:appflowy/plugins/database/domain/database_view_service.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'database_sync_bloc.freezed.dart'; + +class DatabaseSyncBloc extends Bloc { + DatabaseSyncBloc({ + required this.view, + }) : super(DatabaseSyncBlocState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final userProfile = await getIt().getUser().then( + (value) => value.fold((s) => s, (f) => null), + ); + final databaseId = await DatabaseViewBackendService(viewId: view.id) + .getDatabaseId() + .then((value) => value.fold((s) => s, (f) => null)); + emit( + state.copyWith( + shouldShowIndicator: + userProfile?.authenticator != AuthenticatorPB.Local && + databaseId != null, + ), + ); + if (databaseId != null) { + _syncStateListener = + DatabaseSyncStateListener(databaseId: databaseId) + ..start( + didReceiveSyncState: (syncState) { + Log.info( + 'database sync state changed, from ${state.syncState} to $syncState', + ); + add(DatabaseSyncEvent.syncStateChanged(syncState)); + }, + ); + } + + final isNetworkConnected = await _connectivity + .checkConnectivity() + .then((value) => value != ConnectivityResult.none); + emit(state.copyWith(isNetworkConnected: isNetworkConnected)); + + connectivityStream = + _connectivity.onConnectivityChanged.listen((result) { + add(DatabaseSyncEvent.networkStateChanged(result)); + }); + }, + syncStateChanged: (syncState) { + emit(state.copyWith(syncState: syncState.value)); + }, + networkStateChanged: (result) { + emit( + state.copyWith( + isNetworkConnected: result != ConnectivityResult.none, + ), + ); + }, + ); + }, + ); + } + + final ViewPB view; + final _connectivity = Connectivity(); + + StreamSubscription? connectivityStream; + DatabaseSyncStateListener? _syncStateListener; + + @override + Future close() async { + await connectivityStream?.cancel(); + await _syncStateListener?.stop(); + return super.close(); + } +} + +@freezed +class DatabaseSyncEvent with _$DatabaseSyncEvent { + const factory DatabaseSyncEvent.initial() = Initial; + const factory DatabaseSyncEvent.syncStateChanged( + DatabaseSyncStatePB syncState, + ) = syncStateChanged; + const factory DatabaseSyncEvent.networkStateChanged( + ConnectivityResult result, + ) = NetworkStateChanged; +} + +@freezed +class DatabaseSyncBlocState with _$DatabaseSyncBlocState { + const factory DatabaseSyncBlocState({ + required DatabaseSyncState syncState, + @Default(true) bool isNetworkConnected, + @Default(false) bool shouldShowIndicator, + }) = _DatabaseSyncState; + + factory DatabaseSyncBlocState.initial() => const DatabaseSyncBlocState( + syncState: DatabaseSyncState.Syncing, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart new file mode 100644 index 0000000000..67914e3007 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart @@ -0,0 +1,63 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/grid_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; +import 'package:appflowy_backend/rust_stream.dart'; +import 'package:appflowy_result/appflowy_result.dart'; + +typedef DatabaseSyncStateCallback = void Function( + DatabaseSyncStatePB syncState, +); + +class DatabaseSyncStateListener { + DatabaseSyncStateListener({ + // NOTE: NOT the view id. + required this.databaseId, + }); + + final String databaseId; + StreamSubscription? _subscription; + DatabaseNotificationParser? _parser; + + DatabaseSyncStateCallback? didReceiveSyncState; + + void start({ + DatabaseSyncStateCallback? didReceiveSyncState, + }) { + this.didReceiveSyncState = didReceiveSyncState; + + _parser = DatabaseNotificationParser( + id: databaseId, + callback: _callback, + ); + _subscription = RustStreamReceiver.listen( + (observable) => _parser?.parse(observable), + ); + } + + void _callback( + DatabaseNotification ty, + FlowyResult result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateDatabaseSyncUpdate: + result.map( + (r) { + final value = DatabaseSyncStatePB.fromBuffer(r); + didReceiveSyncState?.call(value); + }, + ); + break; + default: + break; + } + } + + Future stop() async { + await _subscription?.cancel(); + _subscription = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart index 8ea1d004fa..7b816bbafc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart @@ -11,9 +11,11 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:intl/intl.dart'; import 'package:protobuf/protobuf.dart' hide FieldInfo; import '../../application/database_controller.dart'; @@ -383,21 +385,28 @@ class BoardBloc extends Bloc { groupList.insert(insertGroups.index, group); add(BoardEvent.didReceiveGroups(groupList)); }, - onUpdateGroup: (updatedGroups) { + onUpdateGroup: (updatedGroups) async { if (isClosed) { return; } + // workaround: update group most of the time gets called before fields in + // field controller are updated. For single and multi-select group + // renames, this is required before generating the new group name. + await Future.delayed(const Duration(milliseconds: 50)); + for (final group in updatedGroups) { // see if the column is already in the board - final index = groupList.indexWhere((g) => g.groupId == group.groupId); - if (index == -1) continue; + if (index == -1) { + continue; + } + final columnController = boardController.getGroupController(group.groupId); if (columnController != null) { // remove the group or update its name - columnController.updateGroupName(group.groupName); + columnController.updateGroupName(generateGroupNameFromGroup(group)); if (!group.isVisible) { boardController.removeGroup(group.groupId); } @@ -491,7 +500,7 @@ class BoardBloc extends Bloc { AppFlowyGroupData _initializeGroupData(GroupPB group) { return AppFlowyGroupData( id: group.groupId, - name: group.groupName, + name: generateGroupNameFromGroup(group), items: _buildGroupItems(group), customData: GroupData( group: group, @@ -499,6 +508,72 @@ class BoardBloc extends Bloc { ), ); } + + String generateGroupNameFromGroup(GroupPB group) { + final field = fieldController.getField(group.fieldId); + if (field == null) { + return ""; + } + + // if the group is the default group, then + if (group.isDefault) { + return "No ${field.name}"; + } + + switch (field.fieldType) { + case FieldType.SingleSelect: + final options = + SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData) + .options; + final option = + options.firstWhereOrNull((option) => option.id == group.groupId); + return option == null ? "" : option.name; + case FieldType.MultiSelect: + final options = + MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData) + .options; + final option = + options.firstWhereOrNull((option) => option.id == group.groupId); + return option == null ? "" : option.name; + case FieldType.Checkbox: + return group.groupId; + case FieldType.URL: + return group.groupId; + case FieldType.DateTime: + // Assume DateCondition::Relative as there isn't an option for this + // right now. + final dateFormat = DateFormat("y/MM/dd"); + try { + final targetDateTime = dateFormat.parseLoose(group.groupId); + final targetDateTimeDay = DateTime( + targetDateTime.year, + targetDateTime.month, + targetDateTime.day, + ); + final now = DateTime.now(); + final nowDay = DateTime( + now.year, + now.month, + now.day, + ); + final diff = targetDateTimeDay.difference(nowDay).inDays; + return switch (diff) { + 0 => "Today", + -1 => "Yesterday", + 1 => "Tomorrow", + -7 => "Last 7 days", + 2 => "Next 7 days", + -30 => "Last 30 days", + 8 => "Next 30 days", + _ => DateFormat("MMM y").format(targetDateTimeDay) + }; + } on FormatException { + return ""; + } + default: + return ""; + } + } } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart index 5f1b8c9609..7e0700e263 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart @@ -31,23 +31,11 @@ class GroupController { final GroupControllerDelegate delegate; final void Function(GroupPB group) onGroupChanged; - RowMetaPB? rowAtIndex(int index) { - if (index < group.rows.length) { - return group.rows[index]; - } else { - return null; - } - } + RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index); - RowMetaPB? firstRow() { - if (group.rows.isEmpty) return null; - return group.rows.first; - } + RowMetaPB? firstRow() => group.rows.firstOrNull; - RowMetaPB? lastRow() { - if (group.rows.isEmpty) return null; - return group.rows.last; - } + RowMetaPB? lastRow() => group.rows.lastOrNull; void startListening() { _listener.start( diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart index 16b2d45f95..40f60a09f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart @@ -1,5 +1,7 @@ import 'dart:collection'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:flutter/material.dart' hide Card; import 'package:flutter/services.dart'; @@ -34,6 +36,8 @@ import 'toolbar/board_setting_bar.dart'; import 'widgets/board_hidden_groups.dart'; class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { + final _toggleExtension = ToggleExtensionNotifier(); + @override Widget content( BuildContext context, @@ -49,14 +53,27 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { BoardSettingBar( key: _makeValueKey(controller), databaseController: controller, + toggleExtension: _toggleExtension, ); @override Widget settingBarExtension( BuildContext context, DatabaseController controller, - ) => - const SizedBox.shrink(); + ) { + return DatabaseViewSettingExtension( + key: _makeValueKey(controller), + viewId: controller.viewId, + databaseController: controller, + toggleExtension: _toggleExtension, + ); + } + + @override + void dispose() { + _toggleExtension.dispose(); + super.dispose(); + } ValueKey _makeValueKey(DatabaseController controller) => ValueKey(controller.viewId); diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart index 4a678a160e..aa2883ff73 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart @@ -1,24 +1,53 @@ import 'package:appflowy/plugins/database/application/database_controller.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class BoardSettingBar extends StatelessWidget { const BoardSettingBar({ super.key, required this.databaseController, + required this.toggleExtension, }); final DatabaseController databaseController; + final ToggleExtensionNotifier toggleExtension; @override Widget build(BuildContext context) { - return SizedBox( - height: 20, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SettingButton(databaseController: databaseController), - ], + return BlocProvider( + create: (context) => DatabaseFilterMenuBloc( + viewId: databaseController.viewId, + fieldController: databaseController.fieldController, + )..add(const DatabaseFilterMenuEvent.initial()), + child: BlocListener( + listenWhen: (p, c) => p.isVisible != c.isVisible, + listener: (context, state) => toggleExtension.toggle(), + child: ValueListenableBuilder( + valueListenable: databaseController.isLoading, + builder: (context, value, child) { + if (value) { + return const SizedBox.shrink(); + } + return SizedBox( + height: 20, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + const FilterButton(), + const HSpace(2), + SettingButton( + databaseController: databaseController, + ), + ], + ), + ); + }, + ), ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart index 82b11a0fd1..0469378c8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart @@ -269,7 +269,7 @@ class HiddenGroupButtonContent extends StatelessWidget { ), const HSpace(4), FlowyText.medium( - group.groupName, + bloc.generateGroupNameFromGroup(group), overflow: TextOverflow.ellipsis, ), const HSpace(6), @@ -369,7 +369,7 @@ class HiddenGroupPopupItemList extends StatelessWidget { Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), child: FlowyText.medium( - group.groupName, + context.read().generateGroupNameFromGroup(group), fontSize: 10, color: Theme.of(context).hintColor, overflow: TextOverflow.ellipsis, diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart index 8d237f9114..8144904cad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart @@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; class DatabaseBackendService { - static Future, FlowyError>> + static Future, FlowyError>> getAllDatabases() { return DatabaseEventGetDatabases().send().then((result) { return result.fold( diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart index 4274c97c87..9bc873f7f1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart @@ -62,6 +62,19 @@ class FieldBackendService { return DatabaseEventDeleteField(payload).send(); } + // Clear all data of all cells in a Field + static Future> clearField({ + required String viewId, + required String fieldId, + }) { + final payload = ClearFieldPayloadPB( + viewId: viewId, + fieldId: fieldId, + ); + + return DatabaseEventClearField(payload).send(); + } + /// Duplicate a field static Future> duplicateField({ required String viewId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart index f1b0a82723..a9295e38dd 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart @@ -61,19 +61,11 @@ class FilterListener { final String viewId; final String filterId; - PublishNotifier? _onDeleteNotifier = PublishNotifier(); PublishNotifier? _onUpdateNotifier = PublishNotifier(); DatabaseNotificationListener? _listener; - void start({ - void Function()? onDeleted, - void Function(FilterPB)? onUpdated, - }) { - _onDeleteNotifier?.addPublishListener((_) { - onDeleted?.call(); - }); - + void start({void Function(FilterPB)? onUpdated}) { _onUpdateNotifier?.addPublishListener((filter) { onUpdated?.call(filter); }); @@ -85,20 +77,12 @@ class FilterListener { } void handleChangeset(FilterChangesetNotificationPB changeset) { - // check the delete filter - final deletedIndex = changeset.deleteFilters.indexWhere( - (element) => element.id == filterId, - ); - if (deletedIndex != -1) { - _onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex]; - } - - // check the updated filter - final updatedIndex = changeset.updateFilters.indexWhere( - (element) => element.filter.id == filterId, + final filters = changeset.filters.items; + final updatedIndex = filters.indexWhere( + (filter) => filter.id == filterId, ); if (updatedIndex != -1) { - _onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter; + _onUpdateNotifier?.value = filters[updatedIndex]; } } @@ -122,9 +106,6 @@ class FilterListener { Future stop() async { await _listener?.stop(); - _onDeleteNotifier?.dispose(); - _onDeleteNotifier = null; - _onUpdateNotifier?.dispose(); _onUpdateNotifier = null; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart index a44cc13731..64854a8faf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart @@ -1,15 +1,6 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:fixnum/fixnum.dart' as $fixnum; @@ -40,12 +31,18 @@ class FilterBackendService { ..condition = condition ..content = content; - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: FieldType.RichText, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.RichText, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.RichText, + data: filter.writeToBuffer(), + ); } Future> insertCheckboxFilter({ @@ -55,12 +52,18 @@ class FilterBackendService { }) { final filter = CheckboxFilterPB()..condition = condition; - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: FieldType.Checkbox, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.Checkbox, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.Checkbox, + data: filter.writeToBuffer(), + ); } Future> insertNumberFilter({ @@ -73,12 +76,18 @@ class FilterBackendService { ..condition = condition ..content = content; - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: FieldType.Number, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.Number, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.Number, + data: filter.writeToBuffer(), + ); } Future> insertDateFilter({ @@ -91,33 +100,35 @@ class FilterBackendService { int? timestamp, }) { assert( - [ - FieldType.DateTime, - FieldType.LastEditedTime, - FieldType.CreatedTime, - ].contains(fieldType), + fieldType == FieldType.DateTime || + fieldType == FieldType.LastEditedTime || + fieldType == FieldType.CreatedTime, ); final filter = DateFilterPB(); + if (timestamp != null) { filter.timestamp = $fixnum.Int64(timestamp); - } else { - if (start != null && end != null) { - filter.start = $fixnum.Int64(start); - filter.end = $fixnum.Int64(end); - } else { - throw Exception( - "Start and end should not be null if the timestamp is null", - ); - } + } + if (start != null) { + filter.start = $fixnum.Int64(start); + } + if (end != null) { + filter.end = $fixnum.Int64(end); } - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: fieldType, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.DateTime, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.DateTime, + data: filter.writeToBuffer(), + ); } Future> insertURLFilter({ @@ -130,18 +141,24 @@ class FilterBackendService { ..condition = condition ..content = content; - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: FieldType.URL, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.URL, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.URL, + data: filter.writeToBuffer(), + ); } Future> insertSelectOptionFilter({ required String fieldId, required FieldType fieldType, - required SelectOptionConditionPB condition, + required SelectOptionFilterConditionPB condition, String? filterId, List optionIds = const [], }) { @@ -149,12 +166,18 @@ class FilterBackendService { ..condition = condition ..optionIds.addAll(optionIds); - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: fieldType, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: fieldType, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: fieldType, + data: filter.writeToBuffer(), + ); } Future> insertChecklistFilter({ @@ -165,67 +188,94 @@ class FilterBackendService { }) { final filter = ChecklistFilterPB()..condition = condition; - return insertFilter( - fieldId: fieldId, - filterId: filterId, - fieldType: FieldType.Checklist, - data: filter.writeToBuffer(), - ); + return filterId == null + ? insertFilter( + fieldId: fieldId, + fieldType: FieldType.Checklist, + data: filter.writeToBuffer(), + ) + : updateFilter( + filterId: filterId, + fieldId: fieldId, + fieldType: FieldType.Checklist, + data: filter.writeToBuffer(), + ); } Future> insertFilter({ required String fieldId, - String? filterId, required FieldType fieldType, required List data, - }) { - final insertFilterPayload = UpdateFilterPayloadPB.create() + }) async { + final filterData = FilterDataPB() ..fieldId = fieldId ..fieldType = fieldType - ..viewId = viewId ..data = data; - if (filterId != null) { - insertFilterPayload.filterId = filterId; - } + final insertFilterPayload = InsertFilterPB()..data = filterData; - final payload = DatabaseSettingChangesetPB.create() + final payload = DatabaseSettingChangesetPB() ..viewId = viewId - ..updateFilter = insertFilterPayload; - return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }); + ..insertFilter = insertFilterPayload; + + final result = await DatabaseEventUpdateDatabaseSetting(payload).send(); + return result.fold( + (l) => FlowyResult.success(l), + (err) { + Log.error(err); + return FlowyResult.failure(err); + }, + ); + } + + Future> updateFilter({ + required String filterId, + required String fieldId, + required FieldType fieldType, + required List data, + }) async { + final filterData = FilterDataPB() + ..fieldId = fieldId + ..fieldType = fieldType + ..data = data; + + final updateFilterPayload = UpdateFilterDataPB() + ..filterId = filterId + ..data = filterData; + + final payload = DatabaseSettingChangesetPB() + ..viewId = viewId + ..updateFilterData = updateFilterPayload; + + final result = await DatabaseEventUpdateDatabaseSetting(payload).send(); + return result.fold( + (l) => FlowyResult.success(l), + (err) { + Log.error(err); + return FlowyResult.failure(err); + }, + ); } Future> deleteFilter({ required String fieldId, required String filterId, - required FieldType fieldType, - }) { - final deleteFilterPayload = DeleteFilterPayloadPB.create() + }) async { + final deleteFilterPayload = DeleteFilterPB() ..fieldId = fieldId - ..filterId = filterId - ..viewId = viewId - ..fieldType = fieldType; + ..filterId = filterId; - final payload = DatabaseSettingChangesetPB.create() + final payload = DatabaseSettingChangesetPB() ..viewId = viewId ..deleteFilter = deleteFilterPayload; - return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) { - return result.fold( - (l) => FlowyResult.success(l), - (err) { - Log.error(err); - return FlowyResult.failure(err); - }, - ); - }); + final result = await DatabaseEventUpdateDatabaseSetting(payload).send(); + return result.fold( + (l) => FlowyResult.success(l), + (err) { + Log.error(err); + return FlowyResult.failure(err); + }, + ); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart index 165671b211..45f3155c4e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart @@ -2,8 +2,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; - -import 'type_option_service.dart'; +import 'package:nanoid/nanoid.dart'; class SelectOptionCellBackendService { SelectOptionCellBackendService({ @@ -18,26 +17,23 @@ class SelectOptionCellBackendService { Future> create({ required String name, + SelectOptionColorPB? color, bool isSelected = true, }) { - return TypeOptionBackendService(viewId: viewId, fieldId: fieldId) - .newOption(name: name) - .then( - (result) { - return result.fold( - (option) { - final payload = RepeatedSelectOptionPayload() - ..viewId = viewId - ..fieldId = fieldId - ..rowId = rowId - ..items.add(option); + final option = SelectOptionPB() + ..id = nanoid(4) + ..name = name; + if (color != null) { + option.color = color; + } - return DatabaseEventInsertOrUpdateSelectOption(payload).send(); - }, - (r) => FlowyResult.failure(r), - ); - }, - ); + final payload = RepeatedSelectOptionPayload() + ..viewId = viewId + ..fieldId = fieldId + ..rowId = rowId + ..items.add(option); + + return DatabaseEventInsertOrUpdateSelectOption(payload).send(); } Future> update({ diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart index 67c4662b56..17449bda44 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart @@ -44,7 +44,6 @@ class CheckboxFilterEditorBloc _filterBackendSvc.deleteFilter( fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id, - fieldType: filterInfo.fieldInfo.fieldType, ); }, didReceiveFilter: (FilterPB filter) { @@ -64,11 +63,10 @@ class CheckboxFilterEditorBloc void _startListening() { _listener.start( - onDeleted: () { - if (!isClosed) add(const CheckboxFilterEditorEvent.delete()); - }, onUpdated: (filter) { - if (!isClosed) add(CheckboxFilterEditorEvent.didReceiveFilter(filter)); + if (!isClosed) { + add(CheckboxFilterEditorEvent.didReceiveFilter(filter)); + } }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart index 5b91cd9195..1decdd8215 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart @@ -44,7 +44,6 @@ class ChecklistFilterEditorBloc _filterBackendSvc.deleteFilter( fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id, - fieldType: filterInfo.fieldInfo.fieldType, ); }, didReceiveFilter: (FilterPB filter) { @@ -64,9 +63,6 @@ class ChecklistFilterEditorBloc void _startListening() { _listener.start( - onDeleted: () { - if (!isClosed) add(const ChecklistFilterEditorEvent.delete()); - }, onUpdated: (filter) { if (!isClosed) { add(ChecklistFilterEditorEvent.didReceiveFilter(filter)); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart index 12dd22f266..d8ea5906a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart @@ -114,7 +114,7 @@ class GridCreateFilterBloc case FieldType.MultiSelect: return _filterBackendSvc.insertSelectOptionFilter( fieldId: fieldId, - condition: SelectOptionConditionPB.OptionIs, + condition: SelectOptionFilterConditionPB.OptionContains, fieldType: FieldType.MultiSelect, ); case FieldType.Checklist: @@ -130,19 +130,19 @@ class GridCreateFilterBloc case FieldType.RichText: return _filterBackendSvc.insertTextFilter( fieldId: fieldId, - condition: TextFilterConditionPB.Contains, + condition: TextFilterConditionPB.TextContains, content: '', ); case FieldType.SingleSelect: return _filterBackendSvc.insertSelectOptionFilter( fieldId: fieldId, - condition: SelectOptionConditionPB.OptionIs, + condition: SelectOptionFilterConditionPB.OptionIs, fieldType: FieldType.SingleSelect, ); case FieldType.URL: return _filterBackendSvc.insertURLFilter( fieldId: fieldId, - condition: TextFilterConditionPB.Contains, + condition: TextFilterConditionPB.TextContains, ); default: throw UnimplementedError(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart index 08e45305de..cc26e42b83 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart @@ -8,11 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'filter_menu_bloc.freezed.dart'; -class GridFilterMenuBloc - extends Bloc { - GridFilterMenuBloc({required this.viewId, required this.fieldController}) +class DatabaseFilterMenuBloc + extends Bloc { + DatabaseFilterMenuBloc({required this.viewId, required this.fieldController}) : super( - GridFilterMenuState.initial( + DatabaseFilterMenuState.initial( viewId, fieldController.filterInfos, fieldController.fieldInfos, @@ -27,7 +27,7 @@ class GridFilterMenuBloc void Function(List)? _onFieldFn; void _dispatch() { - on( + on( (event, emit) async { event.when( initial: () { @@ -55,11 +55,11 @@ class GridFilterMenuBloc void _startListening() { _onFilterFn = (filters) { - add(GridFilterMenuEvent.didReceiveFilters(filters)); + add(DatabaseFilterMenuEvent.didReceiveFilters(filters)); }; _onFieldFn = (fields) { - add(GridFilterMenuEvent.didReceiveFields(fields)); + add(DatabaseFilterMenuEvent.didReceiveFields(fields)); }; fieldController.addListener( @@ -87,32 +87,33 @@ class GridFilterMenuBloc } @freezed -class GridFilterMenuEvent with _$GridFilterMenuEvent { - const factory GridFilterMenuEvent.initial() = _Initial; - const factory GridFilterMenuEvent.didReceiveFilters( +class DatabaseFilterMenuEvent with _$DatabaseFilterMenuEvent { + const factory DatabaseFilterMenuEvent.initial() = _Initial; + const factory DatabaseFilterMenuEvent.didReceiveFilters( List filters, ) = _DidReceiveFilters; - const factory GridFilterMenuEvent.didReceiveFields(List fields) = - _DidReceiveFields; - const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility; + const factory DatabaseFilterMenuEvent.didReceiveFields( + List fields, + ) = _DidReceiveFields; + const factory DatabaseFilterMenuEvent.toggleMenu() = _SetMenuVisibility; } @freezed -class GridFilterMenuState with _$GridFilterMenuState { - const factory GridFilterMenuState({ +class DatabaseFilterMenuState with _$DatabaseFilterMenuState { + const factory DatabaseFilterMenuState({ required String viewId, required List filters, required List fields, required List creatableFields, required bool isVisible, - }) = _GridFilterMenuState; + }) = _DatabaseFilterMenuState; - factory GridFilterMenuState.initial( + factory DatabaseFilterMenuState.initial( String viewId, List filterInfos, List fields, ) => - GridFilterMenuState( + DatabaseFilterMenuState( viewId: viewId, filters: filterInfos, fields: fields, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart index 5b1de6d782..d68dd17537 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart @@ -59,7 +59,6 @@ class NumberFilterEditorBloc _filterBackendSvc.deleteFilter( fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id, - fieldType: filterInfo.fieldInfo.fieldType, ); }, ); @@ -69,11 +68,6 @@ class NumberFilterEditorBloc void _startListening() { _listener.start( - onDeleted: () { - if (!isClosed) { - add(const NumberFilterEditorEvent.delete()); - } - }, onUpdated: (filter) { if (!isClosed) { add(NumberFilterEditorEvent.didReceiveFilter(filter)); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart index cc92d2d906..3f44cb6d36 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart @@ -38,7 +38,7 @@ class SelectOptionFilterEditorBloc _startListening(); _loadOptions(); }, - updateCondition: (SelectOptionConditionPB condition) { + updateCondition: (SelectOptionFilterConditionPB condition) { _filterBackendSvc.insertSelectOptionFilter( filterId: filterInfo.filter.id, fieldId: filterInfo.fieldInfo.id, @@ -60,7 +60,6 @@ class SelectOptionFilterEditorBloc _filterBackendSvc.deleteFilter( fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id, - fieldType: filterInfo.fieldInfo.fieldType, ); }, didReceiveFilter: (FilterPB filter) { @@ -83,9 +82,6 @@ class SelectOptionFilterEditorBloc void _startListening() { _listener.start( - onDeleted: () { - if (!isClosed) add(const SelectOptionFilterEditorEvent.delete()); - }, onUpdated: (filter) { if (!isClosed) { add(SelectOptionFilterEditorEvent.didReceiveFilter(filter)); @@ -121,7 +117,7 @@ class SelectOptionFilterEditorEvent with _$SelectOptionFilterEditorEvent { FilterPB filter, ) = _DidReceiveFilter; const factory SelectOptionFilterEditorEvent.updateCondition( - SelectOptionConditionPB condition, + SelectOptionFilterConditionPB condition, ) = _UpdateCondition; const factory SelectOptionFilterEditorEvent.updateContent( List optionIds, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart index 6bcc5b8806..84e1284822 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart @@ -1,5 +1,5 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -24,16 +24,19 @@ class SelectOptionFilterListBloc _startListening(); _loadOptions(); }, - selectOption: (option) { - final selectedOptionIds = Set.from(state.selectedOptionIds); - selectedOptionIds.add(option.id); + selectOption: (option, condition) { + final selectedOptionIds = delegate.selectOption( + state.selectedOptionIds, + option.id, + condition, + ); _updateSelectOptions( selectedOptionIds: selectedOptionIds, emit: emit, ); }, - unselectOption: (option) { + unSelectOption: (option) { final selectedOptionIds = Set.from(state.selectedOptionIds); selectedOptionIds.remove(option.id); @@ -116,8 +119,9 @@ class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent { const factory SelectOptionFilterListEvent.initial() = _Initial; const factory SelectOptionFilterListEvent.selectOption( SelectOptionPB option, + SelectOptionFilterConditionPB condition, ) = _SelectOption; - const factory SelectOptionFilterListEvent.unselectOption( + const factory SelectOptionFilterListEvent.unSelectOption( SelectOptionPB option, ) = _UnSelectOption; const factory SelectOptionFilterListEvent.didReceiveOptions( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart index 1c2791d8d0..e4fa67c4a8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart @@ -3,8 +3,7 @@ import 'dart:async'; import 'package:appflowy/plugins/database/domain/filter_listener.dart'; import 'package:appflowy/plugins/database/domain/filter_service.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pbserver.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -12,7 +11,7 @@ part 'text_filter_editor_bloc.freezed.dart'; class TextFilterEditorBloc extends Bloc { - TextFilterEditorBloc({required this.filterInfo}) + TextFilterEditorBloc({required this.filterInfo, required this.fieldType}) : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), _listener = FilterListener( viewId: filterInfo.viewId, @@ -23,6 +22,7 @@ class TextFilterEditorBloc } final FilterInfo filterInfo; + final FieldType fieldType; final FilterBackendService _filterBackendSvc; final FilterListener _listener; @@ -34,26 +34,39 @@ class TextFilterEditorBloc _startListening(); }, updateCondition: (TextFilterConditionPB condition) { - _filterBackendSvc.insertTextFilter( - filterId: filterInfo.filter.id, - fieldId: filterInfo.fieldInfo.id, - condition: condition, - content: state.filter.content, - ); + fieldType == FieldType.RichText + ? _filterBackendSvc.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ) + : _filterBackendSvc.insertURLFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); }, - updateContent: (content) { - _filterBackendSvc.insertTextFilter( - filterId: filterInfo.filter.id, - fieldId: filterInfo.fieldInfo.id, - condition: state.filter.condition, - content: content, - ); + updateContent: (String content) { + fieldType == FieldType.RichText + ? _filterBackendSvc.insertTextFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ) + : _filterBackendSvc.insertURLFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: state.filter.condition, + content: content, + ); }, delete: () { _filterBackendSvc.deleteFilter( fieldId: filterInfo.fieldInfo.id, filterId: filterInfo.filter.id, - fieldType: filterInfo.fieldInfo.fieldType, ); }, didReceiveFilter: (FilterPB filter) { @@ -73,11 +86,10 @@ class TextFilterEditorBloc void _startListening() { _listener.start( - onDeleted: () { - if (!isClosed) add(const TextFilterEditorEvent.delete()); - }, onUpdated: (filter) { - if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter)); + if (!isClosed) { + add(TextFilterEditorEvent.didReceiveFilter(filter)); + } }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart index 74b1dfdced..d33dba6293 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart @@ -1,15 +1,12 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/condition_button.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; - -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart'; -import 'package:flutter/material.dart'; - -import '../../condition_button.dart'; -import '../../filter_info.dart'; +import 'package:flutter/widgets.dart'; class SelectOptionFilterConditionList extends StatelessWidget { const SelectOptionFilterConditionList({ @@ -21,7 +18,7 @@ class SelectOptionFilterConditionList extends StatelessWidget { final FilterInfo filterInfo; final PopoverMutex popoverMutex; - final Function(SelectOptionConditionPB) onCondition; + final Function(SelectOptionFilterConditionPB) onCondition; @override Widget build(BuildContext context) { @@ -30,18 +27,17 @@ class SelectOptionFilterConditionList extends StatelessWidget { asBarrier: true, mutex: popoverMutex, direction: PopoverDirection.bottomWithCenterAligned, - actions: SelectOptionConditionPB.values + actions: _conditionsForFieldType(filterInfo.fieldInfo.fieldType) .map( (action) => ConditionWrapper( action, selectOptionFilter.condition == action, - filterInfo.fieldInfo.fieldType, ), ) .toList(), buildChild: (controller) { return ConditionButton( - conditionName: filterName(selectOptionFilter), + conditionName: selectOptionFilter.condition.i18n, onTap: () => controller.show(), ); }, @@ -52,69 +48,62 @@ class SelectOptionFilterConditionList extends StatelessWidget { ); } - String filterName(SelectOptionFilterPB filter) { - if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) { - return filter.condition.singleSelectFilterName; - } else { - return filter.condition.multiSelectFilterName; - } + List _conditionsForFieldType( + FieldType fieldType, + ) { + // SelectOptionFilterConditionPB.values is not in order + return switch (fieldType) { + FieldType.SingleSelect => [ + SelectOptionFilterConditionPB.OptionIs, + SelectOptionFilterConditionPB.OptionIsNot, + SelectOptionFilterConditionPB.OptionIsEmpty, + SelectOptionFilterConditionPB.OptionIsNotEmpty, + ], + FieldType.MultiSelect => [ + SelectOptionFilterConditionPB.OptionContains, + SelectOptionFilterConditionPB.OptionDoesNotContain, + SelectOptionFilterConditionPB.OptionIs, + SelectOptionFilterConditionPB.OptionIsNot, + SelectOptionFilterConditionPB.OptionIsEmpty, + SelectOptionFilterConditionPB.OptionIsNotEmpty, + ], + _ => [], + }; } } class ConditionWrapper extends ActionCell { - ConditionWrapper(this.inner, this.isSelected, this.fieldType); + ConditionWrapper(this.inner, this.isSelected); - final SelectOptionConditionPB inner; + final SelectOptionFilterConditionPB inner; final bool isSelected; - final FieldType fieldType; @override Widget? rightIcon(Color iconColor) { - if (isSelected) { - return const FlowySvg(FlowySvgs.check_s); - } else { - return null; - } + return isSelected ? const FlowySvg(FlowySvgs.check_s) : null; } @override - String get name { - if (fieldType == FieldType.SingleSelect) { - return inner.singleSelectFilterName; - } else { - return inner.multiSelectFilterName; - } - } + String get name => inner.i18n; } -extension SelectOptionConditionPBExtension on SelectOptionConditionPB { - String get singleSelectFilterName { - switch (this) { - case SelectOptionConditionPB.OptionIs: - return LocaleKeys.grid_singleSelectOptionFilter_is.tr(); - case SelectOptionConditionPB.OptionIsEmpty: - return LocaleKeys.grid_singleSelectOptionFilter_isEmpty.tr(); - case SelectOptionConditionPB.OptionIsNot: - return LocaleKeys.grid_singleSelectOptionFilter_isNot.tr(); - case SelectOptionConditionPB.OptionIsNotEmpty: - return LocaleKeys.grid_singleSelectOptionFilter_isNotEmpty.tr(); - default: - return ""; - } - } - - String get multiSelectFilterName { - switch (this) { - case SelectOptionConditionPB.OptionIs: - return LocaleKeys.grid_multiSelectOptionFilter_contains.tr(); - case SelectOptionConditionPB.OptionIsEmpty: - return LocaleKeys.grid_multiSelectOptionFilter_isEmpty.tr(); - case SelectOptionConditionPB.OptionIsNot: - return LocaleKeys.grid_multiSelectOptionFilter_doesNotContain.tr(); - case SelectOptionConditionPB.OptionIsNotEmpty: - return LocaleKeys.grid_multiSelectOptionFilter_isNotEmpty.tr(); - default: - return ""; - } +extension SelectOptionFilterConditionPBExtension + on SelectOptionFilterConditionPB { + String get i18n { + return switch (this) { + SelectOptionFilterConditionPB.OptionIs => + LocaleKeys.grid_selectOptionFilter_is.tr(), + SelectOptionFilterConditionPB.OptionIsNot => + LocaleKeys.grid_selectOptionFilter_isNot.tr(), + SelectOptionFilterConditionPB.OptionContains => + LocaleKeys.grid_selectOptionFilter_contains.tr(), + SelectOptionFilterConditionPB.OptionDoesNotContain => + LocaleKeys.grid_selectOptionFilter_doesNotContain.tr(), + SelectOptionFilterConditionPB.OptionIsEmpty => + LocaleKeys.grid_selectOptionFilter_isEmpty.tr(), + SelectOptionFilterConditionPB.OptionIsNotEmpty => + LocaleKeys.grid_selectOptionFilter_isNotEmpty.tr(), + _ => "", + }; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart index 5d2406094a..b3c1482453 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart @@ -1,8 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; @@ -88,11 +89,18 @@ class _SelectOptionFilterCellState extends State { if (widget.isSelected) { context .read() - .add(SelectOptionFilterListEvent.unselectOption(widget.option)); + .add(SelectOptionFilterListEvent.unSelectOption(widget.option)); } else { - context - .read() - .add(SelectOptionFilterListEvent.selectOption(widget.option)); + context.read().add( + SelectOptionFilterListEvent.selectOption( + widget.option, + context + .read() + .state + .filter + .condition, + ), + ); } }, children: [ diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart index 6c80c9f01a..24d1955a3f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart @@ -101,9 +101,10 @@ class _SelectOptionFilterEditorState extends State { SliverToBoxAdapter(child: _buildFilterPanel(context, state)), ]; - if (state.filter.condition != SelectOptionConditionPB.OptionIsEmpty && + if (state.filter.condition != + SelectOptionFilterConditionPB.OptionIsEmpty && state.filter.condition != - SelectOptionConditionPB.OptionIsNotEmpty) { + SelectOptionFilterConditionPB.OptionIsNotEmpty) { slivers.add(const SliverToBoxAdapter(child: VSpace(4))); slivers.add( SliverToBoxAdapter( diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart index c36ac8c992..7a7aa25cad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart @@ -1,9 +1,15 @@ import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; abstract class SelectOptionFilterDelegate { List loadOptions(); + + Set selectOption( + Set currentOptionIds, + String optionId, + SelectOptionFilterConditionPB condition, + ); } class SingleSelectOptionFilterDelegateImpl @@ -17,6 +23,35 @@ class SingleSelectOptionFilterDelegateImpl final parser = SingleSelectTypeOptionDataParser(); return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options; } + + @override + Set selectOption( + Set currentOptionIds, + String optionId, + SelectOptionFilterConditionPB condition, + ) { + final selectOptionIds = Set.from(currentOptionIds); + + switch (condition) { + case SelectOptionFilterConditionPB.OptionIs: + if (selectOptionIds.isNotEmpty) { + selectOptionIds.clear(); + } + selectOptionIds.add(optionId); + break; + case SelectOptionFilterConditionPB.OptionIsNot: + selectOptionIds.add(optionId); + break; + case SelectOptionFilterConditionPB.OptionIsEmpty || + SelectOptionFilterConditionPB.OptionIsNotEmpty: + selectOptionIds.clear(); + break; + default: + throw UnimplementedError(); + } + + return selectOptionIds; + } } class MultiSelectOptionFilterDelegateImpl @@ -30,4 +65,12 @@ class MultiSelectOptionFilterDelegateImpl final parser = MultiSelectTypeOptionDataParser(); return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options; } + + @override + Set selectOption( + Set currentOptionIds, + String optionId, + SelectOptionFilterConditionPB condition, + ) => + Set.from(currentOptionIds)..add(optionId); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart index 9c15af4f93..66f17e0971 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart @@ -1,59 +1,44 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; - import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../application/filter/text_filter_editor_bloc.dart'; + import '../condition_button.dart'; import '../disclosure_button.dart'; import '../filter_info.dart'; import 'choicechip.dart'; -class TextFilterChoicechip extends StatefulWidget { +class TextFilterChoicechip extends StatelessWidget { const TextFilterChoicechip({required this.filterInfo, super.key}); final FilterInfo filterInfo; - @override - State createState() => _TextFilterChoicechipState(); -} - -class _TextFilterChoicechipState extends State { - late TextFilterEditorBloc bloc; - - @override - void initState() { - bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo) - ..add(const TextFilterEditorEvent.initial()); - super.initState(); - } - - @override - void dispose() { - bloc.close(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return BlocProvider.value( - value: bloc, + return BlocProvider( + create: (_) => TextFilterEditorBloc( + filterInfo: filterInfo, + fieldType: FieldType.RichText, + )..add(const TextFilterEditorEvent.initial()), child: BlocBuilder( - builder: (blocContext, state) { + builder: (context, state) { return AppFlowyPopover( - controller: PopoverController(), constraints: BoxConstraints.loose(const Size(200, 76)), direction: PopoverDirection.bottomWithCenterAligned, - popupBuilder: (BuildContext context) { - return TextFilterEditor(bloc: bloc); + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: const TextFilterEditor(), + ); }, child: ChoiceChipButton( - filterInfo: widget.filterInfo, + filterInfo: filterInfo, filterDesc: _makeFilterDesc(state), ), ); @@ -78,9 +63,7 @@ class _TextFilterChoicechipState extends State { } class TextFilterEditor extends StatefulWidget { - const TextFilterEditor({required this.bloc, super.key}); - - final TextFilterEditorBloc bloc; + const TextFilterEditor({super.key}); @override State createState() => _TextFilterEditorState(); @@ -91,26 +74,23 @@ class _TextFilterEditorState extends State { @override Widget build(BuildContext context) { - return BlocProvider.value( - value: widget.bloc, - child: BlocBuilder( - builder: (context, state) { - final List children = [ - _buildFilterPanel(context, state), - ]; + return BlocBuilder( + builder: (context, state) { + final List children = [ + _buildFilterPanel(context, state), + ]; - if (state.filter.condition != TextFilterConditionPB.TextIsEmpty && - state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) { - children.add(const VSpace(4)); - children.add(_buildFilterTextField(context, state)); - } + if (state.filter.condition != TextFilterConditionPB.TextIsEmpty && + state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) { + children.add(const VSpace(4)); + children.add(_buildFilterTextField(context, state)); + } - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), - child: IntrinsicHeight(child: Column(children: children)), - ); - }, - ), + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: IntrinsicHeight(child: Column(children: children)), + ); + }, ); } @@ -236,17 +216,17 @@ class ConditionWrapper extends ActionCell { extension TextFilterConditionPBExtension on TextFilterConditionPB { String get filterName { switch (this) { - case TextFilterConditionPB.Contains: + case TextFilterConditionPB.TextContains: return LocaleKeys.grid_textFilter_contains.tr(); - case TextFilterConditionPB.DoesNotContain: + case TextFilterConditionPB.TextDoesNotContain: return LocaleKeys.grid_textFilter_doesNotContain.tr(); - case TextFilterConditionPB.EndsWith: + case TextFilterConditionPB.TextEndsWith: return LocaleKeys.grid_textFilter_endsWith.tr(); - case TextFilterConditionPB.Is: + case TextFilterConditionPB.TextIs: return LocaleKeys.grid_textFilter_is.tr(); - case TextFilterConditionPB.IsNot: + case TextFilterConditionPB.TextIsNot: return LocaleKeys.grid_textFilter_isNot.tr(); - case TextFilterConditionPB.StartsWith: + case TextFilterConditionPB.TextStartsWith: return LocaleKeys.grid_textFilter_startWith.tr(); case TextFilterConditionPB.TextIsEmpty: return LocaleKeys.grid_textFilter_isEmpty.tr(); @@ -259,13 +239,13 @@ extension TextFilterConditionPBExtension on TextFilterConditionPB { String get choicechipPrefix { switch (this) { - case TextFilterConditionPB.DoesNotContain: + case TextFilterConditionPB.TextDoesNotContain: return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr(); - case TextFilterConditionPB.EndsWith: + case TextFilterConditionPB.TextEndsWith: return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr(); - case TextFilterConditionPB.IsNot: + case TextFilterConditionPB.TextIsNot: return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr(); - case TextFilterConditionPB.StartsWith: + case TextFilterConditionPB.TextStartsWith: return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr(); case TextFilterConditionPB.TextIsEmpty: return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart index 440091f24d..53d2b0ace8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart @@ -1,14 +1,58 @@ +import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + import '../filter_info.dart'; import 'choicechip.dart'; -class URLFilterChoicechip extends StatelessWidget { - const URLFilterChoicechip({required this.filterInfo, super.key}); +class URLFilterChoiceChip extends StatelessWidget { + const URLFilterChoiceChip({required this.filterInfo, super.key}); final FilterInfo filterInfo; @override Widget build(BuildContext context) { - return ChoiceChipButton(filterInfo: filterInfo); + return BlocProvider( + create: (_) => TextFilterEditorBloc( + filterInfo: filterInfo, + fieldType: FieldType.URL, + ), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 76)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: const TextFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: filterInfo, + filterDesc: _makeFilterDesc(state), + ), + ); + }, + ), + ); + } + + String _makeFilterDesc(TextFilterEditorState state) { + String filterDesc = state.filter.condition.choicechipPrefix; + if (state.filter.condition == TextFilterConditionPB.TextIsEmpty || + state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) { + return filterDesc; + } + + if (state.filter.content.isNotEmpty) { + filterDesc += " ${state.filter.content}"; + } + + return filterDesc; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart index 97fc590748..19c201d026 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart @@ -18,42 +18,46 @@ class FilterInfo { String get filterId => filter.id; - String get fieldId => filter.fieldId; + String get fieldId => filter.data.fieldId; DateFilterPB? dateFilter() { - return filter.fieldType == FieldType.DateTime - ? DateFilterPB.fromBuffer(filter.data) + final fieldType = filter.data.fieldType; + return fieldType == FieldType.DateTime || + fieldType == FieldType.CreatedTime || + fieldType == FieldType.LastEditedTime + ? DateFilterPB.fromBuffer(filter.data.data) : null; } TextFilterPB? textFilter() { - return filter.fieldType == FieldType.RichText - ? TextFilterPB.fromBuffer(filter.data) + return filter.data.fieldType == FieldType.RichText || + filter.data.fieldType == FieldType.URL + ? TextFilterPB.fromBuffer(filter.data.data) : null; } CheckboxFilterPB? checkboxFilter() { - return filter.fieldType == FieldType.Checkbox - ? CheckboxFilterPB.fromBuffer(filter.data) + return filter.data.fieldType == FieldType.Checkbox + ? CheckboxFilterPB.fromBuffer(filter.data.data) : null; } SelectOptionFilterPB? selectOptionFilter() { - return filter.fieldType == FieldType.SingleSelect || - filter.fieldType == FieldType.MultiSelect - ? SelectOptionFilterPB.fromBuffer(filter.data) + return filter.data.fieldType == FieldType.SingleSelect || + filter.data.fieldType == FieldType.MultiSelect + ? SelectOptionFilterPB.fromBuffer(filter.data.data) : null; } ChecklistFilterPB? checklistFilter() { - return filter.fieldType == FieldType.Checklist - ? ChecklistFilterPB.fromBuffer(filter.data) + return filter.data.fieldType == FieldType.Checklist + ? ChecklistFilterPB.fromBuffer(filter.data.data) : null; } NumberFilterPB? numberFilter() { - return filter.fieldType == FieldType.Number - ? NumberFilterPB.fromBuffer(filter.data) + return filter.data.fieldType == FieldType.Number + ? NumberFilterPB.fromBuffer(filter.data.data) : null; } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart index caca16a1ac..80deb98695 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart @@ -23,14 +23,14 @@ class FilterMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => GridFilterMenuBloc( + return BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: fieldController.viewId, fieldController: fieldController, )..add( - const GridFilterMenuEvent.initial(), + const DatabaseFilterMenuEvent.initial(), ), - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { final List children = []; children.addAll( @@ -115,7 +115,7 @@ class _AddFilterButtonState extends State { triggerActions: PopoverTriggerFlags.none, child: child, popupBuilder: (BuildContext context) { - final bloc = buildContext.read(); + final bloc = buildContext.read(); return GridCreateFilterList( viewId: widget.viewId, fieldController: bloc.fieldController, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart index f661ea57de..3ca86d3969 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart @@ -26,7 +26,7 @@ class FilterMenuItem extends StatelessWidget { FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), FieldType.SingleSelect => SelectOptionFilterChoicechip(filterInfo: filterInfo), - FieldType.URL => URLFilterChoicechip(filterInfo: filterInfo), + FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo), FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), _ => const SizedBox(), }; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart index 2a36f5fece..1a63e3c678 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart @@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -12,7 +13,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../layout/sizes.dart'; -import 'field_editor.dart'; class GridFieldCell extends StatefulWidget { const GridFieldCell({ diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart deleted file mode 100644 index e848200e4b..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; -import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy_backend/dispatch/dispatch.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:protobuf/protobuf.dart'; - -import 'builder.dart'; - -class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { - const RelationTypeOptionEditorFactory(); - - @override - Widget? build({ - required BuildContext context, - required String viewId, - required FieldPB field, - required PopoverMutex popoverMutex, - required TypeOptionDataCallback onTypeOptionUpdated, - }) { - final typeOption = _parseTypeOptionData(field.typeOptionData); - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - padding: const EdgeInsets.only(left: 14, right: 8), - height: GridSize.popoverItemHeight, - alignment: Alignment.centerLeft, - child: FlowyText.regular( - LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(), - color: Theme.of(context).hintColor, - fontSize: 11, - ), - ), - AppFlowyPopover( - mutex: popoverMutex, - triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click, - offset: const Offset(6, 0), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), - height: GridSize.popoverItemHeight, - child: FlowyButton( - text: FlowyText( - typeOption.databaseId.isEmpty - ? LocaleKeys.grid_relation_relatedDatabasePlaceholder.tr() - : typeOption.databaseId, - color: typeOption.databaseId.isEmpty - ? Theme.of(context).hintColor - : null, - overflow: TextOverflow.ellipsis, - ), - rightIcon: const FlowySvg(FlowySvgs.more_s), - ), - ), - popupBuilder: (context) { - return _DatabaseList( - onSelectDatabase: (newDatabaseId) { - final newTypeOption = _updateTypeOption( - typeOption: typeOption, - databaseId: newDatabaseId, - ); - onTypeOptionUpdated(newTypeOption.writeToBuffer()); - PopoverContainer.of(context).close(); - }, - currentDatabaseId: - typeOption.databaseId.isEmpty ? null : typeOption.databaseId, - ); - }, - ), - ], - ); - } - - RelationTypeOptionPB _parseTypeOptionData(List data) { - return RelationTypeOptionDataParser().fromBuffer(data); - } - - RelationTypeOptionPB _updateTypeOption({ - required RelationTypeOptionPB typeOption, - required String databaseId, - }) { - typeOption.freeze(); - return typeOption.rebuild((typeOption) { - typeOption.databaseId = databaseId; - }); - } -} - -class _DatabaseList extends StatefulWidget { - const _DatabaseList({ - required this.onSelectDatabase, - required this.currentDatabaseId, - }); - - final String? currentDatabaseId; - final void Function(String databaseId) onSelectDatabase; - - @override - State<_DatabaseList> createState() => _DatabaseListState(); -} - -class _DatabaseListState extends State<_DatabaseList> { - late Future> future; - - @override - void initState() { - super.initState(); - future = DatabaseEventGetDatabases().send(); - } - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: future, - builder: (context, snapshot) { - final data = snapshot.data; - if (!snapshot.hasData || - snapshot.connectionState != ConnectionState.done || - data!.isFailure()) { - return const SizedBox.shrink(); - } - - final databaseIds = data - .fold>((l) => l.items, (r) => []) - .map((databaseDescription) { - final databaseId = databaseDescription.databaseId; - return FlowyButton( - onTap: () => widget.onSelectDatabase(databaseId), - text: FlowyText.medium( - databaseId, - overflow: TextOverflow.ellipsis, - ), - rightIcon: databaseId == widget.currentDatabaseId - ? FlowySvg( - FlowySvgs.check_s, - color: Theme.of(context).colorScheme.primary, - ) - : null, - ); - }).toList(); - - return ListView.separated( - shrinkWrap: true, - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - itemCount: databaseIds.length, - itemBuilder: (context, index) => databaseIds[index], - ); - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart index 1ecb82e66d..21c61713e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart @@ -23,7 +23,7 @@ class _FilterButtonState extends State { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { final textColor = state.filters.isEmpty ? AFThemeExtension.of(context).textColor @@ -41,11 +41,11 @@ class _FilterButtonState extends State { padding: GridSize.toolbarSettingButtonInsets, radius: Corners.s4Border, onPressed: () { - final bloc = context.read(); + final bloc = context.read(); if (bloc.state.filters.isEmpty) { _popoverController.show(); } else { - bloc.add(const GridFilterMenuEvent.toggleMenu()); + bloc.add(const DatabaseFilterMenuEvent.toggleMenu()); } }, ), @@ -63,14 +63,14 @@ class _FilterButtonState extends State { triggerActions: PopoverTriggerFlags.none, child: child, popupBuilder: (BuildContext context) { - final bloc = buildContext.read(); + final bloc = buildContext.read(); return GridCreateFilterList( viewId: bloc.viewId, fieldController: bloc.fieldController, onClosed: () => _popoverController.close(), onCreateFilter: () { if (!bloc.state.isVisible) { - bloc.add(const GridFilterMenuEvent.toggleMenu()); + bloc.add(const DatabaseFilterMenuEvent.toggleMenu()); } }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart index cd861642bb..312bfd7511 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart @@ -24,11 +24,11 @@ class GridSettingBar extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => GridFilterMenuBloc( + BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: controller.viewId, fieldController: controller.fieldController, - )..add(const GridFilterMenuEvent.initial()), + )..add(const DatabaseFilterMenuEvent.initial()), ), BlocProvider( create: (context) => SortEditorBloc( @@ -37,7 +37,7 @@ class GridSettingBar extends StatelessWidget { ), ), ], - child: BlocListener( + child: BlocListener( listenWhen: (p, c) => p.isVisible != c.isVisible, listener: (context, state) => toggleExtension.toggle(), child: ValueListenableBuilder( diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart index 57a747a631..1595eff658 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/tab_bar_view.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/widgets/share_button.dart'; +import 'package:appflowy/plugins/shared/sync_indicator.dart'; import 'package:appflowy/plugins/util.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; @@ -15,6 +15,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'desktop/tab_bar_header.dart'; @@ -258,6 +259,15 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder { value: bloc, child: Row( children: [ + ...FeatureFlag.syncDatabase.isOn + ? [ + DatabaseSyncIndicator( + key: ValueKey('sync_state_${view.id}'), + view: view, + ), + const HSpace(16), + ] + : [], DatabaseShareButton(key: ValueKey(view.id), view: view), const HSpace(4), ViewFavoriteButton(view: view), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart index b4619a7f47..023048355e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/card_cell_skeleton/relation_card_cell.dart @@ -1,8 +1,9 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -52,15 +53,19 @@ class _RelationCellState extends State { return const SizedBox.shrink(); } - final children = state.rows - .map( - (row) => FlowyText.medium( - row.name, + final children = state.rows.map( + (row) { + final isEmpty = row.name.isEmpty; + return Text( + isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, + style: widget.style.textStyle.copyWith( + color: isEmpty ? Theme.of(context).hintColor : null, decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, ), - ) - .toList(); + overflow: TextOverflow.ellipsis, + ); + }, + ).toList(); return Container( alignment: AlignmentDirectional.topStart, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart index f18717b58d..a2e6c9fa8b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_relation_cell.dart @@ -1,10 +1,12 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/relation.dart'; @@ -29,10 +31,6 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { value: bloc, child: RelationCellEditor( selectedRowIds: state.rows.map((row) => row.rowId).toList(), - databaseId: state.relatedDatabaseId, - onSelectRow: (rowId) { - bloc.add(RelationCellEvent.selectRow(rowId)); - }, ), ); }, @@ -42,15 +40,17 @@ class DesktopGridRelationCellSkin extends IEditableRelationCellSkin { child: Wrap( runSpacing: 4.0, spacing: 4.0, - children: state.rows - .map( - (row) => FlowyText.medium( - row.name, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ) - .toList(), + children: state.rows.map( + (row) { + final isEmpty = row.name.isEmpty; + return FlowyText.medium( + isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, + color: isEmpty ? Theme.of(context).hintColor : null, + decoration: TextDecoration.underline, + overflow: TextOverflow.ellipsis, + ); + }, + ).toList(), ), ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart index f988f65d94..5a76dcdc4d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_select_option_cell.dart @@ -2,11 +2,12 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/widgets.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/select_option.dart'; @@ -16,29 +17,29 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { BuildContext context, CellContainerNotifier cellContainerNotifier, SelectOptionCellBloc bloc, - SelectOptionCellState state, PopoverController popoverController, ) { return AppFlowyPopover( controller: popoverController, - constraints: BoxConstraints.loose(const Size.square(300)), + constraints: const BoxConstraints.tightFor(width: 300), margin: EdgeInsets.zero, direction: PopoverDirection.bottomWithLeftAligned, popupBuilder: (BuildContext popoverContext) { - WidgetsBinding.instance.addPostFrameCallback((_) { - cellContainerNotifier.isFocus = true; - }); return SelectOptionCellEditor( cellController: bloc.cellController, ); }, onClose: () => cellContainerNotifier.isFocus = false, - child: Container( - alignment: AlignmentDirectional.centerStart, - padding: GridSize.cellContentInsets, - child: state.selectedOptions.isEmpty - ? const SizedBox.shrink() - : _buildOptions(context, state.selectedOptions), + child: BlocBuilder( + builder: (context, state) { + return Container( + alignment: AlignmentDirectional.centerStart, + padding: GridSize.cellContentInsets, + child: state.selectedOptions.isEmpty + ? const SizedBox.shrink() + : _buildOptions(context, state.selectedOptions), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart index 5773b68dee..04861c71f8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_grid/desktop_grid_url_cell.dart @@ -115,7 +115,7 @@ class _CopyURLAccessoryState extends State<_CopyURLAccessory> Widget build(BuildContext context) { if (widget.cellDataNotifier.value.isNotEmpty) { return FlowyTooltip( - message: LocaleKeys.tooltip_urlCopyAccessory.tr(), + message: LocaleKeys.grid_url_copy.tr(), preferBelow: false, child: _URLAccessoryIconContainer( child: FlowySvg( @@ -161,7 +161,7 @@ class _VisitURLAccessoryState extends State<_VisitURLAccessory> Widget build(BuildContext context) { if (widget.cellDataNotifier.value.isNotEmpty) { return FlowyTooltip( - message: LocaleKeys.tooltip_urlLaunchAccessory.tr(), + message: LocaleKeys.grid_url_launch.tr(), preferBelow: false, child: _URLAccessoryIconContainer( child: FlowySvg( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart index e545718080..63b70c0f78 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_relation_cell.dart @@ -1,9 +1,12 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/relation_cell_editor.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/relation_cell_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/relation.dart'; @@ -26,36 +29,43 @@ class DesktopRowDetailRelationCellSkin extends IEditableRelationCellSkin { popupBuilder: (context) { return BlocProvider.value( value: bloc, - child: BlocBuilder( - builder: (context, state) => RelationCellEditor( - selectedRowIds: state.rows.map((row) => row.rowId).toList(), - databaseId: state.relatedDatabaseId, - onSelectRow: (rowId) { - context - .read() - .add(RelationCellEvent.selectRow(rowId)); - }, - ), + child: RelationCellEditor( + selectedRowIds: state.rows.map((row) => row.rowId).toList(), ), ); }, child: Container( alignment: AlignmentDirectional.centerStart, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), - child: Wrap( - runSpacing: 4.0, - spacing: 4.0, - children: state.rows - .map( - (row) => FlowyText.medium( - row.name, - decoration: TextDecoration.underline, - overflow: TextOverflow.ellipsis, - ), - ) - .toList(), - ), + child: state.rows.isEmpty + ? _buildPlaceholder(context) + : _buildRows(context, state.rows), ), ); } + + Widget _buildPlaceholder(BuildContext context) { + return FlowyText( + LocaleKeys.grid_row_textPlaceholder.tr(), + color: Theme.of(context).hintColor, + ); + } + + Widget _buildRows(BuildContext context, List rows) { + return Wrap( + runSpacing: 4.0, + spacing: 4.0, + children: rows.map( + (row) { + final isEmpty = row.name.isEmpty; + return FlowyText.medium( + isEmpty ? LocaleKeys.grid_row_titlePlaceholder.tr() : row.name, + color: isEmpty ? Theme.of(context).hintColor : null, + decoration: TextDecoration.underline, + overflow: TextOverflow.ellipsis, + ); + }, + ).toList(), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart index 45ddc303b2..0e8c6fdffa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_select_option_cell.dart @@ -2,12 +2,13 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/select_option.dart'; @@ -18,12 +19,11 @@ class DesktopRowDetailSelectOptionCellSkin BuildContext context, CellContainerNotifier cellContainerNotifier, SelectOptionCellBloc bloc, - SelectOptionCellState state, PopoverController popoverController, ) { return AppFlowyPopover( controller: popoverController, - constraints: BoxConstraints.loose(const Size.square(300)), + constraints: const BoxConstraints.tightFor(width: 300), margin: EdgeInsets.zero, direction: PopoverDirection.bottomWithLeftAligned, popupBuilder: (BuildContext popoverContext) { @@ -35,14 +35,18 @@ class DesktopRowDetailSelectOptionCellSkin ); }, onClose: () => cellContainerNotifier.isFocus = false, - child: Container( - alignment: AlignmentDirectional.centerStart, - padding: state.selectedOptions.isEmpty - ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0) - : const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0), - child: state.selectedOptions.isEmpty - ? _buildPlaceholder(context) - : _buildOptions(context, state.selectedOptions), + child: BlocBuilder( + builder: (context, state) { + return Container( + alignment: AlignmentDirectional.centerStart, + padding: state.selectedOptions.isEmpty + ? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0) + : const EdgeInsets.symmetric(horizontal: 8.0, vertical: 5.0), + child: state.selectedOptions.isEmpty + ? _buildPlaceholder(context) + : _buildOptions(context, state.selectedOptions), + ); + }, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart index de3908e585..32df29a60a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart @@ -32,7 +32,6 @@ abstract class IEditableSelectOptionCellSkin { BuildContext context, CellContainerNotifier cellContainerNotifier, SelectOptionCellBloc bloc, - SelectOptionCellState state, PopoverController popoverController, ); } @@ -77,16 +76,11 @@ class _SelectOptionCellState extends GridCellState { Widget build(BuildContext context) { return BlocProvider.value( value: cellBloc, - child: BlocBuilder( - builder: (context, state) { - return widget.skin.build( - context, - widget.cellContainerNotifier, - cellBloc, - state, - _popover, - ); - }, + child: widget.skin.build( + context, + widget.cellContainerNotifier, + cellBloc, + _popover, ), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart index 6244c54072..3628f6c511 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/url.dart @@ -1,5 +1,9 @@ import 'dart:async'; +import 'dart:io'; +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; @@ -8,17 +12,19 @@ import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.d import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:go_router/go_router.dart'; import '../desktop_grid/desktop_grid_url_cell.dart'; import '../desktop_row_detail/desktop_row_detail_url_cell.dart'; import '../mobile_grid/mobile_grid_url_cell.dart'; import '../mobile_row_detail/mobile_row_detail_url_cell.dart'; -const regexUrl = - r"[(http(s)?):\/\/(www\.)?a-zA-Z0-9@:._\+-~#=]{2,256}\.[a-z]{2,6}\b([-a-zA-Z0-9@:_\+.~#?&\/\/=]*)"; - abstract class IEditableURLCellSkin { const IEditableURLCellSkin(); @@ -106,7 +112,8 @@ class _GridURLCellState extends GridEditableTextCell { child: BlocListener( listenWhen: (previous, current) => previous.content != current.content, listener: (context, state) { - _textEditingController.text = state.content; + _textEditingController.value = + _textEditingController.value.copyWith(text: state.content); widget._cellDataNotifier.value = state.content; }, child: widget.skin.build( @@ -125,8 +132,8 @@ class _GridURLCellState extends GridEditableTextCell { Future focusChanged() async { if (mounted && !cellBloc.isClosed && - cellBloc.state.content != _textEditingController.text.trim()) { - cellBloc.add(URLCellEvent.updateURL(_textEditingController.text.trim())); + cellBloc.state.content != _textEditingController.text) { + cellBloc.add(URLCellEvent.updateURL(_textEditingController.text)); } return super.focusChanged(); } @@ -135,8 +142,82 @@ class _GridURLCellState extends GridEditableTextCell { String? onCopy() => cellBloc.state.content; } -void openUrlCellLink(String content) { - if (RegExp(regexUrl).hasMatch(content)) { +class MobileURLEditor extends StatelessWidget { + const MobileURLEditor({ + super.key, + required this.textEditingController, + }); + + final TextEditingController textEditingController; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + const VSpace(4.0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: FlowyTextField( + controller: textEditingController, + hintStyle: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).hintColor), + hintText: LocaleKeys.grid_url_textFieldHint.tr(), + textStyle: Theme.of(context).textTheme.bodyMedium, + keyboardType: TextInputType.url, + hintTextConstraints: const BoxConstraints(maxHeight: 52), + error: context.watch().state.isValid + ? null + : const SizedBox.shrink(), + onChanged: (_) { + if (textEditingController.value.composing.isCollapsed) { + context + .read() + .add(URLCellEvent.updateURL(textEditingController.text)); + } + }, + onSubmitted: (text) => + context.read().add(URLCellEvent.updateURL(text)), + ), + ), + const VSpace(8.0), + MobileQuickActionButton( + enable: context.watch().state.content.isNotEmpty, + onTap: () { + openUrlCellLink(textEditingController.text); + context.pop(); + }, + icon: FlowySvgs.url_s, + text: LocaleKeys.grid_url_launch.tr(), + ), + const Divider(height: 8.5, thickness: 0.5), + MobileQuickActionButton( + enable: context.watch().state.content.isNotEmpty, + onTap: () { + Clipboard.setData( + ClipboardData(text: textEditingController.text), + ); + Fluttertoast.showToast( + msg: LocaleKeys.grid_url_copiedNotification.tr(), + gravity: ToastGravity.BOTTOM, + ); + context.pop(); + }, + icon: FlowySvgs.copy_s, + text: LocaleKeys.grid_url_copy.tr(), + ), + const Divider(height: 8.5, thickness: 0.5), + ], + ); + } +} + +void openUrlCellLink(String content) async { + String url = ""; + + try { + // check protocol is provided const linkPrefix = [ 'http://', 'https://', @@ -147,11 +228,15 @@ void openUrlCellLink(String content) { ]; final shouldAddScheme = !linkPrefix.any((pattern) => content.startsWith(pattern)); - final url = shouldAddScheme ? 'https://$content' : content; - afLaunchUrlString(url); - } else { - afLaunchUrlString( - "https://www.google.com/search?q=${Uri.encodeComponent(content)}", - ); + url = shouldAddScheme ? 'http://$content' : content; + + // get hostname and check validity + final uri = Uri.parse(url); + final hostName = uri.host; + await InternetAddress.lookup(hostName); + } catch (_) { + url = "https://www.google.com/search?q=${Uri.encodeComponent(content)}"; + } finally { + await afLaunchUrlString(url); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart index 95d7a41737..9c01536e71 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_select_option_cell.dart @@ -8,6 +8,7 @@ import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/select_option.dart'; @@ -17,25 +18,28 @@ class MobileGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin { BuildContext context, CellContainerNotifier cellContainerNotifier, SelectOptionCellBloc bloc, - SelectOptionCellState state, PopoverController popoverController, ) { - return FlowyButton( - hoverColor: Colors.transparent, - radius: BorderRadius.zero, - margin: EdgeInsets.zero, - text: Align( - alignment: AlignmentDirectional.centerStart, - child: state.selectedOptions.isEmpty - ? const SizedBox.shrink() - : _buildOptions(context, state.selectedOptions), - ), - onTap: () { - showMobileBottomSheet( - context, - builder: (context) { - return MobileSelectOptionEditor( - cellController: bloc.cellController, + return BlocBuilder( + builder: (context, state) { + return FlowyButton( + hoverColor: Colors.transparent, + radius: BorderRadius.zero, + margin: EdgeInsets.zero, + text: Align( + alignment: AlignmentDirectional.centerStart, + child: state.selectedOptions.isEmpty + ? const SizedBox.shrink() + : _buildOptions(context, state.selectedOptions), + ), + onTap: () { + showMobileBottomSheet( + context, + builder: (context) { + return MobileSelectOptionEditor( + cellController: bloc.cellController, + ); + }, ); }, ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart index 6705a7d776..4dffae3022 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_grid/mobile_grid_url_cell.dart @@ -1,14 +1,9 @@ -import 'package:flutter/material.dart'; - -import 'package:appflowy/core/helpers/url_launcher.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; -import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import '../editable_cell_skeleton/url.dart'; @@ -25,53 +20,9 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { return BlocSelector( selector: (state) => state.content, builder: (context, content) { - if (content.isEmpty) { - return TextField( - focusNode: focusNode, - keyboardType: TextInputType.url, - decoration: const InputDecoration( - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - contentPadding: EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - isCollapsed: true, - ), - onTapOutside: (event) => - FocusManager.instance.primaryFocus?.unfocus(), - onSubmitted: (value) => bloc.add(URLCellEvent.updateURL(value)), - ); - } - return GestureDetector( - onTap: () { - if (content.isEmpty) { - return; - } - final shouldAddScheme = !['http', 'https'] - .any((pattern) => content.startsWith(pattern)); - final url = shouldAddScheme ? 'http://$content' : content; - afLaunchUrlString(url); - }, - onLongPress: () => showMobileBottomSheet( - context, - title: LocaleKeys.board_mobile_editURL.tr(), - showHeader: true, - showCloseButton: true, - builder: (_) { - final controller = TextEditingController(text: content); - return TextField( - controller: controller, - autofocus: true, - keyboardType: TextInputType.url, - onEditingComplete: () { - bloc.add(URLCellEvent.updateURL(controller.text)); - context.pop(); - }, - ); - }, - ), + onTap: () => _showURLEditor(context, bloc, textEditingController), + behavior: HitTestBehavior.opaque, child: Container( alignment: AlignmentDirectional.centerStart, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), @@ -92,6 +43,24 @@ class MobileGridURLCellSkin extends IEditableURLCellSkin { ); } + void _showURLEditor( + BuildContext context, + URLCellBloc bloc, + TextEditingController textEditingController, + ) { + showMobileBottomSheet( + context, + showDragHandle: true, + backgroundColor: Theme.of(context).colorScheme.background, + builder: (context) => BlocProvider.value( + value: bloc, + child: MobileURLEditor( + textEditingController: textEditingController, + ), + ), + ); + } + @override List>> accessoryBuilder( GridCellAccessoryBuildContext context, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart index 6266e28022..59941394fa 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_select_cell_option.dart @@ -10,6 +10,7 @@ import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import '../editable_cell_skeleton/select_option.dart'; @@ -20,53 +21,56 @@ class MobileRowDetailSelectOptionCellSkin BuildContext context, CellContainerNotifier cellContainerNotifier, SelectOptionCellBloc bloc, - SelectOptionCellState state, PopoverController popoverController, ) { - return InkWell( - borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () => showMobileBottomSheet( - context, - builder: (context) { - return MobileSelectOptionEditor( - cellController: bloc.cellController, - ); - }, - ), - child: Container( - constraints: const BoxConstraints( - minHeight: 48, - minWidth: double.infinity, - ), - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: state.selectedOptions.isEmpty ? 13 : 10, - ), - decoration: BoxDecoration( - border: Border.fromBorderSide( - BorderSide(color: Theme.of(context).colorScheme.outline), - ), + return BlocBuilder( + builder: (context, state) { + return InkWell( borderRadius: const BorderRadius.all(Radius.circular(14)), - ), - child: Row( - children: [ - Expanded( - child: state.selectedOptions.isEmpty - ? _buildPlaceholder(context) - : _buildOptions(context, state.selectedOptions), + onTap: () => showMobileBottomSheet( + context, + builder: (context) { + return MobileSelectOptionEditor( + cellController: bloc.cellController, + ); + }, + ), + child: Container( + constraints: const BoxConstraints( + minHeight: 48, + minWidth: double.infinity, ), - const HSpace(6), - RotatedBox( - quarterTurns: 3, - child: Icon( - Icons.chevron_left, - color: Theme.of(context).hintColor, + padding: EdgeInsets.symmetric( + horizontal: 12, + vertical: state.selectedOptions.isEmpty ? 13 : 10, + ), + decoration: BoxDecoration( + border: Border.fromBorderSide( + BorderSide(color: Theme.of(context).colorScheme.outline), ), + borderRadius: const BorderRadius.all(Radius.circular(14)), ), - const HSpace(2), - ], - ), - ), + child: Row( + children: [ + Expanded( + child: state.selectedOptions.isEmpty + ? _buildPlaceholder(context) + : _buildOptions(context, state.selectedOptions), + ), + const HSpace(6), + RotatedBox( + quarterTurns: 3, + child: Icon( + Icons.chevron_left, + color: Theme.of(context).hintColor, + ), + ), + const HSpace(2), + ], + ), + ), + ); + }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart index ec4dc11826..f97eabe830 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_url_cell.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:appflowy/core/helpers/url_launcher.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; import 'package:appflowy/plugins/database/application/cell/bloc/url_cell_bloc.dart'; @@ -8,7 +7,6 @@ import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.d import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:go_router/go_router.dart'; import '../editable_cell_skeleton/url.dart'; @@ -27,17 +25,19 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { builder: (context, content) { return InkWell( borderRadius: const BorderRadius.all(Radius.circular(14)), - onTap: () { - if (content.isEmpty) { - _showURLEditor(context, bloc, content); - return; - } - final shouldAddScheme = !['http', 'https'] - .any((pattern) => content.startsWith(pattern)); - final url = shouldAddScheme ? 'http://$content' : content; - afLaunchUrlString(url); - }, - onLongPress: () => _showURLEditor(context, bloc, content), + onTap: () => showMobileBottomSheet( + context, + showDragHandle: true, + backgroundColor: Theme.of(context).colorScheme.background, + builder: (_) { + return BlocProvider.value( + value: bloc, + child: MobileURLEditor( + textEditingController: textEditingController, + ), + ); + }, + ), child: Container( constraints: const BoxConstraints( minHeight: 48, @@ -77,25 +77,4 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { URLCellDataNotifier cellDataNotifier, ) => const []; - - void _showURLEditor(BuildContext context, URLCellBloc bloc, String content) { - final controller = TextEditingController(text: content); - showMobileBottomSheet( - context, - title: LocaleKeys.board_mobile_editURL.tr(), - showHeader: true, - showCloseButton: true, - builder: (_) { - return TextField( - controller: controller, - autofocus: true, - keyboardType: TextInputType.url, - onEditingComplete: () { - bloc.add(URLCellEvent.updateURL(controller.text)); - context.pop(); - }, - ); - }, - ).then((_) => controller.dispose()); - } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart index c68d39492a..fe9921b4d5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/extension.dart @@ -33,7 +33,7 @@ extension SelectOptionColorExtension on SelectOptionColorPB { } } - String optionName() { + String colorName() { switch (this) { case SelectOptionColorPB.Purple: return LocaleKeys.grid_selectOption_purpleColor.tr(); @@ -123,44 +123,3 @@ class SelectOptionTag extends StatelessWidget { } } -class SelectOptionTagCell extends StatelessWidget { - const SelectOptionTagCell({ - super.key, - required this.option, - required this.onSelected, - this.children = const [], - }); - - final SelectOptionPB option; - final VoidCallback onSelected; - final List children; - - @override - Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Expanded( - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: onSelected, - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 5.0, - vertical: 4.0, - ), - child: SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric(horizontal: 8), - ), - ), - ), - ), - ), - ...children, - ], - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart index f1c8b424f5..81ecfe28e6 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/mobile_select_option_editor.dart @@ -5,7 +5,7 @@ import 'package:appflowy/mobile/presentation/base/option_color_list.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/plugins/base/drag_handler.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; @@ -55,8 +55,9 @@ class _MobileSelectOptionEditorState extends State { child: BlocProvider( create: (context) => SelectOptionCellEditorBloc( cellController: widget.cellController, - )..add(const SelectOptionEditorEvent.initial()), - child: BlocBuilder( + ), + child: BlocBuilder( builder: (context, state) { return Column( mainAxisSize: MainAxisSize.min, @@ -110,7 +111,7 @@ class _MobileSelectOptionEditorState extends State { onDelete: () { context .read() - .add(SelectOptionEditorEvent.deleteOption(option!)); + .add(SelectOptionCellEditorEvent.deleteOption(option!)); _popOrBack(); }, onUpdate: (name, color) { @@ -120,7 +121,7 @@ class _MobileSelectOptionEditorState extends State { } option.freeze(); context.read().add( - SelectOptionEditorEvent.updateOption( + SelectOptionCellEditorEvent.updateOption( option.rebuild((p0) { if (name != null) { p0.name = name; @@ -142,16 +143,16 @@ class _MobileSelectOptionEditorState extends State { _SearchField( controller: searchController, hintText: LocaleKeys.grid_selectOption_searchOrCreateOption.tr(), - onSubmitted: (option) { + onSubmitted: (_) { context .read() - .add(SelectOptionEditorEvent.trySelectOption(option)); + .add(const SelectOptionCellEditorEvent.submitTextField()); searchController.clear(); }, onChanged: (value) { typingOption = value; context.read().add( - SelectOptionEditorEvent.selectMultipleOptions( + SelectOptionCellEditorEvent.selectMultipleOptions( [], value, ), @@ -164,18 +165,18 @@ class _MobileSelectOptionEditorState extends State { onCreateOption: (optionName) { context .read() - .add(SelectOptionEditorEvent.newOption(optionName)); + .add(const SelectOptionCellEditorEvent.createOption()); searchController.clear(); }, onCheck: (option, value) { if (value) { context .read() - .add(SelectOptionEditorEvent.selectOption(option.id)); + .add(SelectOptionCellEditorEvent.selectOption(option.id)); } else { context .read() - .add(SelectOptionEditorEvent.unSelectOption(option.id)); + .add(SelectOptionCellEditorEvent.unSelectOption(option.id)); } }, onMoreOptions: (option) { @@ -253,18 +254,20 @@ class _OptionList extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { // existing options final List cells = []; // create an option cell - final createOption = state.createOption; - if (createOption != null) { + if (state.createSelectOptionSuggestion != null) { cells.add( _CreateOptionCell( - optionName: createOption, - onTap: () => onCreateOption(createOption), + name: state.createSelectOptionSuggestion!.name, + color: state.createSelectOptionSuggestion!.color, + onTap: () => onCreateOption( + state.createSelectOptionSuggestion!.name, + ), ), ); } @@ -332,14 +335,17 @@ class _SelectOption extends StatelessWidget { const HSpace(12), // option tag Expanded( - child: SelectOptionTag( - option: option, - padding: const EdgeInsets.symmetric( - vertical: 10, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: SelectOptionTag( + option: option, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 14, + ), + textAlign: TextAlign.center, + fontSize: 15.0, ), - textAlign: TextAlign.center, - fontSize: 15.0, - isExpanded: true, ), ), const HSpace(24), @@ -359,11 +365,13 @@ class _SelectOption extends StatelessWidget { class _CreateOptionCell extends StatelessWidget { const _CreateOptionCell({ - required this.optionName, + required this.name, + required this.color, required this.onTap, }); - final String optionName; + final String name; + final SelectOptionColorPB color; final VoidCallback onTap; @override @@ -381,13 +389,16 @@ class _CreateOptionCell extends StatelessWidget { ), const HSpace(8), Expanded( - child: SelectOptionTag( - isExpanded: true, - name: optionName, - color: Theme.of(context).colorScheme.surfaceVariant, - textAlign: TextAlign.center, - padding: const EdgeInsets.symmetric( - vertical: 10, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: SelectOptionTag( + name: name, + color: color.toColor(context), + textAlign: TextAlign.center, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 14, + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart index 18182c6fb3..d3bf428ed8 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/relation_cell_editor.dart @@ -1,5 +1,6 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -13,38 +14,24 @@ import '../../application/cell/bloc/relation_row_search_bloc.dart'; class RelationCellEditor extends StatelessWidget { const RelationCellEditor({ super.key, - required this.databaseId, required this.selectedRowIds, - required this.onSelectRow, }); - final String databaseId; final List selectedRowIds; - final void Function(String rowId) onSelectRow; @override Widget build(BuildContext context) { - if (databaseId.isEmpty) { - // no i18n here because UX needs thorough checking. - return const Center( - child: FlowyText( - ''' -No database has been selected, -please select one first in the field editor. - ''', - maxLines: null, - textAlign: TextAlign.center, - ), - ); - } + return BlocBuilder( + builder: (context, cellState) { + if (cellState.relatedDatabaseMeta == null) { + return const _RelationCellEditorDatabaseList(); + } - return BlocProvider( - create: (context) => RelationRowSearchBloc( - databaseId: databaseId, - ), - child: BlocBuilder( - builder: (context, cellState) { - return BlocBuilder( + return BlocProvider( + create: (context) => RelationRowSearchBloc( + databaseId: cellState.relatedDatabaseMeta!.databaseId, + ), + child: BlocBuilder( builder: (context, state) { final children = state.filteredRows .map( @@ -63,12 +50,13 @@ please select one first in the field editor. rightIcon: cellState.rows .map((e) => e.rowId) .contains(row.rowId) - ? FlowySvg( + ? const FlowySvg( FlowySvgs.check_s, - color: Theme.of(context).primaryColor, ) : null, - onTap: () => onSelectRow(row.rowId), + onTap: () => context + .read() + .add(RelationCellEvent.selectRow(row.rowId)), ), ), ) @@ -78,7 +66,6 @@ please select one first in the field editor. mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const VSpace(6.0), Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0) + GridSize.typeOptionContentInsets, @@ -90,15 +77,13 @@ please select one first in the field editor. fontSize: 11, color: Theme.of(context).hintColor, ), - const HSpace(2.0), - FlowyButton( - useIntrinsicWidth: true, - margin: const EdgeInsets.symmetric( + Padding( + padding: const EdgeInsets.symmetric( horizontal: 4, vertical: 2, ), - text: FlowyText.regular( - cellState.relatedDatabaseId, + child: FlowyText.regular( + cellState.relatedDatabaseMeta!.databaseName, fontSize: 11, overflow: TextOverflow.ellipsis, ), @@ -106,10 +91,16 @@ please select one first in the field editor. ], ), ), - VSpace(GridSize.typeOptionSeparatorHeight), Padding( padding: const EdgeInsets.symmetric(horizontal: 6.0), child: FlowyTextField( + hintText: LocaleKeys + .grid_relation_rowSearchTextFieldPlaceholder + .tr(), + hintStyle: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: Theme.of(context).hintColor), onChanged: (text) => context .read() .add(RelationRowSearchEvent.updateFilter(text)), @@ -140,6 +131,62 @@ please select one first in the field editor. ], ); }, + ), + ); + }, + ); + } +} + +class _RelationCellEditorDatabaseList extends StatelessWidget { + const _RelationCellEditorDatabaseList(); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => RelationDatabaseListCubit(), + child: BlocBuilder( + builder: (context, state) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(6, 6, 6, 0), + child: FlowyText( + LocaleKeys.grid_relation_noDatabaseSelected.tr(), + maxLines: null, + fontSize: 10, + color: Theme.of(context).hintColor, + ), + ), + Flexible( + child: ListView.separated( + padding: const EdgeInsets.all(6), + separatorBuilder: (context, index) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: state.databaseMetas.length, + shrinkWrap: true, + itemBuilder: (context, index) { + final databaseMeta = state.databaseMetas[index]; + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + onTap: () => context.read().add( + RelationCellEvent.selectDatabaseId( + databaseMeta.databaseId, + ), + ), + text: FlowyText.medium( + databaseMeta.databaseName, + overflow: TextOverflow.ellipsis, + ), + ), + ); + }, + ), + ), + ], ); }, ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart new file mode 100644 index 0000000000..7f76cb26b3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_cell_editor.dart @@ -0,0 +1,554 @@ +import 'dart:collection'; +import 'dart:io'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; +import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../grid/presentation/layout/sizes.dart'; +import '../../grid/presentation/widgets/common/type_option_separator.dart'; +import '../field/type_option_editor/select/select_option_editor.dart'; +import 'extension.dart'; +import 'select_option_text_field.dart'; + +const double _editorPanelWidth = 300; + +class SelectOptionCellEditor extends StatefulWidget { + const SelectOptionCellEditor({super.key, required this.cellController}); + + final SelectOptionCellController cellController; + + @override + State createState() => _SelectOptionCellEditorState(); +} + +class _SelectOptionCellEditorState extends State { + final TextEditingController textEditingController = TextEditingController(); + final popoverMutex = PopoverMutex(); + late final bloc = SelectOptionCellEditorBloc( + cellController: widget.cellController, + ); + late final FocusNode focusNode; + + @override + void initState() { + super.initState(); + focusNode = FocusNode( + onKeyEvent: (node, event) { + switch (event.logicalKey) { + case LogicalKeyboardKey.arrowUp when event is! KeyUpEvent: + if (textEditingController.value.composing.isCollapsed) { + bloc.add(const SelectOptionCellEditorEvent.focusPreviousOption()); + return KeyEventResult.handled; + } + break; + case LogicalKeyboardKey.arrowDown when event is! KeyUpEvent: + if (textEditingController.value.composing.isCollapsed) { + bloc.add(const SelectOptionCellEditorEvent.focusNextOption()); + return KeyEventResult.handled; + } + break; + case LogicalKeyboardKey.escape when event is! KeyUpEvent: + if (!textEditingController.value.composing.isCollapsed) { + final end = textEditingController.value.composing.end; + final text = textEditingController.text; + + textEditingController.value = TextEditingValue( + text: text, + selection: TextSelection.collapsed(offset: end), + ); + return KeyEventResult.handled; + } + break; + case LogicalKeyboardKey.backspace when event is KeyUpEvent: + if (!textEditingController.text.isNotEmpty) { + bloc.add(const SelectOptionCellEditorEvent.unSelectLastOption()); + return KeyEventResult.handled; + } + break; + } + return KeyEventResult.ignored; + }, + ); + } + + @override + void dispose() { + popoverMutex.dispose(); + textEditingController.dispose(); + bloc.close(); + focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider.value( + value: bloc, + child: TextFieldTapRegion( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _TextField( + textEditingController: textEditingController, + focusNode: focusNode, + popoverMutex: popoverMutex, + ), + const TypeOptionSeparator(spacing: 0.0), + Flexible( + child: Focus( + descendantsAreFocusable: false, + child: _OptionList( + textEditingController: textEditingController, + popoverMutex: popoverMutex, + ), + ), + ), + ], + ), + ), + ); + } +} + +class _OptionList extends StatelessWidget { + const _OptionList({ + required this.textEditingController, + required this.popoverMutex, + }); + + final TextEditingController textEditingController; + final PopoverMutex popoverMutex; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listenWhen: (previous, current) => + previous.clearFilter != current.clearFilter, + listener: (context, state) { + if (state.clearFilter) { + textEditingController.clear(); + context + .read() + .add(const SelectOptionCellEditorEvent.resetClearFilterFlag()); + } + }, + buildWhen: (previous, current) => + !listEquals(previous.options, current.options) || + previous.createSelectOptionSuggestion != + current.createSelectOptionSuggestion, + builder: (context, state) { + return ReorderableListView.builder( + shrinkWrap: true, + proxyDecorator: (child, index, _) => Material( + color: Colors.transparent, + child: Stack( + children: [ + BlocProvider.value( + value: context.read(), + child: child, + ), + MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: const SizedBox.expand(), + ), + ], + ), + ), + buildDefaultDragHandles: false, + itemCount: state.options.length, + onReorderStart: (_) => popoverMutex.close(), + itemBuilder: (_, int index) { + final option = state.options[index]; + return _SelectOptionCell( + key: ValueKey("select_cell_option_list_${option.id}"), + index: index, + option: option, + popoverMutex: popoverMutex, + ); + }, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex--; + } + final fromOptionId = state.options[oldIndex].id; + final toOptionId = state.options[newIndex].id; + context.read().add( + SelectOptionCellEditorEvent.reorderOption( + fromOptionId, + toOptionId, + ), + ); + }, + header: const _Title(), + footer: state.createSelectOptionSuggestion == null + ? null + : _CreateOptionCell( + suggestion: state.createSelectOptionSuggestion!, + ), + padding: const EdgeInsets.symmetric(vertical: 8.0), + ); + }, + ); + } +} + +class _TextField extends StatelessWidget { + const _TextField({ + required this.textEditingController, + required this.focusNode, + required this.popoverMutex, + }); + + final TextEditingController textEditingController; + final FocusNode focusNode; + final PopoverMutex popoverMutex; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final optionMap = LinkedHashMap.fromIterable( + state.selectedOptions, + key: (option) => option.name, + value: (option) => option, + ); + + return Material( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: SelectOptionTextField( + options: state.options, + focusNode: focusNode, + selectedOptionMap: optionMap, + distanceToText: _editorPanelWidth * 0.7, + textController: textEditingController, + textSeparators: const [','], + onClick: () => popoverMutex.close(), + newText: (text) { + context + .read() + .add(SelectOptionCellEditorEvent.filterOption(text)); + }, + onSubmitted: () { + context + .read() + .add(const SelectOptionCellEditorEvent.submitTextField()); + focusNode.requestFocus(); + }, + onPaste: (tagNames, remainder) { + context.read().add( + SelectOptionCellEditorEvent.selectMultipleOptions( + tagNames, + remainder, + ), + ); + }, + onRemove: (optionName) { + context.read().add( + SelectOptionCellEditorEvent.unSelectOption( + optionMap[optionName]!.id, + ), + ); + }, + ), + ), + ); + }, + ); + } +} + +class _Title extends StatelessWidget { + const _Title(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyText.regular( + LocaleKeys.grid_selectOption_panelTitle.tr(), + color: Theme.of(context).hintColor, + ), + ), + ); + } +} + +class _SelectOptionCell extends StatefulWidget { + const _SelectOptionCell({ + super.key, + required this.option, + required this.index, + required this.popoverMutex, + }); + + final SelectOptionPB option; + final int index; + final PopoverMutex popoverMutex; + + @override + State<_SelectOptionCell> createState() => _SelectOptionCellState(); +} + +class _SelectOptionCellState extends State<_SelectOptionCell> { + late PopoverController _popoverController; + + @override + void initState() { + _popoverController = PopoverController(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return AppFlowyPopover( + controller: _popoverController, + offset: const Offset(8, 0), + margin: EdgeInsets.zero, + asBarrier: true, + constraints: BoxConstraints.loose(const Size(200, 470)), + mutex: widget.popoverMutex, + clickHandler: PopoverClickHandler.gestureDetector, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 2.0), + child: MouseRegion( + onEnter: (_) { + context.read().add( + SelectOptionCellEditorEvent.updateFocusedOption( + widget.option.id, + ), + ); + }, + child: Container( + height: 28, + decoration: BoxDecoration( + color: context + .watch() + .state + .focusedOptionId == + widget.option.id + ? AFThemeExtension.of(context).lightGreyHover + : null, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: SelectOptionTagCell( + option: widget.option, + index: widget.index, + onSelected: _onTap, + children: [ + if (context + .watch() + .state + .selectedOptions + .contains(widget.option)) + FlowyIconButton( + width: 20, + hoverColor: Colors.transparent, + onPressed: _onTap, + icon: FlowySvg( + FlowySvgs.check_s, + color: Theme.of(context).iconTheme.color, + ), + ), + FlowyIconButton( + onPressed: () => _popoverController.show(), + iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), + hoverColor: Colors.transparent, + icon: FlowySvg( + FlowySvgs.three_dots_s, + size: const Size.square(16), + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ], + ), + ), + ), + ), + popupBuilder: (BuildContext popoverContext) { + return SelectOptionEditor( + option: widget.option, + onDeleted: () { + context + .read() + .add(SelectOptionCellEditorEvent.deleteOption(widget.option)); + PopoverContainer.of(popoverContext).close(); + }, + onUpdated: (updatedOption) { + context + .read() + .add(SelectOptionCellEditorEvent.updateOption(updatedOption)); + }, + key: ValueKey( + widget.option.id, + ), // Use ValueKey to refresh the UI, otherwise, it will remain the old value. + ); + }, + ); + } + + void _onTap() { + widget.popoverMutex.close(); + if (context + .read() + .state + .selectedOptions + .contains(widget.option)) { + context + .read() + .add(SelectOptionCellEditorEvent.unSelectOption(widget.option.id)); + } else { + context + .read() + .add(SelectOptionCellEditorEvent.selectOption(widget.option.id)); + } + } +} + +class SelectOptionTagCell extends StatelessWidget { + const SelectOptionTagCell({ + super.key, + required this.option, + required this.onSelected, + this.children = const [], + this.index, + }); + + final SelectOptionPB option; + final VoidCallback onSelected; + final List children; + final int? index; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (index != null) + ReorderableDragStartListener( + index: index!, + child: MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grab, + child: GestureDetector( + onTap: onSelected, + child: SizedBox( + width: 26, + child: Center( + child: FlowySvg( + FlowySvgs.drag_element_s, + size: const Size.square(14), + color: Theme.of(context).colorScheme.onBackground, + ), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onSelected, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5.0, + vertical: 4.0, + ), + child: SelectOptionTag( + option: option, + padding: const EdgeInsets.symmetric(horizontal: 8), + ), + ), + ), + ), + ), + ), + ...children, + ], + ); + } +} + +class _CreateOptionCell extends StatelessWidget { + const _CreateOptionCell({ + required this.suggestion, + }); + + final CreateSelectOptionSuggestion suggestion; + + @override + Widget build(BuildContext context) { + return Container( + height: 28, + margin: const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: + context.watch().state.focusedOptionId == + createSelectOptionSuggestionId + ? AFThemeExtension.of(context).lightGreyHover + : null, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: GestureDetector( + onTap: () => context + .read() + .add(const SelectOptionCellEditorEvent.createOption()), + child: MouseRegion( + onEnter: (_) { + context.read().add( + const SelectOptionCellEditorEvent.updateFocusedOption( + createSelectOptionSuggestionId, + ), + ); + }, + child: Row( + children: [ + FlowyText.medium( + LocaleKeys.grid_selectOption_create.tr(), + color: Theme.of(context).hintColor, + ), + const HSpace(10), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: SelectOptionTag( + name: suggestion.name, + color: suggestion.color.toColor(context), + fontSize: 11, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_editor.dart deleted file mode 100644 index a71e783dac..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_editor.dart +++ /dev/null @@ -1,381 +0,0 @@ -import 'dart:collection'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; -import 'package:appflowy_popover/appflowy_popover.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/style_widget/hover.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -import '../../application/cell/bloc/select_option_editor_bloc.dart'; -import '../../grid/presentation/layout/sizes.dart'; -import '../../grid/presentation/widgets/common/type_option_separator.dart'; -import '../../grid/presentation/widgets/header/type_option/select/select_option_editor.dart'; -import 'extension.dart'; -import 'select_option_text_field.dart'; - -const double _editorPanelWidth = 300; - -class SelectOptionCellEditor extends StatefulWidget { - const SelectOptionCellEditor({super.key, required this.cellController}); - - final SelectOptionCellController cellController; - - @override - State createState() => _SelectOptionCellEditorState(); -} - -class _SelectOptionCellEditorState extends State { - final TextEditingController textEditingController = TextEditingController(); - final popoverMutex = PopoverMutex(); - - @override - void dispose() { - popoverMutex.dispose(); - textEditingController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => SelectOptionCellEditorBloc( - cellController: widget.cellController, - )..add(const SelectOptionEditorEvent.initial()), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - _TextField( - textEditingController: textEditingController, - popoverMutex: popoverMutex, - ), - const TypeOptionSeparator(spacing: 0.0), - Flexible( - child: _OptionList( - textEditingController: textEditingController, - popoverMutex: popoverMutex, - ), - ), - ], - ); - }, - ), - ); - } -} - -class _OptionList extends StatelessWidget { - const _OptionList({ - required this.textEditingController, - required this.popoverMutex, - }); - - final TextEditingController textEditingController; - final PopoverMutex popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final cells = [ - _Title(onPressedAddButton: () => onPressedAddButton(context)), - ...state.options.map( - (option) => _SelectOptionCell( - option: option, - isSelected: state.selectedOptions.contains(option), - popoverMutex: popoverMutex, - ), - ), - ]; - - final createOption = state.createOption; - if (createOption != null) { - cells.add(_CreateOptionCell(name: createOption)); - } - - return ListView.separated( - shrinkWrap: true, - itemCount: cells.length, - separatorBuilder: (_, __) => - VSpace(GridSize.typeOptionSeparatorHeight), - physics: StyledScrollPhysics(), - itemBuilder: (_, int index) => cells[index], - padding: const EdgeInsets.symmetric(vertical: 8.0), - ); - }, - ); - } - - void onPressedAddButton(BuildContext context) { - final text = textEditingController.text; - if (text.isNotEmpty) { - context - .read() - .add(SelectOptionEditorEvent.trySelectOption(text)); - } - textEditingController.clear(); - } -} - -class _TextField extends StatelessWidget { - const _TextField({ - required this.textEditingController, - required this.popoverMutex, - }); - - final TextEditingController textEditingController; - final PopoverMutex popoverMutex; - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final optionMap = LinkedHashMap.fromIterable( - state.selectedOptions, - key: (option) => option.name, - value: (option) => option, - ); - - return Padding( - padding: const EdgeInsets.all(12.0), - child: SelectOptionTextField( - options: state.options, - selectedOptionMap: optionMap, - distanceToText: _editorPanelWidth * 0.7, - textController: textEditingController, - textSeparators: const [','], - onClick: () => popoverMutex.close(), - newText: (text) { - context - .read() - .add(SelectOptionEditorEvent.filterOption(text)); - }, - onSubmitted: (tagName) { - context - .read() - .add(SelectOptionEditorEvent.trySelectOption(tagName)); - }, - onPaste: (tagNames, remainder) { - context.read().add( - SelectOptionEditorEvent.selectMultipleOptions( - tagNames, - remainder, - ), - ); - }, - onRemove: (optionName) { - context.read().add( - SelectOptionEditorEvent.unSelectOption( - optionMap[optionName]!.id, - ), - ); - }, - ), - ); - }, - ); - } -} - -class _Title extends StatelessWidget { - const _Title({ - required this.onPressedAddButton, - }); - - final VoidCallback onPressedAddButton; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - height: GridSize.popoverItemHeight, - child: Row( - children: [ - Flexible( - child: FlowyText.medium( - LocaleKeys.grid_selectOption_panelTitle.tr(), - color: Theme.of(context).hintColor, - ), - ), - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4.0, - ), - child: FlowyIconButton( - onPressed: onPressedAddButton, - width: 18, - icon: const FlowySvg( - FlowySvgs.add_s, - ), - iconColorOnHover: Theme.of(context).colorScheme.onSecondary, - ), - ), - ], - ), - ), - ); - } -} - -class _CreateOptionCell extends StatelessWidget { - const _CreateOptionCell({ - required this.name, - }); - - final String name; - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: SizedBox( - height: 28, - child: FlowyButton( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - onTap: () => context - .read() - .add(SelectOptionEditorEvent.newOption(name)), - text: Row( - children: [ - FlowyText.medium( - LocaleKeys.grid_selectOption_create.tr(), - color: Theme.of(context).hintColor, - ), - const HSpace(10), - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: SelectOptionTag( - name: name, - fontSize: 11, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - color: Theme.of(context).colorScheme.surfaceVariant, - ), - ), - ), - ], - ), - ), - ), - ); - } -} - -class _SelectOptionCell extends StatefulWidget { - const _SelectOptionCell({ - required this.option, - required this.isSelected, - required this.popoverMutex, - }); - - final SelectOptionPB option; - final bool isSelected; - final PopoverMutex popoverMutex; - - @override - State<_SelectOptionCell> createState() => _SelectOptionCellState(); -} - -class _SelectOptionCellState extends State<_SelectOptionCell> { - late PopoverController _popoverController; - - @override - void initState() { - _popoverController = PopoverController(); - super.initState(); - } - - @override - Widget build(BuildContext context) { - final child = SizedBox( - height: 28, - child: SelectOptionTagCell( - option: widget.option, - onSelected: _onTap, - children: [ - if (widget.isSelected) - FlowyIconButton( - width: 20, - hoverColor: Colors.transparent, - onPressed: _onTap, - icon: FlowySvg( - FlowySvgs.check_s, - color: Theme.of(context).iconTheme.color, - ), - ), - FlowyIconButton( - onPressed: () => _popoverController.show(), - iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), - hoverColor: Colors.transparent, - icon: FlowySvg( - FlowySvgs.details_s, - color: Theme.of(context).iconTheme.color, - ), - ), - ], - ), - ); - return AppFlowyPopover( - controller: _popoverController, - offset: const Offset(8, 0), - margin: EdgeInsets.zero, - asBarrier: true, - constraints: BoxConstraints.loose(const Size(200, 470)), - mutex: widget.popoverMutex, - clickHandler: PopoverClickHandler.gestureDetector, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: FlowyHover( - resetHoverOnRebuild: false, - style: HoverStyle( - hoverColor: AFThemeExtension.of(context).lightGreyHover, - ), - child: child, - ), - ), - popupBuilder: (BuildContext popoverContext) { - return SelectOptionTypeOptionEditor( - option: widget.option, - onDeleted: () { - context - .read() - .add(SelectOptionEditorEvent.deleteOption(widget.option)); - PopoverContainer.of(popoverContext).close(); - }, - onUpdated: (updatedOption) { - context - .read() - .add(SelectOptionEditorEvent.updateOption(updatedOption)); - }, - key: ValueKey( - widget.option.id, - ), // Use ValueKey to refresh the UI, otherwise, it will remain the old value. - ); - }, - ); - } - - void _onTap() { - widget.popoverMutex.close(); - if (widget.isSelected) { - context - .read() - .add(SelectOptionEditorEvent.unSelectOption(widget.option.id)); - } else { - context - .read() - .add(SelectOptionEditorEvent.selectOption(widget.option.id)); - } - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart index 3f1d2a6ac1..d6ad3ffebe 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/select_option_text_field.dart @@ -4,9 +4,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities import 'package:flowy_infra/size.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:flutter/services.dart'; import 'extension.dart'; @@ -18,6 +15,7 @@ class SelectOptionTextField extends StatefulWidget { required this.distanceToText, required this.textSeparators, required this.textController, + required this.focusNode, required this.onSubmitted, required this.newText, required this.onPaste, @@ -30,8 +28,9 @@ class SelectOptionTextField extends StatefulWidget { final double distanceToText; final List textSeparators; final TextEditingController textController; + final FocusNode focusNode; - final Function(String) onSubmitted; + final Function() onSubmitted; final Function(String) newText; final Function(List, String) onPaste; final Function(String) onRemove; @@ -42,32 +41,11 @@ class SelectOptionTextField extends StatefulWidget { } class _SelectOptionTextFieldState extends State { - late final FocusNode focusNode; - @override void initState() { super.initState(); - focusNode = FocusNode( - onKeyEvent: (node, event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - if (!widget.textController.value.composing.isCollapsed) { - final TextRange(:start, :end) = - widget.textController.value.composing; - final text = widget.textController.text; - - widget.textController.value = TextEditingValue( - text: "${text.substring(0, start)}${text.substring(end)}", - selection: TextSelection(baseOffset: start, extentOffset: start), - ); - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - }, - ); WidgetsBinding.instance.addPostFrameCallback((_) { - focusNode.requestFocus(); + widget.focusNode.requestFocus(); }); widget.textController.addListener(_onChanged); } @@ -75,7 +53,6 @@ class _SelectOptionTextFieldState extends State { @override void dispose() { widget.textController.removeListener(_onChanged); - focusNode.dispose(); super.dispose(); } @@ -83,15 +60,9 @@ class _SelectOptionTextFieldState extends State { Widget build(BuildContext context) { return TextField( controller: widget.textController, - focusNode: focusNode, + focusNode: widget.focusNode, onTap: widget.onClick, - onSubmitted: (text) { - if (text.isNotEmpty) { - widget.onSubmitted(text.trim()); - focusNode.requestFocus(); - widget.textController.clear(); - } - }, + onSubmitted: (_) => widget.onSubmitted(), style: Theme.of(context).textTheme.bodyMedium, decoration: InputDecoration( enabledBorder: OutlineInputBorder( @@ -100,11 +71,6 @@ class _SelectOptionTextFieldState extends State { ), isDense: true, prefixIcon: _renderTags(context), - hintText: LocaleKeys.grid_selectOption_searchOption.tr(), - hintStyle: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: Theme.of(context).hintColor), prefixIconConstraints: BoxConstraints(maxWidth: widget.distanceToText), focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Theme.of(context).colorScheme.primary), @@ -148,23 +114,26 @@ class _SelectOptionTextFieldState extends State { ) .toList(); - return MouseRegion( - cursor: SystemMouseCursors.basic, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: ScrollConfiguration( - behavior: ScrollConfiguration.of(context).copyWith( - dragDevices: { - PointerDeviceKind.mouse, - PointerDeviceKind.touch, - PointerDeviceKind.trackpad, - PointerDeviceKind.stylus, - PointerDeviceKind.invertedStylus, - }, - ), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Wrap(spacing: 4, children: children), + return Focus( + descendantsAreFocusable: false, + child: MouseRegion( + cursor: SystemMouseCursors.basic, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith( + dragDevices: { + PointerDeviceKind.mouse, + PointerDeviceKind.touch, + PointerDeviceKind.trackpad, + PointerDeviceKind.stylus, + PointerDeviceKind.invertedStylus, + }, + ), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Wrap(spacing: 4, children: children), + ), ), ), ), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart similarity index 95% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart index 0cd22a7a29..f619a17c4f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_editor.dart @@ -20,7 +20,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:styled_widget/styled_widget.dart'; import 'field_type_list.dart'; -import 'type_option/builder.dart'; +import 'type_option_editor/builder.dart'; enum FieldEditorPage { general, @@ -104,6 +104,8 @@ class _FieldEditorState extends State { VSpace(GridSize.typeOptionSeparatorHeight), _actionCell(FieldAction.duplicate), VSpace(GridSize.typeOptionSeparatorHeight), + _actionCell(FieldAction.clearData), + VSpace(GridSize.typeOptionSeparatorHeight), _actionCell(FieldAction.delete), ], ).padding(all: 8.0), @@ -195,6 +197,7 @@ enum FieldAction { insertRight, toggleVisibility, duplicate, + clearData, delete; Widget icon(FieldInfo fieldInfo, Color? color) { @@ -213,6 +216,8 @@ enum FieldAction { } case FieldAction.duplicate: svgData = FlowySvgs.copy_s; + case FieldAction.clearData: + svgData = FlowySvgs.reload_s; case FieldAction.delete: svgData = FlowySvgs.delete_s; } @@ -241,6 +246,8 @@ enum FieldAction { } case FieldAction.duplicate: return LocaleKeys.grid_field_duplicate.tr(); + case FieldAction.clearData: + return LocaleKeys.grid_field_clear.tr(); case FieldAction.delete: return LocaleKeys.grid_field_delete.tr(); } @@ -273,6 +280,22 @@ enum FieldAction { fieldId: fieldInfo.id, ); break; + case FieldAction.clearData: + NavigatorAlertDialog( + constraints: const BoxConstraints( + maxWidth: 250, + maxHeight: 260, + ), + title: LocaleKeys.grid_field_clearFieldPromptMessage.tr(), + confirm: () { + FieldBackendService.clearField( + viewId: viewId, + fieldId: fieldInfo.id, + ); + }, + ).show(context); + PopoverContainer.of(context).close(); + break; case FieldAction.delete: NavigatorAlertDialog( title: LocaleKeys.grid_field_deleteFieldPromptMessage.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart similarity index 95% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart index 3cc9bd1c72..f8c5aea5ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/field_type_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/field_type_list.dart @@ -1,12 +1,11 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import '../../layout/sizes.dart'; - typedef SelectFieldCallback = void Function(FieldType); const List _supportedFieldTypes = [ @@ -20,6 +19,7 @@ const List _supportedFieldTypes = [ FieldType.URL, FieldType.LastEditedTime, FieldType.CreatedTime, + FieldType.Relation, ]; class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/builder.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/builder.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checkbox.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/checkbox.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checkbox.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checklist.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/checklist.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/checklist.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/date.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart similarity index 98% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/date.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart index 201e74436e..1ce3b73dad 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date.dart @@ -5,7 +5,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:protobuf/protobuf.dart'; -import '../../../layout/sizes.dart'; +import '../../../grid/presentation/layout/sizes.dart'; import 'builder.dart'; import 'date/date_time_format.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/multi_select.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/multi_select.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/multi_select.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/multi_select.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/number.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart similarity index 97% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/number.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart index fa980d0ab7..244f38326c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/number.dart @@ -10,8 +10,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:protobuf/protobuf.dart'; -import '../../../layout/sizes.dart'; -import '../../common/type_option_separator.dart'; +import '../../../grid/presentation/layout/sizes.dart'; +import '../../../grid/presentation/widgets/common/type_option_separator.dart'; import 'builder.dart'; class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart new file mode 100644 index 0000000000..9ca2729cb6 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/relation.dart @@ -0,0 +1,160 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart'; +import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:protobuf/protobuf.dart'; + +import 'builder.dart'; + +class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory { + const RelationTypeOptionEditorFactory(); + + @override + Widget? build({ + required BuildContext context, + required String viewId, + required FieldPB field, + required PopoverMutex popoverMutex, + required TypeOptionDataCallback onTypeOptionUpdated, + }) { + final typeOption = _parseTypeOptionData(field.typeOptionData); + + return BlocProvider( + create: (_) => RelationDatabaseListCubit(), + child: Builder( + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.only(left: 14, right: 8), + height: GridSize.popoverItemHeight, + alignment: Alignment.centerLeft, + child: FlowyText.regular( + LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(), + color: Theme.of(context).hintColor, + fontSize: 11, + ), + ), + AppFlowyPopover( + mutex: popoverMutex, + triggerActions: + PopoverTriggerFlags.hover | PopoverTriggerFlags.click, + offset: const Offset(6, 0), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8), + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: BlocBuilder( + builder: (context, state) { + final databaseMeta = + state.databaseMetas.firstWhereOrNull( + (meta) => meta.databaseId == typeOption.databaseId, + ); + return FlowyText( + databaseMeta == null + ? LocaleKeys + .grid_relation_relatedDatabasePlaceholder + .tr() + : databaseMeta.databaseName, + color: databaseMeta == null + ? Theme.of(context).hintColor + : null, + overflow: TextOverflow.ellipsis, + ); + }, + ), + rightIcon: const FlowySvg(FlowySvgs.more_s), + ), + ), + popupBuilder: (popoverContext) { + return BlocProvider.value( + value: context.read(), + child: _DatabaseList( + onSelectDatabase: (newDatabaseId) { + final newTypeOption = _updateTypeOption( + typeOption: typeOption, + databaseId: newDatabaseId, + ); + onTypeOptionUpdated(newTypeOption.writeToBuffer()); + PopoverContainer.of(context).close(); + }, + currentDatabaseId: typeOption.databaseId, + ), + ); + }, + ), + ], + ); + }, + ), + ); + } + + RelationTypeOptionPB _parseTypeOptionData(List data) { + return RelationTypeOptionDataParser().fromBuffer(data); + } + + RelationTypeOptionPB _updateTypeOption({ + required RelationTypeOptionPB typeOption, + required String databaseId, + }) { + typeOption.freeze(); + return typeOption.rebuild((typeOption) { + typeOption.databaseId = databaseId; + }); + } +} + +class _DatabaseList extends StatelessWidget { + const _DatabaseList({ + required this.onSelectDatabase, + required this.currentDatabaseId, + }); + + final String currentDatabaseId; + final void Function(String databaseId) onSelectDatabase; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final children = state.databaseMetas.map((meta) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + onTap: () => onSelectDatabase(meta.databaseId), + text: FlowyText.medium( + meta.databaseName, + overflow: TextOverflow.ellipsis, + ), + rightIcon: meta.databaseId == currentDatabaseId + ? const FlowySvg( + FlowySvgs.check_s, + ) + : null, + ), + ); + }).toList(); + + return ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + separatorBuilder: (_, __) => + VSpace(GridSize.typeOptionSeparatorHeight), + itemCount: children.length, + itemBuilder: (context, index) => children[index], + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/rich_text.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/rich_text.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/rich_text.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/rich_text.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart similarity index 75% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart index 619260a631..4c56121890 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option.dart @@ -1,9 +1,11 @@ +import 'dart:io'; + import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/field/type_option/select_option_type_option_bloc.dart'; import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; +import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -48,16 +50,15 @@ class SelectOptionTypeOptionWidget extends StatelessWidget { ] else const _AddOptionButton(), const VSpace(4), - ...state.options.map((option) { - return _OptionCell( - option: option, + Flexible( + child: _OptionList( popoverMutex: popoverMutex, - ); - }), + ), + ), ]; - return ListView( - shrinkWrap: true, + return Column( + mainAxisSize: MainAxisSize.min, children: children, ); }, @@ -90,9 +91,15 @@ class _OptionTitle extends StatelessWidget { } class _OptionCell extends StatefulWidget { - const _OptionCell({required this.option, this.popoverMutex}); + const _OptionCell({ + super.key, + required this.option, + required this.index, + this.popoverMutex, + }); final SelectOptionPB option; + final int index; final PopoverMutex? popoverMutex; @override @@ -108,6 +115,7 @@ class _OptionCellState extends State<_OptionCell> { height: 28, child: SelectOptionTagCell( option: widget.option, + index: widget.index, onSelected: () => _popoverController.show(), children: [ FlowyIconButton( @@ -115,8 +123,9 @@ class _OptionCellState extends State<_OptionCell> { iconPadding: const EdgeInsets.symmetric(horizontal: 6.0), hoverColor: Colors.transparent, icon: FlowySvg( - FlowySvgs.details_s, + FlowySvgs.three_dots_s, color: Theme.of(context).iconTheme.color, + size: const Size.square(16), ), ), ], @@ -140,7 +149,7 @@ class _OptionCellState extends State<_OptionCell> { ), ), popupBuilder: (BuildContext popoverContext) { - return SelectOptionTypeOptionEditor( + return SelectOptionEditor( option: widget.option, onDeleted: () { context @@ -253,3 +262,61 @@ class _CreateOptionTextFieldState extends State { super.dispose(); } } + +class _OptionList extends StatelessWidget { + const _OptionList({ + this.popoverMutex, + }); + + final PopoverMutex? popoverMutex; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return ReorderableListView.builder( + shrinkWrap: true, + onReorderStart: (_) => popoverMutex?.close(), + proxyDecorator: (child, index, _) => Material( + color: Colors.transparent, + child: Stack( + children: [ + BlocProvider.value( + value: context.read(), + child: child, + ), + MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: const SizedBox.expand(), + ), + ], + ), + ), + buildDefaultDragHandles: false, + itemBuilder: (context, index) => _OptionCell( + key: ValueKey("select_type_option_list_${state.options[index].id}"), + index: index, + option: state.options[index], + popoverMutex: popoverMutex, + ), + itemCount: state.options.length, + onReorder: (oldIndex, newIndex) { + if (oldIndex < newIndex) { + newIndex--; + } + final fromOptionId = state.options[oldIndex].id; + final toOptionId = state.options[newIndex].id; + context.read().add( + SelectOptionTypeOptionEvent.reorderOption( + fromOptionId, + toOptionId, + ), + ); + }, + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/select/select_option_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart similarity index 95% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/select/select_option_editor.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart index dad0798cd3..5df44f4b49 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/select/select_option_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/select/select_option_editor.dart @@ -13,11 +13,11 @@ import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../../layout/sizes.dart'; -import '../../../common/type_option_separator.dart'; +import '../../../../grid/presentation/layout/sizes.dart'; +import '../../../../grid/presentation/widgets/common/type_option_separator.dart'; -class SelectOptionTypeOptionEditor extends StatelessWidget { - const SelectOptionTypeOptionEditor({ +class SelectOptionEditor extends StatelessWidget { + const SelectOptionEditor({ super.key, required this.option, required this.onDeleted, @@ -32,7 +32,7 @@ class SelectOptionTypeOptionEditor extends StatelessWidget { final bool showOptions; final bool autoFocus; - static String get identifier => (SelectOptionTypeOptionEditor).toString(); + static String get identifier => (SelectOptionEditor).toString(); @override Widget build(BuildContext context) { @@ -230,7 +230,7 @@ class _SelectOptionColorCell extends StatelessWidget { child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( - color.optionName(), + color.colorName(), color: AFThemeExtension.of(context).textColor, ), leftIcon: colorIcon, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/single_select.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/single_select.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/single_select.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/single_select.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/timestamp.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/timestamp.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/url.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/url.dart similarity index 100% rename from frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/url.dart rename to frontend/appflowy_flutter/lib/plugins/database/widgets/field/type_option_editor/url.dart diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart index 7a5f802179..969c4e6e0c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_property.dart @@ -8,7 +8,7 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/domain/field_service.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart index dd3d31952e..d5f3ce293a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/mobile_database_controls.dart @@ -27,11 +27,11 @@ class MobileDatabaseControls extends StatelessWidget { Widget build(BuildContext context) { return MultiBlocProvider( providers: [ - BlocProvider( - create: (context) => GridFilterMenuBloc( + BlocProvider( + create: (context) => DatabaseFilterMenuBloc( viewId: controller.viewId, fieldController: controller.fieldController, - )..add(const GridFilterMenuEvent.initial()), + )..add(const DatabaseFilterMenuEvent.initial()), ), BlocProvider( create: (context) => SortEditorBloc( @@ -40,7 +40,7 @@ class MobileDatabaseControls extends StatelessWidget { ), ), ], - child: BlocListener( + child: BlocListener( listenWhen: (p, c) => p.isVisible != c.isVisible, listener: (context, state) => toggleExtension.toggle(), child: ValueListenableBuilder( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart index 97a0cf1299..198152ab7d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/setting/setting_property_list.dart @@ -5,7 +5,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/setting/property_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart'; +import 'package:appflowy/plugins/database/widgets/field/field_editor.dart'; import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart'; import 'package:appflowy/util/field_type_extension.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_awareness_metadata.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_awareness_metadata.dart new file mode 100644 index 0000000000..2aa288c58b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_awareness_metadata.dart @@ -0,0 +1,22 @@ +// This file is "main.dart" +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'doc_awareness_metadata.freezed.dart'; +part 'doc_awareness_metadata.g.dart'; + +@freezed +class DocumentAwarenessMetadata with _$DocumentAwarenessMetadata { + const factory DocumentAwarenessMetadata({ + // ignore: invalid_annotation_target + @JsonKey(name: 'cursor_color') required String cursorColor, + // ignore: invalid_annotation_target + @JsonKey(name: 'selection_color') required String selectionColor, + // ignore: invalid_annotation_target + @JsonKey(name: 'user_name') required String userName, + // ignore: invalid_annotation_target + @JsonKey(name: 'user_avatar') required String userAvatar, + }) = _DocumentAwarenessMetadata; + + factory DocumentAwarenessMetadata.fromJson(Map json) => + _$DocumentAwarenessMetadataFromJson(json); +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart index 4bcbbbaafe..aefa957358 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_bloc.dart @@ -1,14 +1,23 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:appflowy/plugins/document/application/doc_awareness_metadata.dart'; +import 'package:appflowy/plugins/document/application/doc_collab_adapter.dart'; +import 'package:appflowy/plugins/document/application/doc_listener.dart'; import 'package:appflowy/plugins/document/application/doc_service.dart'; +import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart'; import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart'; import 'package:appflowy/plugins/trash/application/trash_service.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; -import 'package:appflowy/workspace/application/doc/doc_listener.dart'; -import 'package:appflowy/workspace/application/doc/sync_state_listener.dart'; +import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy/util/color_to_hex_string.dart'; +import 'package:appflowy/util/debounce.dart'; import 'package:appflowy/workspace/application/view/view_listener.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; @@ -47,12 +56,17 @@ class DocumentBloc extends Bloc { final DocumentService _documentService = DocumentService(); final TrashService _trashService = TrashService(); + late DocumentCollabAdapter _documentCollabAdapter; + late final TransactionAdapter _transactionAdapter = TransactionAdapter( documentId: view.id, documentService: _documentService, ); - StreamSubscription? _subscription; + StreamSubscription? _transactionSubscription; + + final _updateSelectionDebounce = Debounce(); + final _syncDocDebounce = Debounce(); bool get isLocalMode { final userProfilePB = state.userProfilePB; @@ -65,7 +79,7 @@ class DocumentBloc extends Bloc { await _documentListener.stop(); await _syncStateListener.stop(); await _viewListener.stop(); - await _subscription?.cancel(); + await _transactionSubscription?.cancel(); await _documentService.closeDocument(view: view); state.editorState?.service.keyboardService?.closeKeyboard(); state.editorState?.dispose(); @@ -78,33 +92,30 @@ class DocumentBloc extends Bloc { ) async { await event.when( initial: () async { - final editorState = await _fetchDocumentState(); + final result = await _fetchDocumentState(); _onViewChanged(); _onDocumentChanged(); - await editorState.fold( + final newState = await result.fold( (s) async { - final result = await getIt().getUser(); - final userProfilePB = result.fold( - (s) => s, - (e) => null, - ); - emit( - state.copyWith( - error: null, - editorState: s, - isLoading: false, - userProfilePB: userProfilePB, - ), + final userProfilePB = + await getIt().getUser().toNullable(); + return state.copyWith( + error: null, + editorState: s, + isLoading: false, + userProfilePB: userProfilePB, ); }, - (f) async => emit( - state.copyWith( - error: f, - editorState: null, - isLoading: false, - ), + (f) async => state.copyWith( + error: f, + editorState: null, + isLoading: false, ), ); + emit(newState); + if (newState.userProfilePB != null) { + await _updateCollaborator(); + } }, moveToTrash: () async { emit(state.copyWith(isDeleted: true)); @@ -122,8 +133,8 @@ class DocumentBloc extends Bloc { final isDeleted = result.fold((l) => false, (r) => true); emit(state.copyWith(isDeleted: isDeleted)); }, - syncStateChanged: (isSyncing) { - emit(state.copyWith(isSyncing: isSyncing)); + syncStateChanged: (syncState) { + emit(state.copyWith(syncState: syncState.value)); }, ); } @@ -144,13 +155,14 @@ class DocumentBloc extends Bloc { /// subscribe to the document content change void _onDocumentChanged() { _documentListener.start( - didReceiveUpdate: syncDocumentDataPB, + onDocEventUpdate: _debounceSyncDoc, + onDocAwarenessUpdate: _onAwarenessStatesUpdate, ); _syncStateListener.start( didReceiveSyncState: (syncState) { if (!isClosed) { - add(DocumentEvent.syncStateChanged(syncState.isSyncing)); + add(DocumentEvent.syncStateChanged(syncState)); } }, ); @@ -174,22 +186,31 @@ class DocumentBloc extends Bloc { final editorState = EditorState(document: document); + _documentCollabAdapter = DocumentCollabAdapter(editorState, view.id); + // subscribe to the document change from the editor - _subscription = editorState.transactionStream.listen((event) async { - final time = event.$1; - if (time != TransactionTime.before) { - return; - } - await _transactionAdapter.apply(event.$2, editorState); + _transactionSubscription = editorState.transactionStream.listen( + (event) async { + final time = event.$1; + final transaction = event.$2; + if (time != TransactionTime.before) { + return; + } - // check if the document is empty. - await applyRules(); + // apply transaction to backend + await _transactionAdapter.apply(transaction, editorState); - if (!isClosed) { - // ignore: invalid_use_of_visible_for_testing_member - emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty)); - } - }); + // check if the document is empty. + await _applyRules(); + + if (!isClosed) { + // ignore: invalid_use_of_visible_for_testing_member + emit(state.copyWith(isDocumentEmpty: editorState.document.isEmpty)); + } + }, + ); + + editorState.selectionNotifier.addListener(_debounceOnSelectionUpdate); // output the log from the editor when debug mode if (kDebugMode) { @@ -203,14 +224,14 @@ class DocumentBloc extends Bloc { return editorState; } - Future applyRules() async { + Future _applyRules() async { await Future.wait([ - ensureAtLeastOneParagraphExists(), - ensureLastNodeIsEditable(), + _ensureAtLeastOneParagraphExists(), + _ensureLastNodeIsEditable(), ]); } - Future ensureLastNodeIsEditable() async { + Future _ensureLastNodeIsEditable() async { final editorState = state.editorState; if (editorState == null) { return; @@ -225,7 +246,7 @@ class DocumentBloc extends Bloc { } } - Future ensureAtLeastOneParagraphExists() async { + Future _ensureAtLeastOneParagraphExists() async { final editorState = state.editorState; if (editorState == null) { return; @@ -241,22 +262,89 @@ class DocumentBloc extends Bloc { } } - void syncDocumentDataPB(DocEventPB docEvent) { - // prettyPrintJson(docEvent.toProto3Json()); - // todo: integrate the document change to the editor - // for (final event in docEvent.events) { - // for (final blockEvent in event.event) { - // switch (blockEvent.command) { - // case DeltaTypePB.Inserted: - // break; - // case DeltaTypePB.Updated: - // break; - // case DeltaTypePB.Removed: - // break; - // default: - // } - // } - // } + Future _onDocumentStateUpdate(DocEventPB docEvent) async { + if (!docEvent.isRemote || !FeatureFlag.syncDocument.isOn) { + return; + } + + unawaited(_documentCollabAdapter.syncV3(docEvent)); + } + + Future _onAwarenessStatesUpdate( + DocumentAwarenessStatesPB awarenessStates, + ) async { + if (!FeatureFlag.syncDocument.isOn) { + return; + } + + final userId = state.userProfilePB?.id; + if (userId != null) { + await _documentCollabAdapter.updateRemoteSelection( + userId.toString(), + awarenessStates, + ); + } + } + + void _debounceOnSelectionUpdate() { + _updateSelectionDebounce.call(_onSelectionUpdate); + } + + void _debounceSyncDoc(DocEventPB docEvent) { + _syncDocDebounce.call(() { + _onDocumentStateUpdate(docEvent); + }); + } + + Future _onSelectionUpdate() async { + final user = state.userProfilePB; + final deviceId = ApplicationInfo.deviceId; + if (!FeatureFlag.syncDocument.isOn || user == null) { + return; + } + + final editorState = state.editorState; + if (editorState == null) { + return; + } + final selection = editorState.selection; + + // sync the selection + final id = user.id.toString() + deviceId; + final basicColor = ColorGenerator(id.toString()).toColor(); + final metadata = DocumentAwarenessMetadata( + cursorColor: basicColor.toHexString(), + selectionColor: basicColor.withOpacity(0.6).toHexString(), + userName: user.name, + userAvatar: user.iconUrl, + ); + await _documentService.syncAwarenessStates( + documentId: view.id, + selection: selection, + metadata: jsonEncode(metadata.toJson()), + ); + } + + Future _updateCollaborator() async { + final user = state.userProfilePB; + final deviceId = ApplicationInfo.deviceId; + if (!FeatureFlag.syncDocument.isOn || user == null) { + return; + } + + // sync the selection + final id = user.id.toString() + deviceId; + final basicColor = ColorGenerator(id.toString()).toColor(); + final metadata = DocumentAwarenessMetadata( + cursorColor: basicColor.toHexString(), + selectionColor: basicColor.withOpacity(0.6).toHexString(), + userName: user.name, + userAvatar: user.iconUrl, + ); + await _documentService.syncAwarenessStates( + documentId: view.id, + metadata: jsonEncode(metadata.toJson()), + ); } } @@ -267,27 +355,29 @@ class DocumentEvent with _$DocumentEvent { const factory DocumentEvent.restore() = Restore; const factory DocumentEvent.restorePage() = RestorePage; const factory DocumentEvent.deletePermanently() = DeletePermanently; - const factory DocumentEvent.syncStateChanged(bool isSyncing) = - syncStateChanged; + const factory DocumentEvent.syncStateChanged( + final DocumentSyncStatePB syncState, + ) = syncStateChanged; } @freezed class DocumentState with _$DocumentState { const factory DocumentState({ - required bool isDeleted, - required bool forceClose, - required bool isLoading, - required bool isSyncing, + required final bool isDeleted, + required final bool forceClose, + required final bool isLoading, + required final DocumentSyncState syncState, bool? isDocumentEmpty, UserProfilePB? userProfilePB, EditorState? editorState, FlowyError? error, + @Default(null) DocumentAwarenessStatesPB? awarenessStates, }) = _DocumentState; factory DocumentState.initial() => const DocumentState( isDeleted: false, forceClose: false, isLoading: true, - isSyncing: false, + syncState: DocumentSyncState.Syncing, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_collab_adapter.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_collab_adapter.dart new file mode 100644 index 0000000000..fc67e14ece --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_collab_adapter.dart @@ -0,0 +1,227 @@ +import 'dart:convert'; + +import 'package:appflowy/plugins/document/application/doc_awareness_metadata.dart'; +import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart'; +import 'package:appflowy/plugins/document/application/prelude.dart'; +import 'package:appflowy/shared/list_extension.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy/util/color_generator/color_generator.dart'; +import 'package:appflowy/util/json_print.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; +import 'package:collection/collection.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class DocumentCollabAdapter { + DocumentCollabAdapter(this.editorState, this.docId); + + final EditorState editorState; + final String docId; + + final _service = DocumentService(); + + /// Sync version 1 + /// + /// Force to reload the document + /// + /// Only use in development + Future syncV1() async { + final result = await _service.getDocument(viewId: docId); + final document = result.fold((s) => s.toDocument(), (f) => null); + if (document == null) { + return null; + } + return EditorState(document: document); + } + + /// Sync version 2 + /// + /// Translate the [docEvent] from yrs to [Operation]s and apply it to the [editorState] + /// + /// Not fully implemented yet + Future syncV2(DocEventPB docEvent) async { + prettyPrintJson(docEvent.toProto3Json()); + + final transaction = editorState.transaction; + + for (final event in docEvent.events) { + for (final blockEvent in event.event) { + switch (blockEvent.command) { + case DeltaTypePB.Inserted: + break; + case DeltaTypePB.Updated: + await _syncUpdated(blockEvent, transaction); + break; + case DeltaTypePB.Removed: + break; + default: + } + } + } + + await editorState.apply(transaction, isRemote: true); + } + + /// Sync version 3 + /// + /// Diff the local document with the remote document and apply the changes + Future syncV3(DocEventPB docEvent) async { + final result = await _service.getDocument(viewId: docId); + final document = result.fold((s) => s.toDocument(), (f) => null); + if (document == null) { + return; + } + + final ops = diffNodes(editorState.document.root, document.root); + if (ops.isEmpty) { + debugPrint('[collab] received empty ops'); + return; + } + + debugPrint('[collab] received ops: $ops'); + + final transaction = editorState.transaction; + for (final op in ops) { + transaction.add(op); + } + await editorState.apply(transaction, isRemote: true); + } + + Future _syncUpdated( + BlockEventPayloadPB payload, + Transaction transaction, + ) async { + assert(payload.command == DeltaTypePB.Updated); + + final path = payload.path; + final id = payload.id; + final value = jsonDecode(payload.value); + + final nodes = NodeIterator( + document: editorState.document, + startNode: editorState.document.root, + ).toList(); + + // 1. meta -> text_map = text delta change + if (path.isTextDeltaChangeset) { + // find the 'text' block and apply the delta + // ⚠️ not completed yet. + final target = nodes.singleWhereOrNull((n) => n.id == id); + if (target != null) { + try { + final delta = Delta.fromJson(jsonDecode(value)); + transaction.insertTextDelta(target, 0, delta); + } catch (e) { + Log.error('Failed to apply delta: $value, error: $e'); + } + } + } else if (path.isBlockChangeset) { + final target = nodes.singleWhereOrNull((n) => n.id == id); + if (target != null) { + try { + final delta = jsonDecode(value['data'])['delta']; + transaction.updateNode(target, { + 'delta': Delta.fromJson(delta).toJson(), + }); + } catch (e) { + Log.error('Failed to update $value, error: $e'); + } + } + } + } + + Future updateRemoteSelection( + String userId, + DocumentAwarenessStatesPB states, + ) async { + final List remoteSelections = []; + final deviceId = ApplicationInfo.deviceId; + // the values may be duplicated, sort by the timestamp and then filter the duplicated values + final values = states.value.values + .sorted( + (a, b) => b.timestamp.compareTo(a.timestamp), + ) // in descending order + .unique( + (e) => Object.hashAll([e.user.uid, e.user.deviceId]), + ); + for (final state in values) { + // the following code is only for version 1 + if (state.version != 1) { + return; + } + final uid = state.user.uid.toString(); + final did = state.user.deviceId; + final metadata = DocumentAwarenessMetadata.fromJson( + jsonDecode(state.metadata), + ); + final selectionColor = metadata.selectionColor.tryToColor(); + final cursorColor = metadata.cursorColor.tryToColor(); + if ((uid == userId && did == deviceId) || + (cursorColor == null || selectionColor == null)) { + continue; + } + final start = state.selection.start; + final end = state.selection.end; + final selection = Selection( + start: Position( + path: start.path.toIntList(), + offset: start.offset.toInt(), + ), + end: Position( + path: end.path.toIntList(), + offset: end.offset.toInt(), + ), + ); + final color = ColorGenerator(uid + did).toColor(); + final remoteSelection = RemoteSelection( + id: uid, + selection: selection, + selectionColor: selectionColor, + cursorColor: cursorColor, + builder: (_, __, rect) { + return Positioned( + top: rect.top - 14, + left: selection.isCollapsed ? rect.right : rect.left, + child: ColoredBox( + color: color, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 2.0, + vertical: 1.0, + ), + child: FlowyText( + metadata.userName, + color: Colors.black, + fontSize: 12.0, + ), + ), + ), + ); + }, + ); + remoteSelections.add(remoteSelection); + } + if (remoteSelections.isNotEmpty) { + editorState.remoteSelections.value = remoteSelections; + } + } +} + +extension on List { + List toIntList() { + return map((e) => e.toInt()).toList(); + } +} + +extension on List { + bool get isTextDeltaChangeset { + return length == 3 && this[0] == 'meta' && this[1] == 'text_map'; + } + + bool get isBlockChangeset { + return length == 2 && this[0] == 'blocks'; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_collaborators_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_collaborators_bloc.dart new file mode 100644 index 0000000000..9d1eca931a --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_collaborators_bloc.dart @@ -0,0 +1,127 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:appflowy/plugins/document/application/doc_awareness_metadata.dart'; +import 'package:appflowy/plugins/document/application/doc_listener.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/startup/tasks/device_info_task.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'doc_collaborators_bloc.freezed.dart'; + +bool _filterCurrentUser = false; + +class DocumentCollaboratorsBloc + extends Bloc { + DocumentCollaboratorsBloc({ + required this.view, + }) : _listener = DocumentListener(id: view.id), + super(DocumentCollaboratorsState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final result = await getIt().getUser(); + final userProfile = result.fold((s) => s, (f) => null); + emit( + state.copyWith( + shouldShowIndicator: + userProfile?.authenticator != AuthenticatorPB.Local, + ), + ); + final deviceId = ApplicationInfo.deviceId; + if (userProfile != null) { + _listener.start( + onDocAwarenessUpdate: (states) { + add( + DocumentCollaboratorsEvent.update( + userProfile, + deviceId, + states, + ), + ); + }, + ); + } + }, + update: (userProfile, deviceId, states) { + final collaborators = _buildCollaborators( + userProfile, + deviceId, + states, + ); + emit(state.copyWith(collaborators: collaborators)); + }, + ); + }, + ); + } + + final ViewPB view; + final DocumentListener _listener; + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } + + List _buildCollaborators( + UserProfilePB userProfile, + String deviceId, + DocumentAwarenessStatesPB states, + ) { + final result = []; + final ids = {}; + final sorted = states.value.values.toList() + ..sort((a, b) => b.timestamp.compareTo(a.timestamp)) + ..retainWhere((e) => ids.add(e.user.uid.toString() + e.user.deviceId)); + for (final state in sorted) { + if (state.version != 1) { + continue; + } + // filter current user + if (_filterCurrentUser && + userProfile.id == state.user.uid && + deviceId == state.user.deviceId) { + continue; + } + try { + final metadata = DocumentAwarenessMetadata.fromJson( + jsonDecode(state.metadata), + ); + result.add(metadata); + } catch (e) { + Log.error('Failed to parse metadata: $e'); + } + } + return result; + } +} + +@freezed +class DocumentCollaboratorsEvent with _$DocumentCollaboratorsEvent { + const factory DocumentCollaboratorsEvent.initial() = Initial; + const factory DocumentCollaboratorsEvent.update( + UserProfilePB userProfile, + String deviceId, + DocumentAwarenessStatesPB states, + ) = Update; +} + +@freezed +class DocumentCollaboratorsState with _$DocumentCollaboratorsState { + const factory DocumentCollaboratorsState({ + @Default([]) List collaborators, + @Default(false) bool shouldShowIndicator, + }) = _DocumentCollaboratorsState; + + factory DocumentCollaboratorsState.initial() => + const DocumentCollaboratorsState(); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_listener.dart similarity index 58% rename from frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart rename to frontend/appflowy_flutter/lib/plugins/document/application/doc_listener.dart index 61ff86edcd..ab102c7ee8 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/doc/doc_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_listener.dart @@ -8,6 +8,11 @@ import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; +typedef OnDocumentEventUpdate = void Function(DocEventPB docEvent); +typedef OnDocumentAwarenessStateUpdate = void Function( + DocumentAwarenessStatesPB awarenessStates, +); + class DocumentListener { DocumentListener({ required this.id, @@ -18,12 +23,15 @@ class DocumentListener { StreamSubscription? _subscription; DocumentNotificationParser? _parser; - Function(DocEventPB docEvent)? didReceiveUpdate; + OnDocumentEventUpdate? _onDocEventUpdate; + OnDocumentAwarenessStateUpdate? _onDocAwarenessUpdate; void start({ - Function(DocEventPB docEvent)? didReceiveUpdate, + OnDocumentEventUpdate? onDocEventUpdate, + OnDocumentAwarenessStateUpdate? onDocAwarenessUpdate, }) { - this.didReceiveUpdate = didReceiveUpdate; + _onDocEventUpdate = onDocEventUpdate; + _onDocAwarenessUpdate = onDocAwarenessUpdate; _parser = DocumentNotificationParser( id: id, @@ -40,7 +48,16 @@ class DocumentListener { ) { switch (ty) { case DocumentNotification.DidReceiveUpdate: - result.map((r) => didReceiveUpdate?.call(DocEventPB.fromBuffer(r))); + result.map( + (s) => _onDocEventUpdate?.call(DocEventPB.fromBuffer(s)), + ); + break; + case DocumentNotification.DidUpdateDocumentAwarenessState: + result.map( + (s) => _onDocAwarenessUpdate?.call( + DocumentAwarenessStatesPB.fromBuffer(s), + ), + ); break; default: break; @@ -48,6 +65,8 @@ class DocumentListener { } Future stop() async { + _onDocAwarenessUpdate = null; + _onDocEventUpdate = null; await _subscription?.cancel(); _subscription = null; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart index 31497e85ac..cf60a3c474 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart @@ -2,7 +2,9 @@ import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:fixnum/fixnum.dart'; class DocumentService { // unused now. @@ -10,7 +12,7 @@ class DocumentService { required ViewPB view, }) async { final canOpen = await openDocument(viewId: view.id); - if (canOpen.isSuccess()) { + if (canOpen.isSuccess) { return FlowyResult.success(null); } final payload = CreateDocumentPayloadPB()..documentId = view.id; @@ -26,6 +28,14 @@ class DocumentService { return result; } + Future> getDocument({ + required String viewId, + }) async { + final payload = OpenDocumentPayloadPB()..documentId = viewId; + final result = await DocumentEventGetDocumentData(payload).send(); + return result; + } + Future> getBlockFromDocument({ required DocumentDataPB document, required String blockId, @@ -46,8 +56,8 @@ class DocumentService { Future> closeDocument({ required ViewPB view, }) async { - final payload = CloseDocumentPayloadPB()..documentId = view.id; - final result = await DocumentEventCloseDocument(payload).send(); + final payload = ViewIdPB()..value = view.id; + final result = await FolderEventCloseView(payload).send(); return result; } @@ -135,4 +145,39 @@ class DocumentService { return FlowyResult.failure(FlowyError(msg: 'Workspace not found')); }); } + + /// Sync the awareness states + /// For example, the cursor position, selection, who is viewing the document. + Future> syncAwarenessStates({ + required String documentId, + Selection? selection, + String? metadata, + }) async { + final payload = UpdateDocumentAwarenessStatePB( + documentId: documentId, + selection: convertSelectionToAwarenessSelection(selection), + metadata: metadata, + ); + + final result = await DocumentEventSetAwarenessState(payload).send(); + return result; + } + + DocumentAwarenessSelectionPB? convertSelectionToAwarenessSelection( + Selection? selection, + ) { + if (selection == null) { + return null; + } + return DocumentAwarenessSelectionPB( + start: DocumentAwarenessPositionPB( + offset: Int64(selection.startIndex), + path: selection.start.path.map((e) => Int64(e)), + ), + end: DocumentAwarenessPositionPB( + offset: Int64(selection.endIndex), + path: selection.end.path.map((e) => Int64(e)), + ), + ); + } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_bloc.dart new file mode 100644 index 0000000000..6d56b74c68 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_bloc.dart @@ -0,0 +1,104 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/document/application/doc_sync_state_listener.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'doc_sync_bloc.freezed.dart'; + +class DocumentSyncBloc extends Bloc { + DocumentSyncBloc({ + required this.view, + }) : _syncStateListener = DocumentSyncStateListener(id: view.id), + super(DocumentSyncBlocState.initial()) { + on( + (event, emit) async { + await event.when( + initial: () async { + final userProfile = await getIt().getUser().then( + (result) => result.fold( + (l) => l, + (r) => null, + ), + ); + emit( + state.copyWith( + shouldShowIndicator: + userProfile?.authenticator != AuthenticatorPB.Local, + ), + ); + _syncStateListener.start( + didReceiveSyncState: (syncState) { + add(DocumentSyncEvent.syncStateChanged(syncState)); + }, + ); + + final isNetworkConnected = await _connectivity + .checkConnectivity() + .then((value) => value != ConnectivityResult.none); + emit(state.copyWith(isNetworkConnected: isNetworkConnected)); + + connectivityStream = + _connectivity.onConnectivityChanged.listen((result) { + add(DocumentSyncEvent.networkStateChanged(result)); + }); + }, + syncStateChanged: (syncState) { + emit(state.copyWith(syncState: syncState.value)); + }, + networkStateChanged: (result) { + emit( + state.copyWith( + isNetworkConnected: result != ConnectivityResult.none, + ), + ); + }, + ); + }, + ); + } + + final ViewPB view; + final DocumentSyncStateListener _syncStateListener; + final _connectivity = Connectivity(); + + StreamSubscription? connectivityStream; + + @override + Future close() async { + await connectivityStream?.cancel(); + await _syncStateListener.stop(); + return super.close(); + } +} + +@freezed +class DocumentSyncEvent with _$DocumentSyncEvent { + const factory DocumentSyncEvent.initial() = Initial; + const factory DocumentSyncEvent.syncStateChanged( + DocumentSyncStatePB syncState, + ) = syncStateChanged; + const factory DocumentSyncEvent.networkStateChanged( + ConnectivityResult result, + ) = NetworkStateChanged; +} + +@freezed +class DocumentSyncBlocState with _$DocumentSyncBlocState { + const factory DocumentSyncBlocState({ + required DocumentSyncState syncState, + @Default(true) bool isNetworkConnected, + @Default(false) bool shouldShowIndicator, + }) = _DocumentSyncState; + + factory DocumentSyncBlocState.initial() => const DocumentSyncBlocState( + syncState: DocumentSyncState.Syncing, + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/doc/sync_state_listener.dart b/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart similarity index 88% rename from frontend/appflowy_flutter/lib/workspace/application/doc/sync_state_listener.dart rename to frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart index 6cd57ba0e6..7f73147d79 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/doc/sync_state_listener.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_sync_state_listener.dart @@ -8,6 +8,10 @@ import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart'; import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; +typedef DocumentSyncStateCallback = void Function( + DocumentSyncStatePB syncState, +); + class DocumentSyncStateListener { DocumentSyncStateListener({ required this.id, @@ -16,10 +20,10 @@ class DocumentSyncStateListener { final String id; StreamSubscription? _subscription; DocumentNotificationParser? _parser; - Function(DocumentSyncStatePB syncState)? didReceiveSyncState; + DocumentSyncStateCallback? didReceiveSyncState; void start({ - Function(DocumentSyncStatePB syncState)? didReceiveSyncState, + DocumentSyncStateCallback? didReceiveSyncState, }) { this.didReceiveSyncState = didReceiveSyncState; diff --git a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart index 1c98b970f2..e60782605a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/document_data_pb_extension.dart @@ -59,11 +59,11 @@ extension DocumentDataPBFromTo on DocumentDataPB { // generate the meta final childrenMap = {}; - blocks.forEach((key, value) { - final parentId = value.parentId; - if (parentId.isNotEmpty) { - childrenMap[parentId] ??= ChildrenPB.create(); - childrenMap[parentId]!.children.add(value.id); + blocks.values.where((e) => e.parentId.isNotEmpty).forEach((value) { + final childrenId = blocks[value.parentId]?.childrenId; + if (childrenId != null) { + childrenMap[childrenId] ??= ChildrenPB.create(); + childrenMap[childrenId]!.children.add(value.id); } }); final meta = MetaPB(childrenMap: childrenMap); @@ -101,10 +101,16 @@ extension DocumentDataPBFromTo on DocumentDataPB { children.addAll(childrenIds.map((e) => buildNode(e)).whereNotNull()); } - return block?.toNode( + final node = block?.toNode( children: children, meta: meta, ); + + for (final element in children) { + element.parent = node; + } + + return node; } } @@ -138,10 +144,11 @@ extension BlockToNode on BlockPB { final deltaString = meta.textMap[externalId]; if (deltaString != null) { final delta = jsonDecode(deltaString); - map.putIfAbsent( - 'delta', - () => delta, - ); + map['delta'] = delta; + // map.putIfAbsent( + // 'delta', + // () => delta, + // ); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index 6a35fb29bc..a8bb6f11a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -1,13 +1,14 @@ library document_plugin; -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/document_page.dart'; +import 'package:appflowy/plugins/document/presentation/document_collaborators.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/plugins/shared/sync_indicator.dart'; import 'package:appflowy/plugins/util.dart'; +import 'package:appflowy/shared/feature_flags.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; @@ -19,6 +20,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class DocumentPluginBuilder extends PluginBuilder { @@ -137,8 +139,28 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder return BlocProvider.value( value: bloc, child: Row( + mainAxisSize: MainAxisSize.min, children: [ - DocumentShareButton(key: ValueKey(view.id), view: view), + ...FeatureFlag.syncDocument.isOn + ? [ + DocumentCollaborators( + key: ValueKey('collaborators_${view.id}'), + width: 100, + height: 32, + view: view, + ), + const HSpace(16), + DocumentSyncIndicator( + key: ValueKey('sync_state_${view.id}'), + view: view, + ), + const HSpace(16), + ] + : [const HSpace(8)], + DocumentShareButton( + key: ValueKey('share_button_${view.id}'), + view: view, + ), const HSpace(4), ViewFavoriteButton( key: ValueKey('favorite_button_${view.id}'), diff --git a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart index 8cc60eaf7c..98ca75ac45 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document_page.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/banner.dart'; +import 'package:appflowy/plugins/document/presentation/editor_notification.dart'; import 'package:appflowy/plugins/document/presentation/editor_page.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_style.dart'; @@ -15,24 +14,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -enum EditorNotificationType { - undo, - redo, -} - -class EditorNotification extends Notification { - const EditorNotification({ - required this.type, - }); - - EditorNotification.undo() : type = EditorNotificationType.undo; - EditorNotification.redo() : type = EditorNotificationType.redo; - - final EditorNotificationType type; -} - class DocumentPage extends StatefulWidget { const DocumentPage({ super.key, @@ -50,12 +34,23 @@ class DocumentPage extends StatefulWidget { } class _DocumentPageState extends State { + EditorState? editorState; + @override void initState() { super.initState(); // The appflowy editor use Intl as localization, set the default language as fallback. Intl.defaultLocale = 'en_US'; + + EditorNotification.addListener(_onEditorNotification); + } + + @override + void dispose() { + EditorNotification.removeListener(_onEditorNotification); + + super.dispose(); } @override @@ -75,6 +70,7 @@ class _DocumentPageState extends State { } final editorState = state.editorState; + this.editorState = editorState; final error = state.error; if (error != null || editorState == null) { Log.error(error); @@ -149,20 +145,19 @@ class _DocumentPageState extends State { ); } - // Future _exportPage(DocumentDataPB data) async { - // final picker = getIt(); - // final dir = await picker.getDirectoryPath(); - // if (dir == null) { - // return; - // } - // final path = p.join(dir, '${documentBloc.view.name}.json'); - // const encoder = JsonEncoder.withIndent(' '); - // final json = encoder.convert(data.toProto3Json()); - // await File(path).writeAsString(json.base64.base64); - // if (mounted) { - // showSnackBarMessage(context, 'Export success to $path'); - // } - // } + void _onEditorNotification(EditorNotificationType type) { + final editorState = this.editorState; + if (editorState == null) { + return; + } + if (type == EditorNotificationType.undo) { + undoCommand.execute(editorState); + } else if (type == EditorNotificationType.redo) { + redoCommand.execute(editorState); + } else if (type == EditorNotificationType.exitEditing) { + editorState.selection = null; + } + } void _onNotificationAction( BuildContext context, @@ -180,20 +175,3 @@ class _DocumentPageState extends State { } } } - -class DocumentSyncIndicator extends StatelessWidget { - const DocumentSyncIndicator({super.key}); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - if (state.isSyncing) { - return const SizedBox(height: 1, child: LinearProgressIndicator()); - } else { - return const SizedBox(height: 1); - } - }, - ); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart new file mode 100644 index 0000000000..bf1e47fd13 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/collaborator_avater_stack.dart @@ -0,0 +1,83 @@ +import 'package:avatar_stack/avatar_stack.dart'; +import 'package:avatar_stack/positions.dart'; +import 'package:flutter/material.dart'; + +class CollaboratorAvatarStack extends StatelessWidget { + const CollaboratorAvatarStack({ + super.key, + required this.avatars, + this.settings, + this.infoWidgetBuilder, + this.width, + this.height, + this.borderWidth, + this.borderColor, + this.backgroundColor, + }); + + final List avatars; + + final Positions? settings; + + final InfoWidgetBuilder? infoWidgetBuilder; + + final double? width; + + final double? height; + + final double? borderWidth; + + final Color? borderColor; + + final Color? backgroundColor; + + @override + Widget build(BuildContext context) { + final settings = this.settings ?? + RestrictedPositions( + maxCoverage: 0.3, + minCoverage: 0.1, + align: StackAlign.right, + ); + + final border = BorderSide( + color: borderColor ?? Theme.of(context).colorScheme.onPrimary, + width: borderWidth ?? 2.0, + ); + + Widget textInfoWidgetBuilder(surplus) => BorderedCircleAvatar( + border: border, + backgroundColor: backgroundColor, + child: FittedBox( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '+$surplus', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + ); + final infoWidgetBuilder = this.infoWidgetBuilder ?? textInfoWidgetBuilder; + + return SizedBox( + height: height, + width: width, + child: WidgetStack( + positions: settings, + buildInfoWidget: infoWidgetBuilder, + stackedWidgets: avatars + .map( + (avatar) => CircleAvatar( + backgroundColor: border.color, + child: Padding( + padding: EdgeInsets.all(border.width), + child: avatar, + ), + ), + ) + .toList(), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart new file mode 100644 index 0000000000..28d61a23a2 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/document_collaborators.dart @@ -0,0 +1,67 @@ +import 'package:appflowy/plugins/document/application/doc_collaborators_bloc.dart'; +import 'package:appflowy/plugins/document/presentation/collaborator_avater_stack.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DocumentCollaborators extends StatelessWidget { + const DocumentCollaborators({ + super.key, + required this.height, + required this.width, + required this.view, + this.padding, + this.fontSize, + }); + + final ViewPB view; + final double height; + final double width; + final EdgeInsets? padding; + final double? fontSize; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => DocumentCollaboratorsBloc(view: view) + ..add(const DocumentCollaboratorsEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + final collaborators = state.collaborators; + if (!state.shouldShowIndicator || collaborators.isEmpty) { + return const SizedBox.shrink(); + } + + return Padding( + padding: padding ?? EdgeInsets.zero, + child: CollaboratorAvatarStack( + height: height, + width: width, + borderWidth: 1.0, + backgroundColor: + Theme.of(context).colorScheme.onSecondaryContainer, + avatars: collaborators + .map( + (c) => FlowyTooltip( + message: c.userName, + child: CircleAvatar( + backgroundColor: c.cursorColor.tryToColor(), + child: FlowyText( + c.userName.characters.firstOrNull ?? ' ', + fontSize: fontSize, + color: Colors.black, + ), + ), + ), + ) + .toList(), + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart new file mode 100644 index 0000000000..9ec6090b5b --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_notification.dart @@ -0,0 +1,46 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; + +enum EditorNotificationType { + none, + undo, + redo, + exitEditing, +} + +class EditorNotification { + const EditorNotification({ + required this.type, + }); + + EditorNotification.undo() : type = EditorNotificationType.undo; + EditorNotification.redo() : type = EditorNotificationType.redo; + EditorNotification.exitEditing() : type = EditorNotificationType.exitEditing; + + static final PropertyValueNotifier _notifier = + PropertyValueNotifier( + EditorNotificationType.none, + ); + + final EditorNotificationType type; + + void post() { + _notifier.value = type; + } + + static void addListener(ValueChanged listener) { + _notifier.addListener(() { + listener(_notifier.value); + }); + } + + static void removeListener(ValueChanged listener) { + _notifier.removeListener(() { + listener(_notifier.value); + }); + } + + static void dispose() { + _notifier.dispose(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart index e21e2fd313..c40bae7266 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/plugins/document/application/doc_bloc.dart'; import 'package:appflowy/plugins/document/presentation/editor_configuration.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/align_toolbar_item/custom_text_align_command.dart'; @@ -23,6 +20,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; final List commandShortcutEvents = [ @@ -198,6 +197,7 @@ class _AppFlowyEditorPageState extends State { _initEditorL10n(); _initializeShortcuts(); + appFlowyEditorAutoScrollEdgeOffset = 220; indentableBlockTypes.add(ToggleListBlockKeys.type); convertibleBlockTypes.add(ToggleListBlockKeys.type); slashMenuItems = _customSlashMenuItems(); @@ -297,19 +297,12 @@ class _AppFlowyEditorPageState extends State { if (PlatformExtension.isMobile) { return AppFlowyMobileToolbar( - toolbarHeight: 46.0, + toolbarHeight: 42.0, editorState: editorState, - toolbarItems: [ - undoToolbarItem, - redoToolbarItem, - addBlockToolbarItem, - todoListToolbarItem, - aaToolbarItem, - boldToolbarItem, - italicToolbarItem, - underlineToolbarItem, - colorToolbarItem, - ], + toolbarItemsBuilder: (selection) => buildMobileToolbarItems( + editorState, + selection, + ), child: Column( children: [ Expanded( @@ -317,19 +310,12 @@ class _AppFlowyEditorPageState extends State { editorState: editorState, editorScrollController: editorScrollController, toolbarBuilder: (context, anchor, closeToolbar) { - return AdaptiveTextSelectionToolbar.editable( - clipboardStatus: ClipboardStatus.pasteable, - onCopy: () { - customCopyCommand.execute(editorState); - closeToolbar(); - }, - onCut: () => customCutCommand.execute(editorState), - onPaste: () => customPasteCommand.execute(editorState), - onSelectAll: () => selectAllCommand.execute(editorState), - onLiveTextInput: null, - onLookUp: null, - onSearchWeb: null, - onShare: null, + return AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: buildMobileFloatingToolbarItems( + editorState, + anchor, + closeToolbar, + ), anchors: TextSelectionToolbarAnchors( primaryAnchor: anchor, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart index 1fc92c2d0f..34c6c8fe06 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart @@ -13,8 +13,8 @@ extension PasteNodes on EditorState { } final transaction = this.transaction; final insertedDelta = insertedNode.delta; - // if the node is empty, replace it with the inserted node. - if (delta.isEmpty) { + // if the node is empty and its type is paragprah, replace it with the inserted node. + if (delta.isEmpty && node.type == ParagraphBlockKeys.type) { transaction.insertNode( selection.end.path.next, insertedNode, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart new file mode 100644 index 0000000000..5a8b99af5d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_floating_toolbar/custom_mobile_floating_toolbar.dart @@ -0,0 +1,84 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; + +List buildMobileFloatingToolbarItems( + EditorState editorState, + Offset offset, + Function closeToolbar, +) { + // copy, paste, select, select all, cut + final selection = editorState.selection; + if (selection == null) { + return []; + } + final toolbarItems = []; + + if (!selection.isCollapsed) { + toolbarItems.add( + ContextMenuButtonItem( + label: LocaleKeys.editor_copy.tr(), + onPressed: () { + copyCommand.execute(editorState); + closeToolbar(); + }, + ), + ); + } + + toolbarItems.add( + ContextMenuButtonItem( + label: LocaleKeys.editor_paste.tr(), + onPressed: () { + pasteCommand.execute(editorState); + closeToolbar(); + }, + ), + ); + + if (!selection.isCollapsed) { + toolbarItems.add( + ContextMenuButtonItem( + label: LocaleKeys.editor_cut.tr(), + onPressed: () { + cutCommand.execute(editorState); + closeToolbar(); + }, + ), + ); + } + + toolbarItems.add( + ContextMenuButtonItem( + label: LocaleKeys.editor_select.tr(), + onPressed: () { + editorState.selectWord(offset); + closeToolbar(); + }, + ), + ); + + toolbarItems.add( + ContextMenuButtonItem( + label: LocaleKeys.editor_selectAll.tr(), + onPressed: () { + selectAllCommand.execute(editorState); + closeToolbar(); + }, + ), + ); + + return toolbarItems; +} + +extension on EditorState { + void selectWord(Offset offset) { + final node = service.selectionService.getNodeInOffset(offset); + final selection = node?.selectable?.getWordBoundaryInOffset(offset); + if (selection == null) { + return; + } + updateSelectionWithReason(selection); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart new file mode 100644 index 0000000000..b60eae3006 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart @@ -0,0 +1,28 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; + +extension SelectionColor on EditorState { + String? getSelectionColor(String key) { + final selection = this.selection; + if (selection == null) { + return null; + } + String? color = toggledStyle[key]; + if (color == null) { + if (selection.isCollapsed && selection.startIndex != 0) { + color = getDeltaAttributeValueInSelection( + key, + selection.copyWith( + start: selection.start.copyWith( + offset: selection.startIndex - 1, + ), + ), + ); + } else { + color = getDeltaAttributeValueInSelection( + key, + ); + } + } + return color; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_align_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart similarity index 92% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_align_items.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart index 22b1028871..dccff22664 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_align_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart @@ -1,7 +1,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; @@ -20,9 +20,9 @@ class AlignItems extends StatelessWidget { final EditorState editorState; final List<(String, FlowySvgData)> _alignMenuItems = [ - (_left, FlowySvgs.m_aa_align_left_s), - (_center, FlowySvgs.m_aa_align_center_s), - (_right, FlowySvgs.m_aa_align_right_s), + (_left, FlowySvgs.m_aa_align_left_m), + (_center, FlowySvgs.m_aa_align_center_m), + (_right, FlowySvgs.m_aa_align_right_m), ]; @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_bius_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart similarity index 87% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_bius_items.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart index 2fb2cd5db3..0de86ffd6c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_bius_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; @@ -14,10 +14,10 @@ class BIUSItems extends StatelessWidget { final EditorState editorState; final List<(FlowySvgData, String)> _bius = [ - (FlowySvgs.m_aa_bold_s, AppFlowyRichTextKeys.bold), - (FlowySvgs.m_aa_italic_s, AppFlowyRichTextKeys.italic), - (FlowySvgs.m_aa_underline_s, AppFlowyRichTextKeys.underline), - (FlowySvgs.m_aa_strike_s, AppFlowyRichTextKeys.strikethrough), + (FlowySvgs.m_toolbar_bold_m, AppFlowyRichTextKeys.bold), + (FlowySvgs.m_toolbar_italic_m, AppFlowyRichTextKeys.italic), + (FlowySvgs.m_toolbar_underline_m, AppFlowyRichTextKeys.underline), + (FlowySvgs.m_toolbar_strike_m, AppFlowyRichTextKeys.strikethrough), ]; @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_block_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart similarity index 93% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_block_items.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart index 00d86be244..57670afadd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_block_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/utils.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; @@ -23,9 +23,9 @@ class BlockItems extends StatelessWidget { final AppFlowyMobileToolbarWidgetService service; final List<(FlowySvgData, String)> _blockItems = [ - (FlowySvgs.m_aa_bulleted_list_s, BulletedListBlockKeys.type), - (FlowySvgs.m_aa_numbered_list_s, NumberedListBlockKeys.type), - (FlowySvgs.m_aa_quote_s, QuoteBlockKeys.type), + (FlowySvgs.m_toolbar_bulleted_list_m, BulletedListBlockKeys.type), + (FlowySvgs.m_toolbar_numbered_list_m, NumberedListBlockKeys.type), + (FlowySvgs.m_aa_quote_m, QuoteBlockKeys.type), ]; @override @@ -82,7 +82,7 @@ class BlockItems extends StatelessWidget { Widget _buildLinkItem(BuildContext context) { final theme = ToolbarColorExtension.of(context); final items = [ - (AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_s), + (AppFlowyRichTextKeys.code, FlowySvgs.m_aa_code_m), // (InlineMathEquationKeys.formula, FlowySvgs.m_aa_math_s), ]; return PopupMenu( @@ -119,7 +119,7 @@ class BlockItems extends StatelessWidget { showDownArrow: true, onTap: _onLinkItemTap, backgroundColor: theme.toolbarMenuItemBackgroundColor, - icon: FlowySvgs.m_aa_link_s, + icon: FlowySvgs.m_toolbar_link_m, isSelected: false, iconPadding: const EdgeInsets.symmetric( vertical: 14.0, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_close_keyboard_or_menu_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart similarity index 56% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_close_keyboard_or_menu_button.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart index c62f110c50..4c91a00bc7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_close_keyboard_or_menu_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart @@ -5,27 +5,20 @@ import 'package:flutter/material.dart'; class CloseKeyboardOrMenuButton extends StatelessWidget { const CloseKeyboardOrMenuButton({ super.key, - required this.showingMenu, required this.onPressed, }); - final bool showingMenu; final VoidCallback onPressed; @override Widget build(BuildContext context) { return SizedBox( width: 62, - height: 46, + height: 42, child: FlowyButton( - margin: showingMenu ? const EdgeInsets.only(right: 0.5) : null, - text: showingMenu - ? const FlowySvg( - FlowySvgs.m_toolbar_show_keyboard_s, - ) - : const FlowySvg( - FlowySvgs.m_toolbar_hide_keyboard_s, - ), + text: const FlowySvg( + FlowySvgs.m_toolbar_keyboard_m, + ), onTap: onPressed, ), ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart similarity index 55% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart index 417c4d5c0a..c5f4b77d62 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_list.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,10 @@ class ColorItem extends StatelessWidget { @override Widget build(BuildContext context) { final theme = ToolbarColorExtension.of(context); - final selectedBackgroundColor = _getBackgroundColor(context); + final String? selectedTextColor = + editorState.getSelectionColor(AppFlowyRichTextKeys.textColor); + final String? selectedBackgroundColor = + editorState.getSelectionColor(AppFlowyRichTextKeys.backgroundColor); return MobileToolbarMenuItemWrapper( size: const Size(82, 52), @@ -43,10 +47,11 @@ class ColorItem extends StatelessWidget { selection: editorState.selection!, ); }, - icon: FlowySvgs.m_aa_color_s, - backgroundColor: - selectedBackgroundColor ?? theme.toolbarMenuItemBackgroundColor, - selectedBackgroundColor: selectedBackgroundColor, + icon: FlowySvgs.m_aa_font_color_m, + iconColor: selectedTextColor?.tryToColor(), + backgroundColor: selectedBackgroundColor?.tryToColor() ?? + theme.toolbarMenuItemBackgroundColor, + selectedBackgroundColor: selectedBackgroundColor?.tryToColor(), isSelected: selectedBackgroundColor != null, showRightArrow: true, iconPadding: const EdgeInsets.only( @@ -56,33 +61,4 @@ class ColorItem extends StatelessWidget { ), ); } - - Color? _getBackgroundColor(BuildContext context) { - final selection = editorState.selection; - if (selection == null) { - return null; - } - String? backgroundColor = - editorState.toggledStyle[AppFlowyRichTextKeys.backgroundColor]; - if (backgroundColor == null) { - if (selection.isCollapsed && selection.startIndex != 0) { - backgroundColor = editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.backgroundColor, - selection.copyWith( - start: selection.start.copyWith( - offset: selection.startIndex - 1, - ), - ), - ); - } else { - backgroundColor = editorState.getDeltaAttributeValueInSelection( - AppFlowyRichTextKeys.backgroundColor, - ); - } - } - if (backgroundColor != null && int.tryParse(backgroundColor) != null) { - return Color(int.parse(backgroundColor)); - } - return null; - } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_list.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart similarity index 88% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_list.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart index 9ca8902143..6b03ff6301 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_list.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_get_selection_color.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -9,6 +10,8 @@ import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; +const _count = 6; + Future showTextColorAndBackgroundColorPicker( BuildContext context, { required EditorState editorState, @@ -26,7 +29,7 @@ Future showTextColorAndBackgroundColorPicker( backgroundColor: theme.toolbarMenuBackgroundColor, elevation: 20, title: LocaleKeys.grid_selectOption_colorPanelTitle.tr(), - padding: const EdgeInsets.fromLTRB(18, 4, 18, 8), + padding: const EdgeInsets.fromLTRB(10, 4, 10, 8), builder: (context) { return _TextColorAndBackgroundColor( editorState: editorState, @@ -63,9 +66,10 @@ class _TextColorAndBackgroundColorState extends State<_TextColorAndBackgroundColor> { @override Widget build(BuildContext context) { - final String? selectedTextColor = _getColor(AppFlowyRichTextKeys.textColor); - final String? selectedBackgroundColor = - _getColor(AppFlowyRichTextKeys.backgroundColor); + final String? selectedTextColor = + widget.editorState.getSelectionColor(AppFlowyRichTextKeys.textColor); + final String? selectedBackgroundColor = widget.editorState + .getSelectionColor(AppFlowyRichTextKeys.backgroundColor); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -79,6 +83,7 @@ class _TextColorAndBackgroundColorState fontSize: 14.0, ), ), + const VSpace(6.0), _TextColors( selectedColor: selectedTextColor?.tryToColor(), onSelectedColor: (textColor) async { @@ -115,6 +120,7 @@ class _TextColorAndBackgroundColorState fontSize: 14.0, ), ), + const VSpace(6.0), _BackgroundColors( selectedColor: selectedBackgroundColor?.tryToColor(), onSelectedColor: (backgroundColor) async { @@ -145,28 +151,6 @@ class _TextColorAndBackgroundColorState ], ); } - - String? _getColor(String key) { - final selection = widget.selection; - String? color = widget.editorState.toggledStyle[key]; - if (color == null) { - if (selection.isCollapsed && selection.startIndex != 0) { - color = widget.editorState.getDeltaAttributeValueInSelection( - key, - selection.copyWith( - start: selection.start.copyWith( - offset: selection.startIndex - 1, - ), - ), - ); - } else { - color = widget.editorState.getDeltaAttributeValueInSelection( - key, - ); - } - } - return color; - } } class _BackgroundColors extends StatelessWidget { @@ -202,7 +186,7 @@ class _BackgroundColors extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.count( - crossAxisCount: 6, + crossAxisCount: _count, shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), children: colors.mapIndexed( @@ -236,9 +220,7 @@ class _BackgroundColorItem extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - margin: const EdgeInsets.all( - 6.0, - ), + margin: const EdgeInsets.all(6.0), decoration: BoxDecoration( color: color, borderRadius: Corners.s12Border, @@ -283,7 +265,7 @@ class _TextColors extends StatelessWidget { @override Widget build(BuildContext context) { return GridView.count( - crossAxisCount: 6, + crossAxisCount: _count, shrinkWrap: true, padding: EdgeInsets.zero, physics: const NeverScrollableScrollPhysics(), @@ -317,9 +299,7 @@ class _TextColorItem extends StatelessWidget { return GestureDetector( onTap: onTap, child: Container( - margin: const EdgeInsets.all( - 6.0, - ), + margin: const EdgeInsets.all(6.0), decoration: BoxDecoration( borderRadius: Corners.s12Border, border: Border.all( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_font_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart similarity index 98% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_font_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart index 13384f9b87..0265ea2c02 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_font_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart @@ -1,13 +1,12 @@ import 'dart:async'; -import 'package:flutter/material.dart'; - import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/util/google_font_family_extension.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_heading_and_text_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart similarity index 94% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_heading_and_text_items.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart index bce94a397c..b98a6fddff 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_heading_and_text_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart @@ -19,25 +19,25 @@ class HeadingsAndTextItems extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ _HeadingOrTextItem( - icon: FlowySvgs.m_aa_h1_s, + icon: FlowySvgs.m_aa_h1_m, blockType: HeadingBlockKeys.type, editorState: editorState, level: 1, ), _HeadingOrTextItem( - icon: FlowySvgs.m_aa_h2_s, + icon: FlowySvgs.m_aa_h2_m, blockType: HeadingBlockKeys.type, editorState: editorState, level: 2, ), _HeadingOrTextItem( - icon: FlowySvgs.m_aa_h3_s, + icon: FlowySvgs.m_aa_h3_m, blockType: HeadingBlockKeys.type, editorState: editorState, level: 3, ), _HeadingOrTextItem( - icon: FlowySvgs.m_aa_text_s, + icon: FlowySvgs.m_aa_paragraph_m, blockType: ParagraphBlockKeys.type, editorState: editorState, ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_indent_items.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart similarity index 92% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_indent_items.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart index 0188ceef2e..2ddcd4dacb 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_indent_items.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -22,7 +22,7 @@ class IndentAndOutdentItems extends StatelessWidget { children: [ MobileToolbarMenuItemWrapper( size: const Size(95, 52), - icon: FlowySvgs.m_aa_outdent_s, + icon: FlowySvgs.m_aa_outdent_m, enable: isOutdentable(editorState), isSelected: false, enableTopRightRadius: false, @@ -37,7 +37,7 @@ class IndentAndOutdentItems extends StatelessWidget { const ScaledVerticalDivider(), MobileToolbarMenuItemWrapper( size: const Size(95, 52), - icon: FlowySvgs.m_aa_indent_s, + icon: FlowySvgs.m_aa_indent_m, enable: isIndentable(editorState), isSelected: false, enableTopLeftRadius: false, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart similarity index 97% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart index 156f743b12..7464514f93 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_menu_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_menu_item.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:flowy_svg/flowy_svg.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart similarity index 98% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart index 6eb948fa1b..d678d7c0ba 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_popup_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_popup_menu.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart similarity index 98% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart index 6bc3d401af..d35b8d56df 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; class ToolbarColorExtension extends ThemeExtension { factory ToolbarColorExtension.light() => const ToolbarColorExtension( - toolbarBackgroundColor: Color(0xFFF3F3F8), + toolbarBackgroundColor: Color(0xFFFFFFFF), toolbarItemIconColor: Color(0xFF1F2329), toolbarItemIconDisabledColor: Color(0xFF999BA0), toolbarItemIconSelectedColor: Color(0x1F232914), - toolbarItemSelectedBackgroundColor: Color(0x1F232914), + toolbarItemSelectedBackgroundColor: Color(0xFFF2F2F2), toolbarMenuBackgroundColor: Color(0xFFFFFFFF), toolbarMenuItemBackgroundColor: Color(0xFFF2F2F7), toolbarMenuItemSelectedBackgroundColor: Color(0xFF00BCF0), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart index c26a33adb2..7489911fb7 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_toolbar_item.dart @@ -1,24 +1,23 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_align_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_bius_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_block_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_font_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_heading_and_text_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_indent_items.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_align_items.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_bius_items.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_block_items.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_font_item.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_heading_and_text_items.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_indent_items.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; final aaToolbarItem = AppFlowyMobileToolbarItem( - pilotAtExpandedSelection: true, itemBuilder: (context, editorState, service, onMenu, _) { return AppFlowyMobileToolbarIconItem( editorState: editorState, isSelected: () => service.showMenuNotifier.value, keepSelectedStatus: true, - icon: FlowySvgs.m_toolbar_aa_s, + icon: FlowySvgs.m_toolbar_aa_m, onTap: () => onMenu?.call(), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart index aac7306be5..0b1ae151d3 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart @@ -7,7 +7,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_block.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_item/mobile_add_block_toolbar_item.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -19,7 +19,7 @@ final addBlockToolbarItem = AppFlowyMobileToolbarItem( itemBuilder: (context, editorState, service, __, onAction) { return AppFlowyMobileToolbarIconItem( editorState: editorState, - icon: FlowySvgs.m_toolbar_add_s, + icon: FlowySvgs.m_toolbar_add_m, onTap: () { final selection = editorState.selection; service.closeKeyboard(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart index e76e7d51ab..bf7d7d098f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'dart:io'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_close_keyboard_or_menu_button.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; @@ -26,13 +26,15 @@ class AppFlowyMobileToolbar extends StatefulWidget { super.key, this.toolbarHeight = 50.0, required this.editorState, - required this.toolbarItems, + required this.toolbarItemsBuilder, required this.child, }); final EditorState editorState; final double toolbarHeight; - final List toolbarItems; + final List Function( + Selection? selection, + ) toolbarItemsBuilder; final Widget child; @override @@ -108,7 +110,7 @@ class _AppFlowyMobileToolbarState extends State { return RepaintBoundary( child: _MobileToolbar( editorState: widget.editorState, - toolbarItems: widget.toolbarItems, + toolbarItems: widget.toolbarItemsBuilder(selection), toolbarHeight: widget.toolbarHeight, ), ); @@ -234,14 +236,14 @@ class _MobileToolbarState extends State<_MobileToolbar> // - otherwise, add a spacer to push the toolbar up when the keyboard is shown return Column( children: [ - Divider( + const Divider( height: 0.5, - color: Colors.grey.withOpacity(0.5), + color: Color(0xFFEDEDED), ), _buildToolbar(context), - Divider( + const Divider( height: 0.5, - color: Colors.grey.withOpacity(0.5), + color: Color(0xFFEDEDED), ), _buildMenuOrSpacer(context), ], @@ -342,62 +344,29 @@ class _MobileToolbarState extends State<_MobileToolbar> }, ), ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 13.0), + child: VerticalDivider( + width: 1.0, + thickness: 1.0, + color: Color(0xFFD9D9D9), + ), + ), // close menu or close keyboard button - ClipRect( - clipper: const _MyClipper( - offset: -20, - ), - child: ValueListenableBuilder( - valueListenable: showMenuNotifier, - builder: (_, showingMenu, __) { - return ValueListenableBuilder( - valueListenable: toolbarOffset, - builder: (_, offset, __) { - final showShadow = offset > 0; - return DecoratedBox( - decoration: BoxDecoration( - color: theme.toolbarBackgroundColor, - boxShadow: showShadow - ? [ - BoxShadow( - color: theme.toolbarShadowColor, - blurRadius: 20, - offset: const Offset(-2, 0), - spreadRadius: -10, - ), - ] - : null, - ), - child: CloseKeyboardOrMenuButton( - showingMenu: showingMenu, - onPressed: () { - if (showingMenu) { - // close the menu and show the keyboard - closeItemMenu(); - _showKeyboard(); - } else { - closeKeyboardInitiative = true; - // close the keyboard and clear the selection - // if the selection is null, the keyboard and the toolbar will be hidden automatically - widget.editorState.selection = null; + CloseKeyboardOrMenuButton( + onPressed: () { + closeKeyboardInitiative = true; + // close the keyboard and clear the selection + // if the selection is null, the keyboard and the toolbar will be hidden automatically + widget.editorState.selection = null; - // sometimes, the keyboard is not closed after the selection is cleared - if (Platform.isAndroid) { - SystemChannels.textInput - .invokeMethod('TextInput.hide'); - } - } - }, - ), - ); - }, - ); - }, - ), - ), - const SizedBox( - width: 4.0, + // sometimes, the keyboard is not closed after the selection is cleared + if (Platform.isAndroid) { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } + }, ), + const HSpace(4.0), ], ), ); @@ -489,7 +458,7 @@ class _ToolbarItemListViewState extends State<_ToolbarItemListView> { @override Widget build(BuildContext context) { final children = [ - const HSpace(16), + const HSpace(8), ...widget.toolbarItems .mapIndexed( (index, element) => element.itemBuilder.call( @@ -567,16 +536,16 @@ class _ToolbarItemListViewState extends State<_ToolbarItemListView> { } } -class _MyClipper extends CustomClipper { - const _MyClipper({ - this.offset = 0, - }); +// class _MyClipper extends CustomClipper { +// const _MyClipper({ +// this.offset = 0, +// }); - final double offset; +// final double offset; - @override - Rect getClip(Size size) => Rect.fromLTWH(offset, 0, 64.0, 46.0); +// @override +// Rect getClip(Size size) => Rect.fromLTWH(offset, 0, 64.0, 46.0); - @override - bool shouldReclip(CustomClipper oldClipper) => false; -} +// @override +// bool shouldReclip(CustomClipper oldClipper) => false; +// } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart index 61654ff5cd..d138e644cd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; @@ -45,6 +45,7 @@ class AppFlowyMobileToolbarIconItem extends StatefulWidget { this.iconBuilder, this.isSelected, this.shouldListenToToggledStyle = false, + this.enable, required this.onTap, required this.editorState, }); @@ -56,6 +57,7 @@ class AppFlowyMobileToolbarIconItem extends StatefulWidget { final bool Function()? isSelected; final bool shouldListenToToggledStyle; final EditorState editorState; + final bool Function()? enable; @override State createState() => @@ -101,32 +103,40 @@ class _AppFlowyMobileToolbarIconItemState @override Widget build(BuildContext context) { final theme = ToolbarColorExtension.of(context); + final enable = widget.enable?.call() ?? true; return Padding( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: const EdgeInsets.symmetric(vertical: 5), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { widget.onTap(); _rebuild(); }, - child: Container( - width: 48, - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: isSelected ? theme.toolbarItemSelectedBackgroundColor : null, - ), - child: widget.iconBuilder?.call(context) ?? - FlowySvg( - widget.icon!, - color: theme.toolbarItemIconColor, + child: widget.iconBuilder?.call(context) ?? + Container( + width: 40, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9), + color: isSelected + ? theme.toolbarItemSelectedBackgroundColor + : null, ), - ), + child: FlowySvg( + widget.icon!, + color: enable + ? theme.toolbarItemIconColor + : theme.toolbarItemIconDisabledColor, + ), + ), ), ); } void _rebuild() { + if (!context.mounted) { + return; + } setState(() { isSelected = (widget.keepSelectedStatus && widget.isSelected == null) ? !isSelected diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biuc_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biusc_toolbar_item.dart similarity index 55% rename from frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biuc_toolbar_item.dart rename to frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biusc_toolbar_item.dart index 16eced6d28..f0bfda04a2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biuc_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/biusc_toolbar_item.dart @@ -1,7 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_color_list.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_color_list.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/widgets.dart'; final boldToolbarItem = AppFlowyMobileToolbarItem( itemBuilder: (context, editorState, _, __, onAction) { @@ -13,7 +14,7 @@ final boldToolbarItem = AppFlowyMobileToolbarItem( AppFlowyRichTextKeys.bold, ) && editorState.toggledStyle[AppFlowyRichTextKeys.bold] != false, - icon: FlowySvgs.m_toolbar_bold_s, + icon: FlowySvgs.m_toolbar_bold_m, onTap: () async => editorState.toggleAttribute( AppFlowyRichTextKeys.bold, selectionExtraInfo: { @@ -32,7 +33,7 @@ final italicToolbarItem = AppFlowyMobileToolbarItem( isSelected: () => editorState.isTextDecorationSelected( AppFlowyRichTextKeys.italic, ), - icon: FlowySvgs.m_toolbar_italic_s, + icon: FlowySvgs.m_toolbar_italic_m, onTap: () async => editorState.toggleAttribute( AppFlowyRichTextKeys.italic, selectionExtraInfo: { @@ -51,7 +52,7 @@ final underlineToolbarItem = AppFlowyMobileToolbarItem( isSelected: () => editorState.isTextDecorationSelected( AppFlowyRichTextKeys.underline, ), - icon: FlowySvgs.m_toolbar_underline_s, + icon: FlowySvgs.m_toolbar_underline_m, onTap: () async => editorState.toggleAttribute( AppFlowyRichTextKeys.underline, selectionExtraInfo: { @@ -62,12 +63,73 @@ final underlineToolbarItem = AppFlowyMobileToolbarItem( }, ); +final strikethroughToolbarItem = AppFlowyMobileToolbarItem( + itemBuilder: (context, editorState, _, __, onAction) { + return AppFlowyMobileToolbarIconItem( + editorState: editorState, + shouldListenToToggledStyle: true, + isSelected: () => editorState.isTextDecorationSelected( + AppFlowyRichTextKeys.strikethrough, + ), + icon: FlowySvgs.m_toolbar_strike_m, + onTap: () async => editorState.toggleAttribute( + AppFlowyRichTextKeys.strikethrough, + selectionExtraInfo: { + selectionExtraInfoDisableFloatingToolbar: true, + }, + ), + ); + }, +); + final colorToolbarItem = AppFlowyMobileToolbarItem( itemBuilder: (context, editorState, service, __, onAction) { return AppFlowyMobileToolbarIconItem( editorState: editorState, shouldListenToToggledStyle: true, - icon: FlowySvgs.m_toolbar_color_s, + icon: FlowySvgs.m_aa_font_color_m, + iconBuilder: (context) { + String? getColor(String key) { + final selection = editorState.selection; + if (selection == null) { + return null; + } + String? color = editorState.toggledStyle[key]; + if (color == null) { + if (selection.isCollapsed && selection.startIndex != 0) { + color = editorState.getDeltaAttributeValueInSelection( + key, + selection.copyWith( + start: selection.start.copyWith( + offset: selection.startIndex - 1, + ), + ), + ); + } else { + color = editorState.getDeltaAttributeValueInSelection( + key, + ); + } + } + return color; + } + + final textColor = getColor(AppFlowyRichTextKeys.textColor); + final backgroundColor = getColor(AppFlowyRichTextKeys.backgroundColor); + + return Container( + width: 40, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9), + color: backgroundColor?.tryToColor(), + ), + child: FlowySvg( + FlowySvgs.m_aa_font_color_m, + color: textColor?.tryToColor(), + ), + ); + }, onTap: () { service.closeKeyboard(); editorState.updateSelectionWithReason( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/checkbox_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/checkbox_toolbar_item.dart deleted file mode 100644 index 2b02bb2e5a..0000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/checkbox_toolbar_item.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; - -final todoListToolbarItem = AppFlowyMobileToolbarItem( - itemBuilder: (context, editorState, _, __, onAction) { - final isSelected = editorState.isBlockTypeSelected(TodoListBlockKeys.type); - return AppFlowyMobileToolbarIconItem( - editorState: editorState, - shouldListenToToggledStyle: true, - keepSelectedStatus: true, - isSelected: () => isSelected, - icon: FlowySvgs.m_toolbar_checkbox_s, - onTap: () async { - await editorState.convertBlockType( - TodoListBlockKeys.type, - extraAttributes: { - TodoListBlockKeys.checked: false, - }, - ); - }, - ); - }, -); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart new file mode 100644 index 0000000000..290fa2d3e0 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/indent_outdent_toolbar_item.dart @@ -0,0 +1,35 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final indentToolbarItem = AppFlowyMobileToolbarItem( + itemBuilder: (context, editorState, _, __, onAction) { + return AppFlowyMobileToolbarIconItem( + editorState: editorState, + shouldListenToToggledStyle: true, + keepSelectedStatus: true, + isSelected: () => false, + enable: () => isIndentable(editorState), + icon: FlowySvgs.m_aa_indent_m, + onTap: () async { + indentCommand.execute(editorState); + }, + ); + }, +); + +final outdentToolbarItem = AppFlowyMobileToolbarItem( + itemBuilder: (context, editorState, _, __, onAction) { + return AppFlowyMobileToolbarIconItem( + editorState: editorState, + shouldListenToToggledStyle: true, + keepSelectedStatus: true, + isSelected: () => false, + enable: () => isOutdentable(editorState), + icon: FlowySvgs.m_aa_outdent_m, + onTap: () async { + outdentCommand.execute(editorState); + }, + ); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart index 5037add78b..912bdb044f 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/keyboard_height_observer.dart @@ -35,7 +35,7 @@ class KeyboardHeightObserver { void notify(double height) { // the keyboard height will notify twice with the same value on Android 14 - if (DeviceOrApplicationInfoTask.androidSDKVersion == 34) { + if (ApplicationInfo.androidSDKVersion == 34) { if (height == 0 && currentKeyboardHeight == 0) { return; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart new file mode 100644 index 0000000000..240ea7072e --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/list_toolbar_item.dart @@ -0,0 +1,61 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final todoListToolbarItem = AppFlowyMobileToolbarItem( + itemBuilder: (context, editorState, _, __, onAction) { + return AppFlowyMobileToolbarIconItem( + editorState: editorState, + shouldListenToToggledStyle: true, + keepSelectedStatus: true, + isSelected: () => false, + icon: FlowySvgs.m_toolbar_checkbox_m, + onTap: () async { + await editorState.convertBlockType( + TodoListBlockKeys.type, + extraAttributes: { + TodoListBlockKeys.checked: false, + }, + ); + }, + ); + }, +); + +final numberedListToolbarItem = AppFlowyMobileToolbarItem( + itemBuilder: (context, editorState, _, __, onAction) { + final isSelected = + editorState.isBlockTypeSelected(NumberedListBlockKeys.type); + return AppFlowyMobileToolbarIconItem( + editorState: editorState, + shouldListenToToggledStyle: true, + keepSelectedStatus: true, + isSelected: () => isSelected, + icon: FlowySvgs.m_toolbar_numbered_list_m, + onTap: () async { + await editorState.convertBlockType( + NumberedListBlockKeys.type, + ); + }, + ); + }, +); + +final bulletedListToolbarItem = AppFlowyMobileToolbarItem( + itemBuilder: (context, editorState, _, __, onAction) { + final isSelected = + editorState.isBlockTypeSelected(BulletedListBlockKeys.type); + return AppFlowyMobileToolbarIconItem( + editorState: editorState, + shouldListenToToggledStyle: true, + keepSelectedStatus: true, + isSelected: () => isSelected, + icon: FlowySvgs.m_toolbar_bulleted_list_m, + onTap: () async { + await editorState.convertBlockType( + BulletedListBlockKeys.type, + ); + }, + ); + }, +); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart new file mode 100644 index 0000000000..adb1feeb35 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/toolbar_item_builder.dart @@ -0,0 +1,81 @@ +import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; + +final _listBlockTypes = [ + BulletedListBlockKeys.type, + NumberedListBlockKeys.type, + TodoListBlockKeys.type, +]; + +final _defaultToolbarItems = [ + addBlockToolbarItem, + aaToolbarItem, + todoListToolbarItem, + bulletedListToolbarItem, + numberedListToolbarItem, + boldToolbarItem, + italicToolbarItem, + underlineToolbarItem, + strikethroughToolbarItem, + colorToolbarItem, + undoToolbarItem, + redoToolbarItem, +]; + +final _listToolbarItems = [ + addBlockToolbarItem, + aaToolbarItem, + outdentToolbarItem, + indentToolbarItem, + todoListToolbarItem, + bulletedListToolbarItem, + numberedListToolbarItem, + boldToolbarItem, + italicToolbarItem, + underlineToolbarItem, + strikethroughToolbarItem, + colorToolbarItem, + undoToolbarItem, + redoToolbarItem, +]; + +final _textToolbarItems = [ + aaToolbarItem, + boldToolbarItem, + italicToolbarItem, + underlineToolbarItem, + strikethroughToolbarItem, + colorToolbarItem, +]; + +/// Calculate the toolbar items based on the current selection. +/// +/// Default: +/// Add, Aa, Todo List, Image, Bulleted List, Numbered List, B, I, U, S, Color, Undo, Redo +/// +/// Selecting text: +/// Aa, B, I, U, S, Color +/// +/// Selecting a list: +/// Add, Aa, Indent, Outdent, Bulleted List, Numbered List, Todo List B, I, U, S +List buildMobileToolbarItems( + EditorState editorState, + Selection? selection, +) { + if (selection == null) { + return []; + } + + if (!selection.isCollapsed) { + return _textToolbarItems; + } + + final allSelectedAreListType = editorState + .getSelectedNodes(selection: selection) + .every((node) => _listBlockTypes.contains(node.type)); + if (allSelectedAreListType) { + return _listToolbarItems; + } + + return _defaultToolbarItems; +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart index 65c4a9b783..5578d8a33c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/undo_redo_toolbar_item.dart @@ -1,21 +1,28 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/widgets.dart'; final undoToolbarItem = AppFlowyMobileToolbarItem( - pilotAtCollapsedSelection: true, itemBuilder: (context, editorState, _, __, onAction) { final theme = ToolbarColorExtension.of(context); return AppFlowyMobileToolbarIconItem( editorState: editorState, iconBuilder: (context) { final canUndo = editorState.undoManager.undoStack.isNonEmpty; - return FlowySvg( - FlowySvgs.m_toolbar_undo_s, - color: canUndo - ? theme.toolbarItemIconColor - : theme.toolbarItemIconDisabledColor, + return Container( + width: 40, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9), + ), + child: FlowySvg( + FlowySvgs.m_toolbar_undo_m, + color: canUndo + ? theme.toolbarItemIconColor + : theme.toolbarItemIconDisabledColor, + ), ); }, onTap: () => undoCommand.execute(editorState), @@ -30,11 +37,18 @@ final redoToolbarItem = AppFlowyMobileToolbarItem( editorState: editorState, iconBuilder: (context) { final canRedo = editorState.undoManager.redoStack.isNonEmpty; - return FlowySvg( - FlowySvgs.m_toolbar_redo_s, - color: canRedo - ? theme.toolbarItemIconColor - : theme.toolbarItemIconDisabledColor, + return Container( + width: 40, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9), + ), + child: FlowySvg( + FlowySvgs.m_toolbar_redo_m, + color: canRedo + ? theme.toolbarItemIconColor + : theme.toolbarItemIconDisabledColor, + ), ); }, onTap: () => redoCommand.execute(editorState), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart index 4c3f1da550..07983ab078 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart @@ -1,5 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -24,6 +24,7 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget { this.showRightArrow = false, this.textPadding = EdgeInsets.zero, required this.onTap, + this.iconColor, }); final Size size; @@ -43,17 +44,20 @@ class MobileToolbarMenuItemWrapper extends StatelessWidget { final Color? backgroundColor; final Color? selectedBackgroundColor; final EdgeInsets textPadding; + final Color? iconColor; @override Widget build(BuildContext context) { final theme = ToolbarColorExtension.of(context); - Color? iconColor; - if (enable != null) { - iconColor = enable! ? null : theme.toolbarMenuIconDisabledColor; - } else { - iconColor = isSelected - ? theme.toolbarMenuIconSelectedColor - : theme.toolbarMenuIconColor; + Color? iconColor = this.iconColor; + if (iconColor == null) { + if (enable != null) { + iconColor = enable! ? null : theme.toolbarMenuIconDisabledColor; + } else { + iconColor = isSelected + ? theme.toolbarMenuIconSelectedColor + : theme.toolbarMenuIconColor; + } } final textColor = enable == false ? theme.toolbarMenuIconDisabledColor : null; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart index 6cfd92f221..f5f1331c94 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/plugins.dart @@ -29,13 +29,16 @@ export 'link_preview/link_preview_cache.dart'; export 'link_preview/link_preview_menu.dart'; export 'math_equation/math_equation_block_component.dart'; export 'math_equation/mobile_math_equation_toolbar_item.dart'; +export 'mobile_floating_toolbar/custom_mobile_floating_toolbar.dart'; export 'mobile_toolbar_v3/aa_toolbar_item.dart'; export 'mobile_toolbar_v3/add_block_toolbar_item.dart'; export 'mobile_toolbar_v3/appflowy_mobile_toolbar.dart'; export 'mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart'; -export 'mobile_toolbar_v3/biuc_toolbar_item.dart'; -export 'mobile_toolbar_v3/checkbox_toolbar_item.dart'; +export 'mobile_toolbar_v3/biusc_toolbar_item.dart'; +export 'mobile_toolbar_v3/indent_outdent_toolbar_item.dart'; +export 'mobile_toolbar_v3/list_toolbar_item.dart'; export 'mobile_toolbar_v3/more_toolbar_item.dart'; +export 'mobile_toolbar_v3/toolbar_item_builder.dart'; export 'mobile_toolbar_v3/undo_redo_toolbar_item.dart'; export 'mobile_toolbar_v3/util.dart'; export 'openai/widgets/auto_completion_node_widget.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/shared/sync_indicator.dart b/frontend/appflowy_flutter/lib/plugins/shared/sync_indicator.dart new file mode 100644 index 0000000000..f70ccf4537 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/shared/sync_indicator.dart @@ -0,0 +1,126 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/application/sync/database_sync_bloc.dart'; +import 'package:appflowy/plugins/document/application/doc_sync_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class DocumentSyncIndicator extends StatelessWidget { + const DocumentSyncIndicator({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + DocumentSyncBloc(view: view)..add(const DocumentSyncEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + // don't show indicator if user is local + if (!state.shouldShowIndicator) { + return const SizedBox.shrink(); + } + final Color color; + final String hintText; + + if (!state.isNetworkConnected) { + color = Colors.grey; + hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr(); + } else { + switch (state.syncState) { + case DocumentSyncState.SyncFinished: + color = Colors.green; + hintText = LocaleKeys.newSettings_syncState_synced.tr(); + break; + case DocumentSyncState.Syncing: + case DocumentSyncState.InitSyncBegin: + color = Colors.yellow; + hintText = LocaleKeys.newSettings_syncState_syncing.tr(); + break; + default: + return const SizedBox.shrink(); + } + } + + return FlowyTooltip( + message: hintText, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + width: 8, + height: 8, + ), + ); + }, + ), + ); + } +} + +class DatabaseSyncIndicator extends StatelessWidget { + const DatabaseSyncIndicator({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => + DatabaseSyncBloc(view: view)..add(const DatabaseSyncEvent.initial()), + child: BlocBuilder( + builder: (context, state) { + // don't show indicator if user is local + if (!state.shouldShowIndicator) { + return const SizedBox.shrink(); + } + final Color color; + final String hintText; + + if (!state.isNetworkConnected) { + color = Colors.grey; + hintText = LocaleKeys.newSettings_syncState_noNetworkConnected.tr(); + } else { + switch (state.syncState) { + case DatabaseSyncState.SyncFinished: + color = Colors.green; + hintText = LocaleKeys.newSettings_syncState_synced.tr(); + break; + case DatabaseSyncState.Syncing: + case DatabaseSyncState.InitSyncBegin: + color = Colors.yellow; + hintText = LocaleKeys.newSettings_syncState_syncing.tr(); + break; + default: + return const SizedBox.shrink(); + } + } + + return FlowyTooltip( + message: hintText, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color, + ), + width: 8, + height: 8, + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart b/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart index 32b993938a..c20ba0db10 100644 --- a/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart +++ b/frontend/appflowy_flutter/lib/shared/af_role_pb_extension.dart @@ -5,12 +5,16 @@ import 'package:easy_localization/easy_localization.dart'; extension AFRolePBExtension on AFRolePB { bool get isOwner => this == AFRolePB.Owner; + bool get isMember => this == AFRolePB.Member; + bool get canInvite => isOwner; bool get canDelete => isOwner; bool get canUpdate => isOwner; + bool get canLeave => this != AFRolePB.Owner; + String get description { switch (this) { case AFRolePB.Owner: diff --git a/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart b/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart index b0624301c1..f2e6d9cc0a 100644 --- a/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart +++ b/frontend/appflowy_flutter/lib/shared/custom_image_cache_manager.dart @@ -1,16 +1,26 @@ import 'package:appflowy/shared/appflowy_cache_manager.dart'; +import 'package:appflowy/startup/tasks/prelude.dart'; +import 'package:file/file.dart' hide FileSystem; +import 'package:file/local.dart'; import 'package:flutter_cache_manager/flutter_cache_manager.dart'; +import 'package:path/path.dart' as p; class CustomImageCacheManager extends CacheManager with ImageCacheManager implements ICache { - CustomImageCacheManager._() : super(Config(key)); + CustomImageCacheManager._() + : super( + Config( + key, + fileSystem: CustomIOFileSystem(key), + ), + ); factory CustomImageCacheManager() => _instance; static final CustomImageCacheManager _instance = CustomImageCacheManager._(); - static const key = 'appflowy_image_cache'; + static const key = 'image_cache'; @override Future cacheSize() async { @@ -24,3 +34,28 @@ class CustomImageCacheManager extends CacheManager await emptyCache(); } } + +class CustomIOFileSystem implements FileSystem { + CustomIOFileSystem(this._cacheKey) : _fileDir = createDirectory(_cacheKey); + final Future _fileDir; + final String _cacheKey; + + static Future createDirectory(String key) async { + final baseDir = await appFlowyApplicationDataDirectory(); + final path = p.join(baseDir.path, key); + + const fs = LocalFileSystem(); + final directory = fs.directory(path); + await directory.create(recursive: true); + return directory; + } + + @override + Future createFile(String name) async { + final directory = await _fileDir; + if (!(await directory.exists())) { + await createDirectory(_cacheKey); + } + return directory.childFile(name); + } +} diff --git a/frontend/appflowy_flutter/lib/shared/feature_flags.dart b/frontend/appflowy_flutter/lib/shared/feature_flags.dart index 83a2a341a0..e2a7b64ece 100644 --- a/frontend/appflowy_flutter/lib/shared/feature_flags.dart +++ b/frontend/appflowy_flutter/lib/shared/feature_flags.dart @@ -1,9 +1,9 @@ -import 'dart:collection'; import 'dart:convert'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:collection/collection.dart'; typedef FeatureFlagMap = Map; @@ -19,16 +19,30 @@ enum FeatureFlag { // used to control the visibility of the members settings // if it's on, you can see the members settings in the settings page - membersSettings; + membersSettings, + + // used to control the sync feature of the document + // if it's on, the document will be synced the events from server in real-time + syncDocument, + + // used to control the sync feature of the database + // if it's on, the collaborators will show in the database + syncDatabase, + + // used for ignore the conflicted feature flag + unknown; static Future initialize() async { final values = await getIt().getWithFormat( KVKeys.featureFlag, (value) => Map.from(jsonDecode(value)).map( - (key, value) => MapEntry( - FeatureFlag.values.firstWhere((e) => e.name == key), - value as bool, - ), + (key, value) { + final k = FeatureFlag.values.firstWhereOrNull( + (e) => e.name == key, + ) ?? + FeatureFlag.unknown; + return MapEntry(k, value as bool); + }, ), ) ?? {}; @@ -67,6 +81,16 @@ enum FeatureFlag { } bool get isOn { + // release this feature in version 0.5.4 + if ([ + FeatureFlag.collaborativeWorkspace, + FeatureFlag.membersSettings, + FeatureFlag.syncDatabase, + FeatureFlag.syncDocument, + ].contains(this)) { + return true; + } + if (_values.containsKey(this)) { return _values[this]!; } @@ -76,6 +100,12 @@ enum FeatureFlag { return false; case FeatureFlag.membersSettings: return false; + case FeatureFlag.syncDocument: + return true; + case FeatureFlag.syncDatabase: + return true; + case FeatureFlag.unknown: + return false; } } @@ -85,6 +115,12 @@ enum FeatureFlag { return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app'; case FeatureFlag.membersSettings: return 'if it\'s on, you can see the members settings in the settings page'; + case FeatureFlag.syncDocument: + return 'if it\'s on, the document will be synced in real-time'; + case FeatureFlag.syncDatabase: + return 'if it\'s on, the collaborators will show in the database'; + case FeatureFlag.unknown: + return ''; } } diff --git a/frontend/appflowy_flutter/lib/shared/list_extension.dart b/frontend/appflowy_flutter/lib/shared/list_extension.dart new file mode 100644 index 0000000000..e701ec3c5e --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/list_extension.dart @@ -0,0 +1,8 @@ +extension Unique on List { + List unique([Id Function(E element)? id]) { + final ids = {}; + final list = [...this]; + list.retainWhere((x) => ids.add(id != null ? id(x) : x as Id)); + return list; + } +} diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index 38a4911da8..0454bea651 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -130,7 +130,7 @@ class FlowyRunner { if (!mode.isUnitTest) ...[ // The DeviceOrApplicationInfoTask should be placed before the AppWidgetTask to fetch the app information. // It is unable to get the device information from the test environment. - const DeviceOrApplicationInfoTask(), + const ApplicationInfoTask(), const HotKeyTask(), if (isSupabaseEnabled) InitSupabaseTask(), if (isAppFlowyCloudEnabled) InitAppFlowyCloudTask(), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index 00f3300bff..4b59782976 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -1,8 +1,5 @@ import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/mobile/application/mobile_router.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; @@ -21,6 +18,8 @@ import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -68,6 +67,7 @@ class InitAppWidgetTask extends LaunchTask { Locale('en'), Locale('es', 'VE'), Locale('eu', 'ES'), + Locale('el', 'GR'), Locale('fr', 'FR'), Locale('fr', 'CA'), Locale('hu', 'HU'), diff --git a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart index 7f174428bf..75c43f64fd 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/appflowy_cloud_task.dart @@ -89,8 +89,10 @@ class AppFlowyCloudDeepLink { Log.error('onDeepLinkError: Unexpected empty deep link callback'); _completer?.complete(FlowyResult.failure(AuthError.emptyDeepLink)); _completer = null; + return; } - return _isAuthCallbackDeepLink(uri!).fold( + + return _isAuthCallbackDeepLink(uri).fold( (_) async { final deviceId = await getDeviceId(); final payload = OauthSignInPB( @@ -101,8 +103,7 @@ class AppFlowyCloudDeepLink { }, ); _stateNotifier?.value = DeepLinkResult(state: DeepLinkState.loading); - final result = - await UserEventOauthSignIn(payload).send().then((value) => value); + final result = await UserEventOauthSignIn(payload).send(); _stateNotifier?.value = DeepLinkResult( state: DeepLinkState.finish, diff --git a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart index 61225b8f58..61e1f52460 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/device_info_task.dart @@ -1,16 +1,20 @@ import 'dart:io'; +import 'package:appflowy_backend/log.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import '../startup.dart'; -class DeviceOrApplicationInfoTask extends LaunchTask { - const DeviceOrApplicationInfoTask(); - +class ApplicationInfo { static int androidSDKVersion = -1; static String applicationVersion = ''; static String buildNumber = ''; + static String deviceId = ''; +} + +class ApplicationInfoTask extends LaunchTask { + const ApplicationInfoTask(); @override Future initialize(LaunchContext context) async { @@ -19,13 +23,41 @@ class DeviceOrApplicationInfoTask extends LaunchTask { if (Platform.isAndroid) { final androidInfo = await deviceInfoPlugin.androidInfo; - androidSDKVersion = androidInfo.version.sdkInt; + ApplicationInfo.androidSDKVersion = androidInfo.version.sdkInt; } if (Platform.isAndroid || Platform.isIOS) { - applicationVersion = packageInfo.version; - buildNumber = packageInfo.buildNumber; + ApplicationInfo.applicationVersion = packageInfo.version; + ApplicationInfo.buildNumber = packageInfo.buildNumber; } + + String? deviceId; + try { + if (Platform.isAndroid) { + final AndroidDeviceInfo androidInfo = + await deviceInfoPlugin.androidInfo; + deviceId = androidInfo.device; + } else if (Platform.isIOS) { + final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; + deviceId = iosInfo.identifierForVendor; + } else if (Platform.isMacOS) { + final MacOsDeviceInfo macInfo = await deviceInfoPlugin.macOsInfo; + deviceId = macInfo.systemGUID; + } else if (Platform.isWindows) { + final WindowsDeviceInfo windowsInfo = + await deviceInfoPlugin.windowsInfo; + deviceId = windowsInfo.deviceId; + } else if (Platform.isLinux) { + final LinuxDeviceInfo linuxInfo = await deviceInfoPlugin.linuxInfo; + deviceId = linuxInfo.machineId; + } else { + deviceId = null; + } + } catch (e) { + Log.error('Failed to get platform version, $e'); + } + + ApplicationInfo.deviceId = deviceId ?? ''; } @override diff --git a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart index 05de151b7a..10d267dfbf 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/generate_router.dart @@ -25,6 +25,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/user/presentation/presentation.dart'; import 'package:appflowy/workspace/presentation/home/desktop_home_screen.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flowy_infra/time/duration.dart'; @@ -53,6 +54,7 @@ GoRouter generateRouter(Widget child) { _mobileHomeSettingPageRoute(), _mobileCloudSettingAppFlowyCloudPageRoute(), _mobileLaunchSettingsPageRoute(), + _mobileFeatureFlagPageRoute(), // view page _mobileEditorScreenRoute(), @@ -219,6 +221,16 @@ GoRoute _mobileLaunchSettingsPageRoute() { ); } +GoRoute _mobileFeatureFlagPageRoute() { + return GoRoute( + parentNavigatorKey: AppGlobals.rootNavKey, + path: FeatureFlagScreen.routeName, + pageBuilder: (context, state) { + return const MaterialExtendedPage(child: FeatureFlagScreen()); + }, + ); +} + GoRoute _mobileHomeTrashPageRoute() { return GoRoute( parentNavigatorKey: AppGlobals.rootNavKey, @@ -463,14 +475,14 @@ GoRoute _signInScreenRoute() { GoRoute _mobileEditorScreenRoute() { return GoRoute( - path: MobileEditorScreen.routeName, + path: MobileDocumentScreen.routeName, parentNavigatorKey: AppGlobals.rootNavKey, pageBuilder: (context, state) { - final id = state.uri.queryParameters[MobileEditorScreen.viewId]!; - final title = state.uri.queryParameters[MobileEditorScreen.viewTitle]; + final id = state.uri.queryParameters[MobileDocumentScreen.viewId]!; + final title = state.uri.queryParameters[MobileDocumentScreen.viewTitle]; return MaterialExtendedPage( - child: MobileEditorScreen(id: id, title: title), + child: MobileDocumentScreen(id: id, title: title), ); }, ); @@ -577,7 +589,7 @@ GoRoute _rootRoute(Widget child) { return GoRoute( path: '/', redirect: (context, state) async { - // Every time before navigating to splash screen, we check if user is already logged in in desktop. It is used to skip showing splash screen when user just changes apperance settings like theme mode. + // Every time before navigating to splash screen, we check if user is already logged in desktop. It is used to skip showing splash screen when user just changes apperance settings like theme mode. final userResponse = await getIt().getUser(); final routeName = userResponse.fold( (user) => DesktopHomeScreen.routeName, diff --git a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart index aabbd6e286..010ffac63e 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/af_cloud_auth_service.dart @@ -72,8 +72,9 @@ class AppFlowyCloudAuthService implements AuthService { throw Exception('AppFlowyCloudDeepLink is not registered'); } } else { - completer - .complete(FlowyResult.failure(AuthError.signInWithOauthError)); + completer.complete( + FlowyResult.failure(AuthError.unableToGetDeepLink), + ); } return completer.future; diff --git a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart index 2d9858c717..06ddd90238 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/auth_error.dart @@ -29,4 +29,8 @@ class AuthError { static final deepLinkError = FlowyError() ..msg = 'DeepLink error' ..code = ErrorCode.Internal; + + static final unableToGetDeepLink = FlowyError() + ..msg = 'Unable to get the deep link' + ..code = ErrorCode.Internal; } diff --git a/frontend/appflowy_flutter/lib/user/application/auth/device_id.dart b/frontend/appflowy_flutter/lib/user/application/auth/device_id.dart index a8758d2c9f..2d7fe580ae 100644 --- a/frontend/appflowy_flutter/lib/user/application/auth/device_id.dart +++ b/frontend/appflowy_flutter/lib/user/application/auth/device_id.dart @@ -12,26 +12,26 @@ Future getDeviceId() async { return "test_device_id"; } - String deviceId = ""; + String? deviceId; try { if (Platform.isAndroid) { final AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; deviceId = androidInfo.device; } else if (Platform.isIOS) { final IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - deviceId = iosInfo.identifierForVendor ?? ""; + deviceId = iosInfo.identifierForVendor; } else if (Platform.isMacOS) { final MacOsDeviceInfo macInfo = await deviceInfo.macOsInfo; - deviceId = macInfo.systemGUID ?? ""; + deviceId = macInfo.systemGUID; } else if (Platform.isWindows) { final WindowsDeviceInfo windowsInfo = await deviceInfo.windowsInfo; - deviceId = windowsInfo.computerName; + deviceId = windowsInfo.deviceId; } else if (Platform.isLinux) { final LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; - deviceId = linuxInfo.machineId ?? ""; + deviceId = linuxInfo.machineId; } } on PlatformException { Log.error('Failed to get platform version'); } - return deviceId; + return deviceId ?? ''; } diff --git a/frontend/appflowy_flutter/lib/user/application/user_listener.dart b/frontend/appflowy_flutter/lib/user/application/user_listener.dart index 2fbacf3b6b..9de85f1c6d 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_listener.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_listener.dart @@ -14,6 +14,9 @@ import 'package:appflowy_backend/rust_stream.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; +typedef DidUserWorkspaceUpdateCallback = void Function( + RepeatedUserWorkspacePB workspaces, +); typedef UserProfileNotifyValue = FlowyResult; typedef AuthNotifyValue = FlowyResult; @@ -27,14 +30,20 @@ class UserListener { UserNotificationParser? _userParser; StreamSubscription? _subscription; PublishNotifier? _profileNotifier = PublishNotifier(); + DidUserWorkspaceUpdateCallback? didUpdateUserWorkspaces; void start({ void Function(UserProfileNotifyValue)? onProfileUpdated, + void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces, }) { if (onProfileUpdated != null) { _profileNotifier?.addPublishListener(onProfileUpdated); } + if (didUpdateUserWorkspaces != null) { + this.didUpdateUserWorkspaces = didUpdateUserWorkspaces; + } + _userParser = UserNotificationParser( id: _userProfile.id.toString(), callback: _userNotificationCallback, @@ -63,6 +72,14 @@ class UserListener { (error) => _profileNotifier?.value = FlowyResult.failure(error), ); break; + case user.UserNotification.DidUpdateUserWorkspaces: + result.map( + (r) { + final value = RepeatedUserWorkspacePB.fromBuffer(r); + didUpdateUserWorkspaces?.call(value); + }, + ); + break; default: break; } @@ -108,6 +125,7 @@ class UserWorkspaceListener { _settingChangedNotifier?.value = FlowyResult.failure(error), ); break; + default: break; } diff --git a/frontend/appflowy_flutter/lib/user/application/user_service.dart b/frontend/appflowy_flutter/lib/user/application/user_service.dart index 34673ae4f4..6838d2822b 100644 --- a/frontend/appflowy_flutter/lib/user/application/user_service.dart +++ b/frontend/appflowy_flutter/lib/user/application/user_service.dart @@ -109,7 +109,7 @@ class UserBackendService { final request = CreateWorkspacePayloadPB.create() ..name = name ..desc = desc; - return FolderEventCreateWorkspace(request).send().then((result) { + return FolderEventCreateFolderWorkspace(request).send().then((result) { return result.fold( (workspace) => FlowyResult.success(workspace), (error) => FlowyResult.failure(error), @@ -150,4 +150,51 @@ class UserBackendService { ..newIcon = icon; return UserEventChangeWorkspaceIcon(request).send(); } + + Future> + getWorkspaceMembers( + String workspaceId, + ) async { + final data = QueryWorkspacePB()..workspaceId = workspaceId; + return UserEventGetWorkspaceMember(data).send(); + } + + Future> addWorkspaceMember( + String workspaceId, + String email, + ) async { + final data = AddWorkspaceMemberPB() + ..workspaceId = workspaceId + ..email = email; + return UserEventAddWorkspaceMember(data).send(); + } + + Future> removeWorkspaceMember( + String workspaceId, + String email, + ) async { + final data = RemoveWorkspaceMemberPB() + ..workspaceId = workspaceId + ..email = email; + return UserEventRemoveWorkspaceMember(data).send(); + } + + Future> updateWorkspaceMember( + String workspaceId, + String email, + AFRolePB role, + ) async { + final data = UpdateWorkspaceMemberPB() + ..workspaceId = workspaceId + ..email = email + ..role = role; + return UserEventUpdateWorkspaceMember(data).send(); + } + + Future> leaveWorkspace( + String workspaceId, + ) async { + final data = UserWorkspaceIdPB.create()..workspaceId = workspaceId; + return UserEventLeaveWorkspace(data).send(); + } } diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart index 49ceb81383..fe02f193cc 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/splash_screen.dart @@ -105,7 +105,7 @@ class SplashScreen extends StatelessWidget { Future _registerIfNeeded() async { final result = await UserEventGetUserProfile().send(); - if (result.isFailure()) { + if (result.isFailure) { await getIt().signUpAsGuest(); } } diff --git a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart index c77650443e..6777beb0e1 100644 --- a/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart +++ b/frontend/appflowy_flutter/lib/util/color_generator/color_generator.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -class ColorGenerator { - static Color generateColorFromString(String string) { - final int hash = - string.codeUnits.fold(0, (int acc, int unit) => acc + unit); +extension type ColorGenerator(String value) { + Color toColor() { + final int hash = value.codeUnits.fold(0, (int acc, int unit) => acc + unit); final double hue = (hash % 360).toDouble(); return HSLColor.fromAHSL(1.0, hue, 0.5, 0.8).toColor(); } diff --git a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart index b7fcbb9443..34925235cb 100644 --- a/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart +++ b/frontend/appflowy_flutter/lib/util/color_to_hex_string.dart @@ -1,8 +1,16 @@ +import 'dart:math' as math; + import 'package:flutter/material.dart'; -extension ColorExtensionn on Color { +extension ColorExtension on Color { /// return a hex string in 0xff000000 format String toHexString() { return '0x${value.toRadixString(16).padLeft(8, '0')}'; } + + /// return a random color + static Color random({double opacity = 1.0}) { + return Color((math.Random().nextDouble() * 0xFFFFFF).toInt()) + .withOpacity(opacity); + } } diff --git a/frontend/appflowy_flutter/lib/util/json_print.dart b/frontend/appflowy_flutter/lib/util/json_print.dart index b73a740249..35824b8212 100644 --- a/frontend/appflowy_flutter/lib/util/json_print.dart +++ b/frontend/appflowy_flutter/lib/util/json_print.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:appflowy_backend/log.dart'; +import 'package:flutter/material.dart'; const JsonEncoder _encoder = JsonEncoder.withIndent(' '); void prettyPrintJson(Object? object) { Log.trace(_encoder.convert(object)); + debugPrint(_encoder.convert(object)); } diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart index 1f88340cc1..7b9e86e16b 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_bloc.dart @@ -27,8 +27,8 @@ class FavoriteBloc extends Bloc { void _dispatch() { on( (event, emit) async { - await event.map( - initial: (e) async { + await event.when( + initial: () async { _listener.start( favoritesUpdated: _onFavoritesUpdated, ); @@ -44,23 +44,23 @@ class FavoriteBloc extends Bloc { ), ); }, - didFavorite: (e) { + fetchFavorites: () async { + final result = await _service.readFavorites(); emit( - state.copyWith(views: [...state.views, ...e.favorite.items]), + result.fold( + (view) => state.copyWith( + views: view.items, + ), + (error) => state.copyWith( + views: [], + ), + ), ); }, - didUnfavorite: (e) { - final views = [...state.views]..removeWhere( - (view) => e.favorite.items.any((item) => item.id == view.id), - ); - emit( - state.copyWith(views: views), - ); - }, - toggle: (e) async { + toggle: (view) async { await _service.toggleFavorite( - e.view.id, - !e.view.isFavorite, + view.id, + !view.isFavorite, ); }, ); @@ -73,9 +73,7 @@ class FavoriteBloc extends Bloc { bool didFavorite, ) { favoriteOrFailed.fold( - (favorite) => didFavorite - ? add(FavoriteEvent.didFavorite(favorite)) - : add(FavoriteEvent.didUnfavorite(favorite)), + (favorite) => add(const FetchFavorites()), (error) => Log.error(error), ); } @@ -84,11 +82,8 @@ class FavoriteBloc extends Bloc { @freezed class FavoriteEvent with _$FavoriteEvent { const factory FavoriteEvent.initial() = Initial; - const factory FavoriteEvent.didFavorite(RepeatedViewPB favorite) = - DidFavorite; - const factory FavoriteEvent.didUnfavorite(RepeatedViewPB favorite) = - DidUnfavorite; const factory FavoriteEvent.toggle(ViewPB view) = ToggleFavorite; + const factory FavoriteEvent.fetchFavorites() = FetchFavorites; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart index 6606ab26eb..0cadf9c91f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/favorite/favorite_listener.dart @@ -37,24 +37,25 @@ class FavoriteListener { FolderNotification ty, FlowyResult result, ) { - if (_favoriteUpdated == null) { - return; - } - - final isFavorite = ty == FolderNotification.DidFavoriteView; - result.fold( - (payload) { - final view = RepeatedViewPB.fromBuffer(payload); - _favoriteUpdated!( - FlowyResult.success(view), - isFavorite, + switch (ty) { + case FolderNotification.DidFavoriteView: + result.onSuccess( + (success) => _favoriteUpdated?.call( + FlowyResult.success(RepeatedViewPB.fromBuffer(success)), + true, + ), ); - }, - (error) => _favoriteUpdated!( - FlowyResult.failure(error), - isFavorite, - ), - ); + case FolderNotification.DidUnfavoriteView: + result.map( + (success) => _favoriteUpdated?.call( + FlowyResult.success(RepeatedViewPB.fromBuffer(success)), + false, + ), + ); + break; + default: + break; + } } Future stop() async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart index 7d24a56b0c..0412a9956d 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/prelude.dart @@ -1,2 +1,2 @@ export 'menu_user_bloc.dart'; -export 'sidebar_root_views_bloc.dart'; +export 'sidebar_sections_bloc.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart deleted file mode 100644 index 1ad50401b7..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_root_views_bloc.dart +++ /dev/null @@ -1,160 +0,0 @@ -import 'dart:async'; - -import 'package:appflowy/workspace/application/workspace/workspace_listener.dart'; -import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; -import 'package:appflowy_backend/log.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_result/appflowy_result.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'sidebar_root_views_bloc.freezed.dart'; - -class SidebarRootViewsBloc - extends Bloc { - SidebarRootViewsBloc() : super(SidebarRootViewState.initial()) { - _dispatch(); - } - - late WorkspaceService _workspaceService; - WorkspaceListener? _listener; - - @override - Future close() async { - await _listener?.stop(); - return super.close(); - } - - void _dispatch() { - on( - (event, emit) async { - await event.when( - initial: (userProfile, workspaceId) async { - _initial(userProfile, workspaceId); - await _fetchApps(emit); - }, - reset: (userProfile, workspaceId) async { - await _listener?.stop(); - _initial(userProfile, workspaceId); - await _fetchApps(emit); - }, - createRootView: (name, desc, index) async { - final result = await _workspaceService.createApp( - name: name, - desc: desc, - index: index, - ); - result.fold( - (view) => emit(state.copyWith(lastCreatedRootView: view)), - (error) { - Log.error(error); - emit( - state.copyWith( - successOrFailure: FlowyResult.failure(error), - ), - ); - }, - ); - }, - didReceiveViews: (viewsOrFailure) async { - emit( - viewsOrFailure.fold( - (views) => state.copyWith( - views: views, - successOrFailure: FlowyResult.success(null), - ), - (err) => - state.copyWith(successOrFailure: FlowyResult.failure(err)), - ), - ); - }, - moveRootView: (int fromIndex, int toIndex) { - if (state.views.length > fromIndex) { - final view = state.views[fromIndex]; - - _workspaceService.moveApp( - appId: view.id, - fromIndex: fromIndex, - toIndex: toIndex, - ); - - final views = List.from(state.views); - views.insert(toIndex, views.removeAt(fromIndex)); - emit(state.copyWith(views: views)); - } - }, - ); - }, - ); - } - - Future _fetchApps(Emitter emit) async { - final viewsOrError = await _workspaceService.getViews(); - emit( - viewsOrError.fold( - (views) => state.copyWith(views: views), - (error) { - Log.error(error); - return state.copyWith(successOrFailure: FlowyResult.failure(error)); - }, - ), - ); - } - - void _handleAppsOrFail(FlowyResult, FlowyError> viewsOrFail) { - viewsOrFail.fold( - (views) => add( - SidebarRootViewsEvent.didReceiveViews(FlowyResult.success(views)), - ), - (error) => add( - SidebarRootViewsEvent.didReceiveViews(FlowyResult.failure(error)), - ), - ); - } - - void _initial(UserProfilePB userProfile, String workspaceId) { - _workspaceService = WorkspaceService(workspaceId: workspaceId); - _listener = WorkspaceListener( - user: userProfile, - workspaceId: workspaceId, - )..start(appsChanged: _handleAppsOrFail); - } -} - -@freezed -class SidebarRootViewsEvent with _$SidebarRootViewsEvent { - const factory SidebarRootViewsEvent.initial( - UserProfilePB userProfile, - String workspaceId, - ) = _Initial; - const factory SidebarRootViewsEvent.reset( - UserProfilePB userProfile, - String workspaceId, - ) = _Reset; - const factory SidebarRootViewsEvent.createRootView( - String name, { - String? desc, - int? index, - }) = _createRootView; - const factory SidebarRootViewsEvent.moveRootView(int fromIndex, int toIndex) = - _MoveRootView; - const factory SidebarRootViewsEvent.didReceiveViews( - FlowyResult, FlowyError> appsOrFail, - ) = _ReceiveApps; -} - -@freezed -class SidebarRootViewState with _$SidebarRootViewState { - const factory SidebarRootViewState({ - required List views, - required FlowyResult successOrFailure, - @Default(null) ViewPB? lastCreatedRootView, - }) = _SidebarRootViewState; - - factory SidebarRootViewState.initial() => SidebarRootViewState( - views: [], - successOrFailure: FlowyResult.success(null), - ); -} diff --git a/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart new file mode 100644 index 0000000000..8f3e4d1f59 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/menu/sidebar_sections_bloc.dart @@ -0,0 +1,261 @@ +import 'dart:async'; + +import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart'; +import 'package:appflowy/workspace/application/workspace/workspace_service.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sidebar_sections_bloc.freezed.dart'; + +class SidebarSection { + const SidebarSection({ + required this.publicViews, + required this.privateViews, + }); + + const SidebarSection.empty() + : publicViews = const [], + privateViews = const []; + + final List publicViews; + final List privateViews; + + List get views => publicViews + privateViews; + + SidebarSection copyWith({ + List? publicViews, + List? privateViews, + }) { + return SidebarSection( + publicViews: publicViews ?? this.publicViews, + privateViews: privateViews ?? this.privateViews, + ); + } +} + +/// The [SidebarSectionsBloc] is responsible for +/// managing the root views in different sections of the workspace. +class SidebarSectionsBloc + extends Bloc { + SidebarSectionsBloc() : super(SidebarSectionsState.initial()) { + on( + (event, emit) async { + await event.when( + initial: (userProfile, workspaceId) async { + _initial(userProfile, workspaceId); + final sectionViews = await _getSectionViews(); + if (sectionViews != null) { + emit( + state.copyWith( + section: sectionViews, + ), + ); + } + }, + reset: (userProfile, workspaceId) async { + _reset(userProfile, workspaceId); + final sectionViews = await _getSectionViews(); + if (sectionViews != null) { + emit( + state.copyWith( + section: sectionViews, + ), + ); + } + }, + createRootViewInSection: (name, section, desc, index) async { + final result = await _workspaceService.createView( + name: name, + viewSection: section, + desc: desc, + index: index, + ); + result.fold( + (view) => emit( + state.copyWith( + lastCreatedRootView: view, + createRootViewResult: FlowyResult.success(null), + ), + ), + (error) { + Log.error('Failed to create root view: $error'); + emit( + state.copyWith( + createRootViewResult: FlowyResult.failure(error), + ), + ); + }, + ); + }, + receiveSectionViewsUpdate: (sectionViews) async { + final section = sectionViews.section; + switch (section) { + case ViewSectionPB.Public: + emit( + state.copyWith( + section: state.section.copyWith( + publicViews: sectionViews.views, + ), + ), + ); + case ViewSectionPB.Private: + emit( + state.copyWith( + section: state.section.copyWith( + privateViews: sectionViews.views, + ), + ), + ); + break; + default: + break; + } + }, + moveRootView: (fromIndex, toIndex, fromSection, toSection) async { + final views = fromSection == ViewSectionPB.Public + ? List.from(state.section.publicViews) + : List.from(state.section.privateViews); + if (fromIndex < 0 || fromIndex >= views.length) { + Log.error( + 'Invalid fromIndex: $fromIndex, maxIndex: ${views.length - 1}', + ); + return; + } + final view = views[fromIndex]; + final result = await _workspaceService.moveView( + viewId: view.id, + fromIndex: fromIndex, + toIndex: toIndex, + ); + result.fold( + (value) { + views.insert(toIndex, views.removeAt(fromIndex)); + var newState = state; + if (fromSection == ViewSectionPB.Public) { + newState = newState.copyWith( + section: newState.section.copyWith(publicViews: views), + ); + } else if (fromSection == ViewSectionPB.Private) { + newState = newState.copyWith( + section: newState.section.copyWith(privateViews: views), + ); + } + emit(newState); + }, + (error) { + Log.error('Failed to move root view: $error'); + }, + ); + }, + ); + }, + ); + } + + late WorkspaceService _workspaceService; + WorkspaceSectionsListener? _listener; + + @override + Future close() async { + await _listener?.stop(); + _listener = null; + return super.close(); + } + + ViewSectionPB? getViewSection(ViewPB view) { + final publicViews = state.section.publicViews.map((e) => e.id); + final privateViews = state.section.privateViews.map((e) => e.id); + if (publicViews.contains(view.id)) { + return ViewSectionPB.Public; + } else if (privateViews.contains(view.id)) { + return ViewSectionPB.Private; + } else { + return null; + } + } + + Future _getSectionViews() async { + try { + final publicViews = await _workspaceService.getPublicViews().getOrThrow(); + final privateViews = + await _workspaceService.getPrivateViews().getOrThrow(); + return SidebarSection( + publicViews: publicViews, + privateViews: privateViews, + ); + } catch (e) { + Log.error('Failed to get section views: $e'); + return null; + } + } + + void _initial(UserProfilePB userProfile, String workspaceId) { + _workspaceService = WorkspaceService(workspaceId: workspaceId); + + _listener = WorkspaceSectionsListener( + user: userProfile, + workspaceId: workspaceId, + )..start( + sectionChanged: (result) { + if (!isClosed) { + result.fold( + (s) => add(SidebarSectionsEvent.receiveSectionViewsUpdate(s)), + (f) => Log.error('Failed to receive section views: $f'), + ); + } + }, + ); + } + + void _reset(UserProfilePB userProfile, String workspaceId) { + _listener?.stop(); + _listener = null; + + _initial(userProfile, workspaceId); + } +} + +@freezed +class SidebarSectionsEvent with _$SidebarSectionsEvent { + const factory SidebarSectionsEvent.initial( + UserProfilePB userProfile, + String workspaceId, + ) = _Initial; + const factory SidebarSectionsEvent.reset( + UserProfilePB userProfile, + String workspaceId, + ) = _Reset; + const factory SidebarSectionsEvent.createRootViewInSection({ + required String name, + required ViewSectionPB viewSection, + String? desc, + int? index, + }) = _CreateRootViewInSection; + const factory SidebarSectionsEvent.moveRootView({ + required int fromIndex, + required int toIndex, + required ViewSectionPB fromSection, + required ViewSectionPB toSection, + }) = _MoveRootView; + const factory SidebarSectionsEvent.receiveSectionViewsUpdate( + SectionViewsPB sectionViews, + ) = _ReceiveSectionViewsUpdate; +} + +@freezed +class SidebarSectionsState with _$SidebarSectionsState { + const factory SidebarSectionsState({ + required SidebarSection section, + @Default(null) ViewPB? lastCreatedRootView, + FlowyResult? createRootViewResult, + }) = _SidebarSectionsState; + + factory SidebarSectionsState.initial() => const SidebarSectionsState( + section: SidebarSection.empty(), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart index 6057ddc8d2..2cbbf5476e 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/settings/appearance/mobile_appearance.dart @@ -1,5 +1,5 @@ // ThemeData in mobile -import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; +import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_toolbar_theme.dart'; import 'package:appflowy/workspace/application/settings/appearance/base_appearance.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart index 51891a0be9..e82b54241a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/sidebar/folder/folder_bloc.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -10,7 +11,19 @@ part 'folder_bloc.freezed.dart'; enum FolderCategoryType { favorite, - personal, + private, + public; + + ViewSectionPB get toViewSectionPB { + switch (this) { + case FolderCategoryType.private: + return ViewSectionPB.Private; + case FolderCategoryType.public: + return ViewSectionPB.Public; + case FolderCategoryType.favorite: + throw UnimplementedError(); + } + } } class FolderBloc extends Bloc { diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart index c36a557c42..1ba04629c4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart @@ -1,5 +1,3 @@ -import 'package:flutter/foundation.dart'; - import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; @@ -8,11 +6,12 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:bloc/bloc.dart'; +import 'package:flutter/foundation.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +part 'tabs_bloc.freezed.dart'; part 'tabs_event.dart'; part 'tabs_state.dart'; -part 'tabs_bloc.freezed.dart'; class TabsBloc extends Bloc { TabsBloc() : super(TabsState()) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart index 8d33334c8d..92c663c614 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/user/user_workspace_bloc.dart @@ -1,9 +1,18 @@ +import 'package:appflowy/core/config/kv.dart'; +import 'package:appflowy/core/config/kv_keys.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/feature_flags.dart'; +import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/user_listener.dart'; import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; +import 'package:collection/collection.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -15,140 +24,150 @@ class UserWorkspaceBloc extends Bloc { UserWorkspaceBloc({ required this.userProfile, }) : _userService = UserBackendService(userId: userProfile.id), + _listener = UserListener(userProfile: userProfile), super(UserWorkspaceState.initial()) { on( (event, emit) async { await event.when( initial: () async { - // do nothing - }, - workspacesReceived: (workspaceId) async {}, - fetchWorkspaces: () async { + _listener + ..didUpdateUserWorkspaces = (workspaces) { + add(UserWorkspaceEvent.updateWorkspaces(workspaces)); + } + ..start(); + final result = await _fetchWorkspaces(); - if (result != null) { - emit( - state.copyWith( - currentWorkspace: result.$1, - workspaces: result.$2, - ), - ); - } - }, - createWorkspace: (name, desc) async { - final result = await _userService.createUserWorkspace(name); - final (workspaces, createWorkspaceResult) = result.fold( - (s) { - final workspaces = [...state.workspaces, s]; - return ( - workspaces, - FlowyResult.success(null) + final currentWorkspace = result.$1; + final workspaces = result.$2; + final isCollabWorkspaceOn = + userProfile.authenticator != AuthenticatorPB.Local && + FeatureFlag.collaborativeWorkspace.isOn; + if (currentWorkspace != null && result.$3 == true) { + final result = await _userService + .openWorkspace(currentWorkspace.workspaceId); + result.onSuccess((s) async { + await getIt().set( + KVKeys.lastOpenedWorkspaceId, + currentWorkspace.workspaceId, ); - }, - (e) { - Log.error(e); - return (state.workspaces, FlowyResult.failure(e)); - }, - ); + }); + } emit( state.copyWith( - openWorkspaceResult: null, - deleteWorkspaceResult: null, - updateWorkspaceIconResult: null, - createWorkspaceResult: createWorkspaceResult, + currentWorkspace: currentWorkspace, workspaces: workspaces, + isCollabWorkspaceOn: isCollabWorkspaceOn, + actionResult: null, ), ); }, + fetchWorkspaces: () async { + final result = await _fetchWorkspaces(); + emit( + state.copyWith( + currentWorkspace: result.$1, + workspaces: result.$2, + ), + ); + }, + createWorkspace: (name) async { + final result = await _userService.createUserWorkspace(name); + final workspaces = result.fold( + (s) => [...state.workspaces, s], + (e) => state.workspaces, + ); + emit( + state.copyWith( + workspaces: workspaces, + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.create, + result: result, + ), + ), + ); + // open the created workspace by default + result.onSuccess((s) { + add(OpenWorkspace(s.workspaceId)); + }); + }, deleteWorkspace: (workspaceId) async { - if (state.workspaces.length <= 1) { - // do not allow to delete the last workspace + final remoteWorkspaces = await _fetchWorkspaces().then( + (value) => value.$2, + ); + if (state.workspaces.length <= 1 || remoteWorkspaces.length <= 1) { + // do not allow to delete the last workspace, otherwise the user + // cannot do create workspace again + final result = FlowyResult.failure( + FlowyError( + code: ErrorCode.Internal, + msg: LocaleKeys.workspace_cannotDeleteTheOnlyWorkspace.tr(), + ), + ); return emit( state.copyWith( - openWorkspaceResult: null, - createWorkspaceResult: null, - updateWorkspaceIconResult: null, - renameWorkspaceResult: null, - deleteWorkspaceResult: FlowyResult.failure( - FlowyError( - code: ErrorCode.Internal, - msg: 'Cannot delete the last workspace', - ), + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.delete, + result: result, ), ), ); } final result = await _userService.deleteWorkspaceById(workspaceId); - final (workspaces, deleteWorkspaceResult) = result.fold( - (s) { - // if the current workspace is deleted, open the first workspace - if (state.currentWorkspace?.workspaceId == workspaceId) { - add(OpenWorkspace(state.workspaces.first.workspaceId)); - } - // remove the deleted workspace from the list instead of fetching - // the workspaces again - final workspaces = [...state.workspaces]..removeWhere( - (e) => e.workspaceId == workspaceId, - ); - return ( - workspaces, - FlowyResult.success(null) - ); - }, - (e) { - Log.error(e); - return (state.workspaces, FlowyResult.failure(e)); - }, + final workspaces = result.fold( + // remove the deleted workspace from the list instead of fetching + // the workspaces again + (s) => state.workspaces + .where((e) => e.workspaceId != workspaceId) + .toList(), + (e) => state.workspaces, ); - + result.onSuccess((_) { + // if the current workspace is deleted, open the first workspace + if (state.currentWorkspace?.workspaceId == workspaceId) { + add(OpenWorkspace(workspaces.first.workspaceId)); + } + }); emit( state.copyWith( - openWorkspaceResult: null, - createWorkspaceResult: null, - updateWorkspaceIconResult: null, - renameWorkspaceResult: null, - deleteWorkspaceResult: deleteWorkspaceResult, workspaces: workspaces, + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.delete, + result: result, + ), ), ); }, openWorkspace: (workspaceId) async { - final (currentWorkspace, openWorkspaceResult) = - await _userService.openWorkspace(workspaceId).fold( - (s) { - final openedWorkspace = state.workspaces.firstWhere( - (e) => e.workspaceId == workspaceId, - ); - return ( - openedWorkspace, - FlowyResult.success(null) - ); - }, - (f) { - Log.error(f); - return (state.currentWorkspace, FlowyResult.failure(f)); - }, + final result = await _userService.openWorkspace(workspaceId); + final currentWorkspace = result.fold( + (s) => state.workspaces.firstWhereOrNull( + (e) => e.workspaceId == workspaceId, + ), + (e) => state.currentWorkspace, ); - + result.onSuccess((_) async { + await getIt().set( + KVKeys.lastOpenedWorkspaceId, + workspaceId, + ); + }); emit( state.copyWith( - createWorkspaceResult: null, - deleteWorkspaceResult: null, - updateWorkspaceIconResult: null, - openWorkspaceResult: openWorkspaceResult, currentWorkspace: currentWorkspace, + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.open, + result: result, + ), ), ); }, renameWorkspace: (workspaceId, name) async { - final result = await _userService.renameWorkspace( - workspaceId, - name, - ); - final (workspaces, currentWorkspace, renameWorkspaceResult) = - result.fold( - (s) { - final workspaces = state.workspaces.map((e) { + final result = + await _userService.renameWorkspace(workspaceId, name); + final workspaces = result.fold( + (s) => state.workspaces.map( + (e) { if (e.workspaceId == workspaceId) { e.freeze(); return e.rebuild((p0) { @@ -156,36 +175,21 @@ class UserWorkspaceBloc extends Bloc { }); } return e; - }).toList(); - - final currentWorkspace = workspaces.firstWhere( - (e) => e.workspaceId == state.currentWorkspace?.workspaceId, - ); - - return ( - workspaces, - currentWorkspace, - FlowyResult.success(null), - ); - }, - (e) { - Log.error(e); - return ( - state.workspaces, - state.currentWorkspace, - FlowyResult.failure(e), - ); - }, + }, + ).toList(), + (f) => state.workspaces, + ); + final currentWorkspace = workspaces.firstWhere( + (e) => e.workspaceId == state.currentWorkspace?.workspaceId, ); emit( state.copyWith( - createWorkspaceResult: null, - deleteWorkspaceResult: null, - openWorkspaceResult: null, - updateWorkspaceIconResult: null, workspaces: workspaces, currentWorkspace: currentWorkspace, - renameWorkspaceResult: renameWorkspaceResult, + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.rename, + result: result, + ), ), ); }, @@ -194,49 +198,62 @@ class UserWorkspaceBloc extends Bloc { workspaceId, icon, ); - - final (workspaces, currentWorkspace, updateWorkspaceIconResult) = - result.fold( - (s) { - final workspaces = state.workspaces.map((e) { + final workspaces = result.fold( + (s) => state.workspaces.map( + (e) { if (e.workspaceId == workspaceId) { e.freeze(); return e.rebuild((p0) { - // TODO(Lucas): the icon is not ready in the backend + p0.icon = icon; }); } return e; - }).toList(); - - final currentWorkspace = workspaces.firstWhere( - (e) => e.workspaceId == state.currentWorkspace?.workspaceId, - ); - - return ( - workspaces, - currentWorkspace, - FlowyResult.success(null), - ); - }, - (e) { - Log.error(e); - return ( - state.workspaces, - state.currentWorkspace, - FlowyResult.failure(e), - ); - }, + }, + ).toList(), + (f) => state.workspaces, + ); + final currentWorkspace = workspaces.firstWhere( + (e) => e.workspaceId == state.currentWorkspace?.workspaceId, ); - emit( state.copyWith( - createWorkspaceResult: null, - deleteWorkspaceResult: null, - openWorkspaceResult: null, - renameWorkspaceResult: null, - updateWorkspaceIconResult: updateWorkspaceIconResult, workspaces: workspaces, currentWorkspace: currentWorkspace, + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.updateIcon, + result: result, + ), + ), + ); + }, + leaveWorkspace: (workspaceId) async { + final result = await _userService.leaveWorkspace(workspaceId); + final workspaces = result.fold( + (s) => state.workspaces + .where((e) => e.workspaceId != workspaceId) + .toList(), + (e) => state.workspaces, + ); + result.onSuccess((_) { + // if leaving the current workspace, open the first workspace + if (state.currentWorkspace?.workspaceId == workspaceId) { + add(OpenWorkspace(workspaces.first.workspaceId)); + } + }); + emit( + state.copyWith( + workspaces: workspaces, + actionResult: UserWorkspaceActionResult( + actionType: UserWorkspaceActionType.leave, + result: result, + ), + ), + ); + }, + updateWorkspaces: (workspaces) async { + emit( + state.copyWith( + workspaces: workspaces.items, ), ); }, @@ -245,31 +262,70 @@ class UserWorkspaceBloc extends Bloc { ); } + @override + Future close() { + _listener.stop(); + return super.close(); + } + final UserProfilePB userProfile; final UserBackendService _userService; + final UserListener _listener; - Future<(UserWorkspacePB currentWorkspace, List workspaces)?> - _fetchWorkspaces() async { + Future< + ( + UserWorkspacePB? currentWorkspace, + List workspaces, + bool shouldOpenWorkspace, + )> _fetchWorkspaces() async { try { + final lastOpenedWorkspaceId = await getIt().get( + KVKeys.lastOpenedWorkspaceId, + ); final currentWorkspace = await _userService.getCurrentWorkspace().getOrThrow(); final workspaces = await _userService.getWorkspaces().getOrThrow(); - final currentWorkspaceInList = - workspaces.firstWhere((e) => e.workspaceId == currentWorkspace.id); - return (currentWorkspaceInList, workspaces); + if (workspaces.isEmpty) { + workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace)); + } + UserWorkspacePB? currentWorkspaceInList = workspaces + .firstWhereOrNull((e) => e.workspaceId == currentWorkspace.id); + if (lastOpenedWorkspaceId != null) { + final lastOpenedWorkspace = workspaces + .firstWhereOrNull((e) => e.workspaceId == lastOpenedWorkspaceId); + if (lastOpenedWorkspace != null) { + currentWorkspaceInList = lastOpenedWorkspace; + } + } + currentWorkspaceInList ??= workspaces.firstOrNull; + return ( + currentWorkspaceInList, + workspaces + ..sort( + (a, b) => a.createdAtTimestamp.compareTo(b.createdAtTimestamp), + ), + lastOpenedWorkspaceId != currentWorkspace.id + ); } catch (e) { - Log.error(e); - return null; + Log.error('fetch workspace error: $e'); + return (null, [], false); } } + + UserWorkspacePB convertWorkspacePBToUserWorkspace(WorkspacePB workspace) { + return UserWorkspacePB.create() + ..workspaceId = workspace.id + ..name = workspace.name + ..createdAtTimestamp = workspace.createTime; + } } @freezed class UserWorkspaceEvent with _$UserWorkspaceEvent { const factory UserWorkspaceEvent.initial() = Initial; - const factory UserWorkspaceEvent.createWorkspace(String name, String desc) = - CreateWorkspace; const factory UserWorkspaceEvent.fetchWorkspaces() = FetchWorkspaces; + const factory UserWorkspaceEvent.createWorkspace(String name) = + CreateWorkspace; const factory UserWorkspaceEvent.deleteWorkspace(String workspaceId) = DeleteWorkspace; const factory UserWorkspaceEvent.openWorkspace(String workspaceId) = @@ -282,23 +338,60 @@ class UserWorkspaceEvent with _$UserWorkspaceEvent { String workspaceId, String icon, ) = _UpdateWorkspaceIcon; - const factory UserWorkspaceEvent.workspacesReceived( - FlowyResult, FlowyError> workspacesOrFail, - ) = WorkspacesReceived; + const factory UserWorkspaceEvent.leaveWorkspace(String workspaceId) = + LeaveWorkspace; + const factory UserWorkspaceEvent.updateWorkspaces( + RepeatedUserWorkspacePB workspaces, + ) = UpdateWorkspaces; +} + +enum UserWorkspaceActionType { + none, + create, + delete, + open, + rename, + updateIcon, + fetchWorkspaces, + leave; +} + +class UserWorkspaceActionResult { + const UserWorkspaceActionResult({ + required this.actionType, + required this.result, + }); + + final UserWorkspaceActionType actionType; + final FlowyResult result; } @freezed class UserWorkspaceState with _$UserWorkspaceState { + const UserWorkspaceState._(); + const factory UserWorkspaceState({ - required UserWorkspacePB? currentWorkspace, - required List workspaces, - @Default(null) FlowyResult? createWorkspaceResult, - @Default(null) FlowyResult? deleteWorkspaceResult, - @Default(null) FlowyResult? openWorkspaceResult, - @Default(null) FlowyResult? renameWorkspaceResult, - @Default(null) FlowyResult? updateWorkspaceIconResult, + @Default(null) UserWorkspacePB? currentWorkspace, + @Default([]) List workspaces, + @Default(null) UserWorkspaceActionResult? actionResult, + @Default(false) bool isCollabWorkspaceOn, }) = _UserWorkspaceState; - factory UserWorkspaceState.initial() => - const UserWorkspaceState(currentWorkspace: null, workspaces: []); + factory UserWorkspaceState.initial() => const UserWorkspaceState(); + + @override + int get hashCode => runtimeType.hashCode; + + final DeepCollectionEquality _deepCollectionEquality = + const DeepCollectionEquality(); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is UserWorkspaceState && + other.currentWorkspace == currentWorkspace && + _deepCollectionEquality.equals(other.workspaces, workspaces) && + identical(other.actionResult, actionResult); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart index a6df92fa23..39b7862134 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_bloc.dart @@ -165,6 +165,8 @@ class ViewBloc extends Bloc { viewId: value.from.id, newParentId: value.newParentId, prevViewId: value.prevId, + fromSection: value.fromSection, + toSection: value.toSection, ); emit( result.fold( @@ -184,8 +186,8 @@ class ViewBloc extends Bloc { layoutType: e.layoutType, ext: {}, openAfterCreate: e.openAfterCreated, + section: e.section, ); - emit( result.fold( (view) => state.copyWith( @@ -205,6 +207,13 @@ class ViewBloc extends Bloc { ), ); }, + updateViewVisibility: (value) async { + final view = value.view; + await ViewBackendService.updateViewsVisibility( + [view], + value.isPublic, + ); + }, ); }, ); @@ -353,18 +362,23 @@ class ViewEvent with _$ViewEvent { ViewPB from, String newParentId, String? prevId, + ViewSectionPB? fromSection, + ViewSectionPB? toSection, ) = Move; const factory ViewEvent.createView( String name, ViewLayoutPB layoutType, { /// open the view after created @Default(true) bool openAfterCreated, + ViewSectionPB? section, }) = CreateView; const factory ViewEvent.viewDidUpdate( FlowyResult result, ) = ViewDidUpdate; const factory ViewEvent.viewUpdateChildView(ViewPB result) = ViewUpdateChildView; + const factory ViewEvent.updateViewVisibility(ViewPB view, bool isPublic) = + UpdateViewVisibility; } @freezed diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart index 395699f147..476f80b484 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_ext.dart @@ -96,7 +96,7 @@ extension ViewExtension on ViewPB { } FlowyResult parent = await ViewBackendService.getView(parentViewId); - while (parent.isSuccess()) { + while (parent.isSuccess) { // parent is not null final view = parent.fold((s) => s, (e) => null); if (view == null || (!includeRoot && view.parentViewId.isEmpty)) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart index 893a11d9b1..ac726b687f 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/view/view_service.dart @@ -37,6 +37,7 @@ class ViewBackendService { /// The [index] is the index of the view in the parent view. /// If the index is null, the view will be added to the end of the list. int? index, + ViewSectionPB? section, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = parentViewId @@ -58,6 +59,10 @@ class ViewBackendService { payload.index = index; } + if (section != null) { + payload.section = section; + } + return FolderEventCreateView(payload).send(); } @@ -195,11 +200,15 @@ class ViewBackendService { required String viewId, required String newParentId, required String? prevViewId, + ViewSectionPB? fromSection, + ViewSectionPB? toSection, }) { final payload = MoveNestedViewPayloadPB( viewId: viewId, newParentId: newParentId, prevViewId: prevViewId, + fromSection: fromSection, + toSection: toSection, ); return FolderEventMoveNestedView(payload).send(); @@ -271,4 +280,15 @@ class ViewBackendService { ); }); } + + static Future> updateViewsVisibility( + List views, + bool isPublic, + ) async { + final payload = UpdateViewVisibilityStatusPayloadPB( + viewIds: views.map((e) => e.id).toList(), + isPublic: isPublic, + ); + return FolderEventUpdateViewVisibilityStatus(payload).send(); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart index 4c5ba13ec0..fb3beb4dd5 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_listener.dart @@ -11,23 +11,28 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' import 'package:appflowy_result/appflowy_result.dart'; import 'package:flowy_infra/notifier.dart'; -typedef AppListNotifyValue = FlowyResult, FlowyError>; +typedef RootViewsNotifyValue = FlowyResult, FlowyError>; typedef WorkspaceNotifyValue = FlowyResult; +/// The [WorkspaceListener] listens to the changes including the below: +/// +/// - The root views of the workspace. (Not including the views are inside the root views) +/// - The workspace itself. class WorkspaceListener { WorkspaceListener({required this.user, required this.workspaceId}); final UserProfilePB user; final String workspaceId; - PublishNotifier? _appsChangedNotifier = PublishNotifier(); + PublishNotifier? _appsChangedNotifier = + PublishNotifier(); PublishNotifier? _workspaceUpdatedNotifier = PublishNotifier(); FolderNotificationListener? _listener; void start({ - void Function(AppListNotifyValue)? appsChanged, + void Function(RootViewsNotifyValue)? appsChanged, void Function(WorkspaceNotifyValue)? onWorkspaceUpdated, }) { if (appsChanged != null) { diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart new file mode 100644 index 0000000000..73c2a9045f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_sections_listener.dart @@ -0,0 +1,68 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:appflowy/core/notification/folder_notification.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/notification.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' + show UserProfilePB; +import 'package:appflowy_result/appflowy_result.dart'; +import 'package:flowy_infra/notifier.dart'; + +typedef SectionNotifyValue = FlowyResult; + +/// The [WorkspaceSectionsListener] listens to the changes including the below: +/// +/// - The root views inside different section of the workspace. (Not including the views are inside the root views) +/// depends on the section type(s). +class WorkspaceSectionsListener { + WorkspaceSectionsListener({ + required this.user, + required this.workspaceId, + }); + + final UserProfilePB user; + final String workspaceId; + + final _sectionNotifier = PublishNotifier(); + late final FolderNotificationListener _listener; + + void start({ + void Function(SectionNotifyValue)? sectionChanged, + }) { + if (sectionChanged != null) { + _sectionNotifier.addPublishListener(sectionChanged); + } + + _listener = FolderNotificationListener( + objectId: workspaceId, + handler: _handleObservableType, + ); + } + + void _handleObservableType( + FolderNotification ty, + FlowyResult result, + ) { + switch (ty) { + case FolderNotification.DidUpdateSectionViews: + final FlowyResult value = result.fold( + (s) => FlowyResult.success( + SectionViewsPB.fromBuffer(s), + ), + (f) => FlowyResult.failure(f), + ); + _sectionNotifier.value = value; + break; + default: + break; + } + } + + Future stop() async { + _sectionNotifier.dispose(); + + await _listener.stop(); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart index 3c29c9a4c1..6e42b744f6 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/workspace/workspace_service.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart' - show CreateViewPayloadPB, MoveViewPayloadPB, ViewLayoutPB, ViewPB; -import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart'; import 'package:appflowy_result/appflowy_result.dart'; class WorkspaceService { @@ -12,15 +10,18 @@ class WorkspaceService { final String workspaceId; - Future> createApp({ + Future> createView({ required String name, + required ViewSectionPB viewSection, String? desc, int? index, }) { final payload = CreateViewPayloadPB.create() ..parentViewId = workspaceId ..name = name - ..layout = ViewLayoutPB.Document; + // only allow document layout for the top-level views + ..layout = ViewLayoutPB.Document + ..section = viewSection; if (desc != null) { payload.desc = desc; @@ -37,8 +38,8 @@ class WorkspaceService { return FolderEventReadCurrentWorkspace().send(); } - Future, FlowyError>> getViews() { - final payload = WorkspaceIdPB.create()..value = workspaceId; + Future, FlowyError>> getPublicViews() { + final payload = GetWorkspaceViewPB.create()..value = workspaceId; return FolderEventReadWorkspaceViews(payload).send().then((result) { return result.fold( (views) => FlowyResult.success(views.items), @@ -47,13 +48,23 @@ class WorkspaceService { }); } - Future> moveApp({ - required String appId, + Future, FlowyError>> getPrivateViews() { + final payload = GetWorkspaceViewPB.create()..value = workspaceId; + return FolderEventReadPrivateViews(payload).send().then((result) { + return result.fold( + (views) => FlowyResult.success(views.items), + (error) => FlowyResult.failure(error), + ); + }); + } + + Future> moveView({ + required String viewId, required int fromIndex, required int toIndex, }) { final payload = MoveViewPayloadPB.create() - ..viewId = appId + ..viewId = viewId ..from = fromIndex ..to = toIndex; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart similarity index 100% rename from frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart rename to frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart new file mode 100644 index 0000000000..00f88e153d --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart @@ -0,0 +1,63 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; + +class FolderHeader extends StatefulWidget { + const FolderHeader({ + super.key, + required this.title, + required this.expandButtonTooltip, + required this.addButtonTooltip, + required this.onPressed, + required this.onAdded, + }); + + final String title; + final String expandButtonTooltip; + final String addButtonTooltip; + final VoidCallback onPressed; + final VoidCallback onAdded; + + @override + State createState() => _FolderHeaderState(); +} + +class _FolderHeaderState extends State { + bool onHover = false; + + @override + Widget build(BuildContext context) { + const iconSize = 26.0; + const textPadding = 4.0; + return MouseRegion( + onEnter: (event) => setState(() => onHover = true), + onExit: (event) => setState(() => onHover = false), + child: Row( + children: [ + FlowyTextButton( + widget.title, + tooltip: widget.expandButtonTooltip, + constraints: const BoxConstraints( + minHeight: iconSize + textPadding * 2, + ), + padding: const EdgeInsets.all(textPadding), + fillColor: Colors.transparent, + onPressed: widget.onPressed, + ), + if (onHover) ...[ + const Spacer(), + FlowyIconButton( + tooltipText: widget.addButtonTooltip, + hoverColor: Theme.of(context).colorScheme.secondaryContainer, + iconPadding: const EdgeInsets.all(2), + height: iconSize, + width: iconSize, + icon: const FlowySvg(FlowySvgs.add_s), + onPressed: widget.onAdded, + ), + ], + ], + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart new file mode 100644 index 0000000000..328b4a831f --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart @@ -0,0 +1,124 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_folder_header.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SectionFolder extends StatelessWidget { + const SectionFolder({ + super.key, + required this.title, + required this.categoryType, + required this.views, + this.isHoverEnabled = true, + required this.expandButtonTooltip, + required this.addButtonTooltip, + }); + + final String title; + final FolderCategoryType categoryType; + final List views; + final bool isHoverEnabled; + final String expandButtonTooltip; + final String addButtonTooltip; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => FolderBloc(type: categoryType) + ..add( + const FolderEvent.initial(), + ), + child: BlocBuilder( + builder: (context, state) { + return Column( + children: [ + FolderHeader( + title: title, + expandButtonTooltip: expandButtonTooltip, + addButtonTooltip: addButtonTooltip, + onPressed: () => context + .read() + .add(const FolderEvent.expandOrUnExpand()), + onAdded: () { + createViewAndShowRenameDialogIfNeeded( + context, + LocaleKeys.newPageText.tr(), + (viewName, _) { + if (viewName.isNotEmpty) { + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + index: 0, + viewSection: categoryType.toViewSectionPB, + ), + ); + + context.read().add( + const FolderEvent.expandOrUnExpand( + isExpanded: true, + ), + ); + } + }, + ); + }, + ), + if (state.isExpanded) + ...views.map( + (view) => ViewItem( + key: ValueKey( + '${categoryType.name} ${view.id}', + ), + categoryType: categoryType, + isFirstChild: view.id == views.first.id, + view: view, + level: 0, + leftPadding: 16, + isFeedback: false, + onSelected: (view) { + if (HardwareKeyboard.instance.isControlPressed) { + context.read().openTab(view); + } + + context.read().openPlugin(view); + }, + onTertiarySelected: (view) => + context.read().openTab(view), + isHoverEnabled: isHoverEnabled, + ), + ), + if (views.isEmpty) + ViewItem( + categoryType: categoryType, + view: ViewPB( + parentViewId: context + .read() + .state + .currentWorkspace + ?.workspaceId ?? + '', + ), + level: 0, + leftPadding: 16, + isFeedback: false, + onSelected: (_) {}, + onTertiarySelected: (_) {}, + isHoverEnabled: isHoverEnabled, + isPlaceholder: true, + ), + ], + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart deleted file mode 100644 index ec86203599..0000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart +++ /dev/null @@ -1,146 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; -import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; -import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -class PersonalFolder extends StatelessWidget { - const PersonalFolder({ - super.key, - required this.views, - this.isHoverEnabled = true, - }); - - final List views; - final bool isHoverEnabled; - - @override - Widget build(BuildContext context) { - return BlocProvider( - create: (context) => FolderBloc(type: FolderCategoryType.personal) - ..add( - const FolderEvent.initial(), - ), - child: BlocBuilder( - builder: (context, state) { - return Column( - children: [ - PersonalFolderHeader( - onPressed: () => context - .read() - .add(const FolderEvent.expandOrUnExpand()), - onAdded: () => context - .read() - .add(const FolderEvent.expandOrUnExpand(isExpanded: true)), - ), - if (state.isExpanded) - ...views.map( - (view) => ViewItem( - key: ValueKey( - '${FolderCategoryType.personal.name} ${view.id}', - ), - categoryType: FolderCategoryType.personal, - isFirstChild: view.id == views.first.id, - view: view, - level: 0, - leftPadding: 16, - isFeedback: false, - onSelected: (view) { - if (HardwareKeyboard.instance.isControlPressed) { - context.read().openTab(view); - } - - context.read().openPlugin(view); - }, - onTertiarySelected: (view) => - context.read().openTab(view), - isHoverEnabled: isHoverEnabled, - ), - ), - ], - ); - }, - ), - ); - } -} - -class PersonalFolderHeader extends StatefulWidget { - const PersonalFolderHeader({ - super.key, - required this.onPressed, - required this.onAdded, - }); - - final VoidCallback onPressed; - final VoidCallback onAdded; - - @override - State createState() => _PersonalFolderHeaderState(); -} - -class _PersonalFolderHeaderState extends State { - bool onHover = false; - - @override - Widget build(BuildContext context) { - const iconSize = 26.0; - const textPadding = 4.0; - return MouseRegion( - onEnter: (event) => setState(() => onHover = true), - onExit: (event) => setState(() => onHover = false), - child: Row( - children: [ - FlowyTextButton( - LocaleKeys.sideBar_personal.tr(), - tooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), - constraints: const BoxConstraints( - minHeight: iconSize + textPadding * 2, - ), - padding: const EdgeInsets.all(textPadding), - fillColor: Colors.transparent, - onPressed: widget.onPressed, - ), - if (onHover) ...[ - const Spacer(), - FlowyIconButton( - tooltipText: LocaleKeys.sideBar_addAPage.tr(), - hoverColor: Theme.of(context).colorScheme.secondaryContainer, - iconPadding: const EdgeInsets.all(2), - height: iconSize, - width: iconSize, - icon: const FlowySvg(FlowySvgs.add_s), - onPressed: () { - createViewAndShowRenameDialogIfNeeded( - context, - LocaleKeys.newPageText.tr(), - (viewName, _) { - if (viewName.isNotEmpty) { - context.read().add( - SidebarRootViewsEvent.createRootView( - viewName, - index: 0, - ), - ); - - widget.onAdded(); - } - }, - ); - }, - ), - ], - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart index e02a5b0c74..4bb0600462 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar.dart @@ -1,9 +1,10 @@ import 'dart:async'; -import 'package:appflowy/shared/feature_flags.dart'; +import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/favorite/prelude.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/notifications/notification_action.dart'; import 'package:appflowy/workspace/application/notifications/notification_action_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; @@ -15,7 +16,6 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_top_me import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_trash.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; @@ -31,7 +31,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// - settings /// - scrollable document list /// - trash -class HomeSideBar extends StatefulWidget { +class HomeSideBar extends StatelessWidget { const HomeSideBar({ super.key, required this.userProfile, @@ -42,72 +42,56 @@ class HomeSideBar extends StatefulWidget { final WorkspaceSettingPB workspaceSetting; - @override - State createState() => _HomeSideBarState(); -} - -class _HomeSideBarState extends State { - final _scrollController = ScrollController(); - Timer? _srollDebounce; - bool isScrolling = false; - - @override - void initState() { - super.initState(); - _scrollController.addListener(_onScrollChanged); - } - - void _onScrollChanged() { - setState(() => isScrolling = true); - - _srollDebounce?.cancel(); - _srollDebounce = - Timer(const Duration(milliseconds: 300), _setScrollStopped); - } - - void _setScrollStopped() { - if (mounted) { - setState(() => isScrolling = false); - } - } - - @override - void dispose() { - _srollDebounce?.cancel(); - _scrollController.removeListener(_onScrollChanged); - _scrollController.dispose(); - super.dispose(); - } - @override Widget build(BuildContext context) { + // Workspace Bloc: control the current workspace + // | + // +-- Workspace Menu + // | | + // | +-- Workspace List: control to switch workspace + // | | + // | +-- Workspace Settings + // | | + // | +-- Notification Center + // | + // +-- Favorite Section + // | + // +-- Public Or Private Section: control the sections of the workspace + // | + // +-- Trash Section return BlocProvider( - create: (_) => UserWorkspaceBloc(userProfile: widget.userProfile) - ..add(const UserWorkspaceEvent.fetchWorkspaces()), + create: (_) => UserWorkspaceBloc(userProfile: userProfile) + ..add( + const UserWorkspaceEvent.initial(), + ), child: BlocBuilder( + // Rebuild the whole sidebar when the current workspace changes buildWhen: (previous, current) => previous.currentWorkspace?.workspaceId != current.currentWorkspace?.workspaceId, builder: (context, state) { + if (state.currentWorkspace == null) { + return const SizedBox.shrink(); + } return MultiBlocProvider( providers: [ BlocProvider( create: (_) => getIt(), ), BlocProvider( - create: (_) => SidebarRootViewsBloc() + create: (_) => SidebarSectionsBloc() ..add( - SidebarRootViewsEvent.initial( - widget.userProfile, + SidebarSectionsEvent.initial( + userProfile, state.currentWorkspace?.workspaceId ?? - widget.workspaceSetting.workspaceId, + workspaceSetting.workspaceId, ), ), ), ], child: MultiBlocListener( listeners: [ - BlocListener( + BlocListener( listenWhen: (p, c) => p.lastCreatedRootView?.id != c.lastCreatedRootView?.id, listener: (context, state) => context.read().add( @@ -122,28 +106,25 @@ class _HomeSideBarState extends State { ), BlocListener( listener: (context, state) { - context.read().add( - SidebarRootViewsEvent.reset( - widget.userProfile, - state.currentWorkspace?.workspaceId ?? - widget.workspaceSetting.workspaceId, + context.read().add( + TabsEvent.openPlugin( + plugin: makePlugin(pluginType: PluginType.blank), ), ); + context.read().add( + SidebarSectionsEvent.initial( + userProfile, + state.currentWorkspace?.workspaceId ?? + workspaceSetting.workspaceId, + ), + ); + context.read().add( + const FavoriteEvent.fetchFavorites(), + ); }, ), ], - child: Builder( - builder: (context) { - final menuState = context.watch().state; - final favoriteState = context.watch().state; - - return _buildSidebar( - context, - menuState.views, - favoriteState.views, - ); - }, - ), + child: _Sidebar(userProfile: userProfile), ), ); }, @@ -151,71 +132,6 @@ class _HomeSideBarState extends State { ); } - Widget _buildSidebar( - BuildContext context, - List views, - List favoriteViews, - ) { - const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12); - return DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceVariant, - border: Border( - right: BorderSide(color: Theme.of(context).dividerColor), - ), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - // top menu - const Padding( - padding: menuHorizontalInset, - child: SidebarTopMenu(), - ), - // user or workspace, setting - Padding( - padding: menuHorizontalInset, - child: FeatureFlag.collaborativeWorkspace.isOn - ? SidebarWorkspace( - userProfile: widget.userProfile, - views: views, - ) - : SidebarUser( - userProfile: widget.userProfile, - views: views, - ), - ), - - const VSpace(20), - // scrollable document list - Expanded( - child: Padding( - padding: menuHorizontalInset, - child: SingleChildScrollView( - controller: _scrollController, - physics: const ClampingScrollPhysics(), - child: SidebarFolder( - views: views, - favoriteViews: favoriteViews, - isHoverEnabled: !isScrolling, - ), - ), - ), - ), - const VSpace(10), - // trash - const Padding( - padding: menuHorizontalInset, - child: SidebarTrashButton(), - ), - const VSpace(10), - // new page button - const SidebarNewPageButton(), - ], - ), - ); - } - void _onNotificationAction( BuildContext context, NotificationActionState state, @@ -224,9 +140,10 @@ class _HomeSideBarState extends State { if (action != null) { if (action.type == ActionType.openView) { final view = context - .read() + .read() .state - .views + .section + .publicViews .findView(action.objectId); if (view != null) { @@ -250,3 +167,114 @@ class _HomeSideBarState extends State { } } } + +class _Sidebar extends StatefulWidget { + const _Sidebar({ + required this.userProfile, + }); + + final UserProfilePB userProfile; + + @override + State<_Sidebar> createState() => _SidebarState(); +} + +class _SidebarState extends State<_Sidebar> { + final _scrollController = ScrollController(); + Timer? _scrollDebounce; + bool isScrolling = false; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScrollChanged); + } + + @override + void dispose() { + _scrollDebounce?.cancel(); + _scrollController.removeListener(_onScrollChanged); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + const menuHorizontalInset = EdgeInsets.symmetric(horizontal: 12); + return DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceVariant, + border: Border( + right: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // top menu + const Padding( + padding: menuHorizontalInset, + child: SidebarTopMenu(), + ), + // user or workspace, setting + Padding( + padding: menuHorizontalInset, + child: + // if the workspaces are empty, show the user profile instead + context.read().state.isCollabWorkspaceOn && + context + .read() + .state + .workspaces + .isNotEmpty + ? SidebarWorkspace( + userProfile: widget.userProfile, + ) + : SidebarUser( + userProfile: widget.userProfile, + ), + ), + + const VSpace(20), + // scrollable document list + Expanded( + child: Padding( + padding: menuHorizontalInset, + child: SingleChildScrollView( + controller: _scrollController, + physics: const ClampingScrollPhysics(), + child: SidebarFolder( + userProfile: widget.userProfile, + isHoverEnabled: !isScrolling, + ), + ), + ), + ), + const VSpace(10), + // trash + const Padding( + padding: menuHorizontalInset, + child: SidebarTrashButton(), + ), + const VSpace(10), + // new page button + const SidebarNewPageButton(), + ], + ), + ); + } + + void _onScrollChanged() { + setState(() => isScrolling = true); + + _scrollDebounce?.cancel(); + _scrollDebounce = + Timer(const Duration(milliseconds: 300), _setScrollStopped); + } + + void _setScrollStopped() { + if (mounted) { + setState(() => isScrolling = false); + } + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart index 397a3e3d90..e9cd8059ab 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_folder.dart @@ -1,50 +1,123 @@ -import 'package:flutter/material.dart'; - +import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart'; -import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_section_folder.dart'; +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarFolder extends StatelessWidget { const SidebarFolder({ super.key, - required this.views, - required this.favoriteViews, this.isHoverEnabled = true, + required this.userProfile, }); - final List views; - final List favoriteViews; final bool isHoverEnabled; + final UserProfilePB userProfile; @override Widget build(BuildContext context) { - // check if there is any duplicate views - final views = this.views.toSet().toList(); - final favoriteViews = this.favoriteViews.toSet().toList(); - assert(views.length == this.views.length); - assert(favoriteViews.length == favoriteViews.length); - return ValueListenableBuilder( valueListenable: getIt().notifier, builder: (context, value, child) { return Column( children: [ // favorite - if (favoriteViews.isNotEmpty) ...[ - FavoriteFolder( - // remove the duplicate views - views: favoriteViews, - ), - const VSpace(10), - ], - // personal - PersonalFolder(views: views, isHoverEnabled: isHoverEnabled), + BlocBuilder( + builder: (context, state) { + if (state.views.isEmpty) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 10), + child: FavoriteFolder( + // remove the duplicate views + views: state.views, + ), + ); + }, + ), + // public or private + BlocBuilder( + builder: (context, state) { + // only show public and private section if the workspace is collaborative and not local + final isCollaborativeWorkspace = + context.read().state.isCollabWorkspaceOn; + + return Column( + children: + // only show public and private section if the workspace is collaborative + isCollaborativeWorkspace + ? [ + // public + const VSpace(10), + PublicSectionFolder( + views: state.section.publicViews, + ), + + // private + const VSpace(10), + PrivateSectionFolder( + views: state.section.privateViews, + ), + ] + : [ + // personal + const VSpace(10), + PersonalSectionFolder( + views: state.section.publicViews, + ), + ], + ); + }, + ), ], ); }, ); } } + +class PrivateSectionFolder extends SectionFolder { + PrivateSectionFolder({ + super.key, + required super.views, + }) : super( + title: LocaleKeys.sideBar_private.tr(), + categoryType: FolderCategoryType.private, + expandButtonTooltip: LocaleKeys.sideBar_clickToHidePrivate.tr(), + addButtonTooltip: LocaleKeys.sideBar_addAPageToPrivate.tr(), + ); +} + +class PublicSectionFolder extends SectionFolder { + PublicSectionFolder({ + super.key, + required super.views, + }) : super( + title: LocaleKeys.sideBar_public.tr(), + categoryType: FolderCategoryType.public, + expandButtonTooltip: LocaleKeys.sideBar_clickToHidePublic.tr(), + addButtonTooltip: LocaleKeys.sideBar_addAPageToPublic.tr(), + ); +} + +class PersonalSectionFolder extends SectionFolder { + PersonalSectionFolder({ + super.key, + required super.views, + }) : super( + title: LocaleKeys.sideBar_personal.tr(), + categoryType: FolderCategoryType.public, + expandButtonTooltip: LocaleKeys.sideBar_clickToHidePersonal.tr(), + addButtonTooltip: LocaleKeys.sideBar_addAPage.tr(), + ); +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart index d5cd8a65ae..eac80118b4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_new_page_button.dart @@ -1,7 +1,9 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/rename_view_dialog.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/extension.dart'; @@ -25,9 +27,17 @@ class SidebarNewPageButton extends StatelessWidget { LocaleKeys.newPageText.tr(), (viewName, _) { if (viewName.isNotEmpty) { - context - .read() - .add(SidebarRootViewsEvent.createRootView(viewName)); + // if the workspace is collaborative, create the view in the private section by default. + final section = + context.read().state.isCollabWorkspaceOn + ? ViewSectionPB.Private + : ViewSectionPB.Public; + context.read().add( + SidebarSectionsEvent.createRootViewInSection( + name: viewName, + viewSection: section, + ), + ); } }, ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart index 71c04cf048..e4d5f2fa3e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_top_menu.dart @@ -4,7 +4,7 @@ import 'package:appflowy/core/frameless_window.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/home/home_sizes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/icon_button.dart'; @@ -24,7 +24,7 @@ class SidebarTopMenu extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { return SizedBox( height: HomeSizes.topBarHeight, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart index 473ca6f1d3..288bd76a74 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_user.dart @@ -3,7 +3,6 @@ import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_setting.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart' show UserProfilePB; import 'package:easy_localization/easy_localization.dart'; @@ -17,11 +16,9 @@ class SidebarUser extends StatelessWidget { const SidebarUser({ super.key, required this.userProfile, - required this.views, }); final UserProfilePB userProfile; - final List views; @override Widget build(BuildContext context) { @@ -37,13 +34,13 @@ class SidebarUser extends StatelessWidget { iconUrl: state.userProfile.iconUrl, name: state.userProfile.name, ), - const HSpace(4), + const HSpace(8), Expanded( child: _buildUserName(context, state), ), UserSettingButton(userProfile: state.userProfile), const HSpace(4), - NotificationButton(views: views), + const NotificationButton(), ], ), ), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart index 1ac1afbabd..a20818b105 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart @@ -6,25 +6,23 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sid import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart'; -import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class SidebarWorkspace extends StatelessWidget { const SidebarWorkspace({ super.key, required this.userProfile, - required this.views, }); final UserProfilePB userProfile; - final List views; @override Widget build(BuildContext context) { @@ -32,21 +30,20 @@ class SidebarWorkspace extends StatelessWidget { listener: _showResultDialog, builder: (context, state) { final currentWorkspace = state.currentWorkspace; - // todo: show something if there is no workspace if (currentWorkspace == null) { return const SizedBox.shrink(); } return Row( children: [ Expanded( - child: _WorkspaceWrapper( + child: SidebarSwitchWorkspaceButton( userProfile: userProfile, currentWorkspace: currentWorkspace, ), ), UserSettingButton(userProfile: userProfile), const HSpace(4), - NotificationButton(views: views), + const NotificationButton(), ], ); }, @@ -54,60 +51,82 @@ class SidebarWorkspace extends StatelessWidget { } void _showResultDialog(BuildContext context, UserWorkspaceState state) { - var result = state.createWorkspaceResult; - - if (result != null) { - final message = result.fold( - (s) => LocaleKeys.workspace_createSuccess.tr(), - (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}', - ); - return showSnackBarMessage(context, message); - } - - result = state.deleteWorkspaceResult; - if (result != null) { - final message = result.fold( - (s) => LocaleKeys.workspace_deleteSuccess.tr(), - (e) => '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}', - ); - showSnackBarMessage(context, message); + final actionResult = state.actionResult; + if (actionResult == null) { return; } - result = state.openWorkspaceResult; - if (result != null) { - final message = result.fold( - (s) => LocaleKeys.workspace_openSuccess.tr(), - (e) => '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}', + final actionType = actionResult.actionType; + final result = actionResult.result; + + result.onFailure((f) { + Log.error( + '[Workspace] Failed to perform ${actionType.toString()} action: $f', + ); + }); + + // show a confirmation dialog if the action is create and the result is LimitExceeded failure + if (actionType == UserWorkspaceActionType.create && + result.isFailure && + result.getFailure().code == ErrorCode.WorkspaceLimitExceeded) { + showDialog( + context: context, + builder: (context) => NavigatorOkCancelDialog( + message: LocaleKeys.workspace_createLimitExceeded.tr(), + ), ); - showSnackBarMessage(context, message); return; } - result = state.updateWorkspaceIconResult; - if (result != null) { - final message = result.fold( - (s) => LocaleKeys.workspace_updateIconSuccess.tr(), - (e) => '${LocaleKeys.workspace_updateIconFailed.tr()}: ${e.msg}', - ); - showSnackBarMessage(context, message); - return; + final String? message; + switch (actionType) { + case UserWorkspaceActionType.create: + message = result.fold( + (s) => LocaleKeys.workspace_createSuccess.tr(), + (e) => '${LocaleKeys.workspace_createFailed.tr()}: ${e.msg}', + ); + break; + case UserWorkspaceActionType.delete: + message = result.fold( + (s) => LocaleKeys.workspace_deleteSuccess.tr(), + (e) => '${LocaleKeys.workspace_deleteFailed.tr()}: ${e.msg}', + ); + break; + case UserWorkspaceActionType.open: + message = result.fold( + (s) => LocaleKeys.workspace_openSuccess.tr(), + (e) => '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}', + ); + break; + case UserWorkspaceActionType.updateIcon: + message = result.fold( + (s) => LocaleKeys.workspace_updateIconSuccess.tr(), + (e) => '${LocaleKeys.workspace_updateIconFailed.tr()}: ${e.msg}', + ); + break; + case UserWorkspaceActionType.rename: + message = result.fold( + (s) => LocaleKeys.workspace_renameSuccess.tr(), + (e) => '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}', + ); + break; + case UserWorkspaceActionType.none: + case UserWorkspaceActionType.fetchWorkspaces: + case UserWorkspaceActionType.leave: + message = null; + break; } - result = state.renameWorkspaceResult; - if (result != null) { - final message = result.fold( - (s) => LocaleKeys.workspace_renameSuccess.tr(), - (e) => '${LocaleKeys.workspace_renameFailed.tr()}: ${e.msg}', - ); + if (message != null) { + Log.info('[Workspace] $message'); showSnackBarMessage(context, message); - return; } } } -class _WorkspaceWrapper extends StatefulWidget { - const _WorkspaceWrapper({ +class SidebarSwitchWorkspaceButton extends StatefulWidget { + const SidebarSwitchWorkspaceButton({ + super.key, required this.userProfile, required this.currentWorkspace, }); @@ -116,39 +135,12 @@ class _WorkspaceWrapper extends StatefulWidget { final UserProfilePB userProfile; @override - State<_WorkspaceWrapper> createState() => _WorkspaceWrapperState(); + State createState() => + _SidebarSwitchWorkspaceButtonState(); } -class _WorkspaceWrapperState extends State<_WorkspaceWrapper> { - @override - Widget build(BuildContext context) { - if (PlatformExtension.isDesktopOrWeb) { - return _DesktopWorkspaceWrapper( - userProfile: widget.userProfile, - currentWorkspace: widget.currentWorkspace, - ); - } else { - // TODO(Lucas) mobile workspace menu - return const Placeholder(); - } - } -} - -class _DesktopWorkspaceWrapper extends StatefulWidget { - const _DesktopWorkspaceWrapper({ - required this.userProfile, - required this.currentWorkspace, - }); - - final UserWorkspacePB currentWorkspace; - final UserProfilePB userProfile; - - @override - State<_DesktopWorkspaceWrapper> createState() => - _DesktopWorkspaceWrapperState(); -} - -class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { +class _SidebarSwitchWorkspaceButtonState + extends State { final controller = PopoverController(); @override @@ -157,6 +149,11 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { direction: PopoverDirection.bottomWithCenterAligned, offset: const Offset(0, 10), constraints: const BoxConstraints(maxWidth: 260, maxHeight: 600), + onOpen: () { + context.read().add( + const UserWorkspaceEvent.fetchWorkspaces(), + ); + }, popupBuilder: (_) { return BlocProvider.value( value: context.read(), @@ -164,7 +161,7 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { builder: (context, state) { final currentWorkspace = state.currentWorkspace; final workspaces = state.workspaces; - if (currentWorkspace == null || workspaces.isEmpty) { + if (currentWorkspace == null) { return const SizedBox.shrink(); } return WorkspacesMenu( @@ -182,12 +179,16 @@ class _DesktopWorkspaceWrapperState extends State<_DesktopWorkspaceWrapper> { margin: const EdgeInsets.symmetric(vertical: 8), text: Row( children: [ - const HSpace(4.0), - SizedBox( - width: 24.0, - child: WorkspaceIcon(workspace: widget.currentWorkspace), + const HSpace(2.0), + SizedBox.square( + dimension: 30.0, + child: WorkspaceIcon( + workspace: widget.currentWorkspace, + iconSize: 20, + enableEdit: false, + ), ), - const HSpace(8), + const HSpace(6), Expanded( child: FlowyText.medium( widget.currentWorkspace.name, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart index 7fa07bcfe5..13020a3034 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart @@ -1,6 +1,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; @@ -13,6 +15,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; enum WorkspaceMoreAction { rename, delete, + leave, } class WorkspaceMoreActionList extends StatelessWidget { @@ -25,9 +28,20 @@ class WorkspaceMoreActionList extends StatelessWidget { @override Widget build(BuildContext context) { + final myRole = context.read().state.myRole; + final actions = []; + if (myRole.isOwner) { + actions.add(WorkspaceMoreAction.rename); + actions.add(WorkspaceMoreAction.delete); + } else if (myRole.canLeave) { + actions.add(WorkspaceMoreAction.leave); + } + if (actions.isEmpty) { + return const SizedBox.shrink(); + } return PopoverActionList<_WorkspaceMoreActionWrapper>( direction: PopoverDirection.bottomWithCenterAligned, - actions: WorkspaceMoreAction.values + actions: actions .map((e) => _WorkspaceMoreActionWrapper(e, workspace)) .toList(), buildChild: (controller) { @@ -92,6 +106,20 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { ); }, ).show(context); + case WorkspaceMoreAction.leave: + await showDialog( + context: context, + builder: (_) => NavigatorOkCancelDialog( + title: LocaleKeys.workspace_leaveCurrentWorkspace.tr(), + message: LocaleKeys.workspace_leaveCurrentWorkspacePrompt.tr(), + onOkPressed: () { + workspaceBloc.add( + UserWorkspaceEvent.leaveWorkspace(workspace.workspaceId), + ); + }, + okTitle: LocaleKeys.button_yes.tr(), + ), + ); } }, ); @@ -103,6 +131,8 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell { return LocaleKeys.button_delete.tr(); case WorkspaceMoreAction.rename: return LocaleKeys.button_rename.tr(); + case WorkspaceMoreAction.leave: + return LocaleKeys.workspace_leaveCurrentWorkspace.tr(); } } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart index 93d60414bd..ffc5083db8 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:appflowy/plugins/base/icon/icon_picker.dart'; import 'package:appflowy/util/color_generator/color_generator.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; @@ -7,48 +9,78 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class WorkspaceIcon extends StatelessWidget { +class WorkspaceIcon extends StatefulWidget { const WorkspaceIcon({ super.key, + required this.enableEdit, + required this.iconSize, required this.workspace, }); final UserWorkspacePB workspace; + final double iconSize; + final bool enableEdit; + + @override + State createState() => _WorkspaceIconState(); +} + +class _WorkspaceIconState extends State { + final controller = PopoverController(); @override Widget build(BuildContext context) { - return AppFlowyPopover( - offset: const Offset(0, 8), - direction: PopoverDirection.bottomWithLeftAligned, - constraints: BoxConstraints.loose(const Size(360, 380)), - clickHandler: PopoverClickHandler.gestureDetector, - popupBuilder: (BuildContext popoverContext) { - return FlowyIconPicker( - onSelected: (result) { - context.read().add( - UserWorkspaceEvent.updateWorkspaceIcon( - workspace.workspaceId, - result.emoji, - ), - ); - }, - ); - }, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - color: ColorGenerator.generateColorFromString(workspace.name), - borderRadius: BorderRadius.circular(4), - ), - child: FlowyText( - workspace.name.isEmpty ? '' : workspace.name.substring(0, 1), - fontSize: 16, - color: Colors.black, - ), + Widget child = widget.workspace.icon.isNotEmpty + ? Container( + width: widget.iconSize, + alignment: Alignment.center, + child: FlowyText( + widget.workspace.icon, + fontSize: widget.iconSize, + ), + ) + : Container( + alignment: Alignment.center, + width: widget.iconSize, + height: max(widget.iconSize, 26), + decoration: BoxDecoration( + color: ColorGenerator(widget.workspace.name).toColor(), + borderRadius: BorderRadius.circular(4), + ), + child: FlowyText( + widget.workspace.name.isEmpty + ? '' + : widget.workspace.name.substring(0, 1), + fontSize: 16, + color: Colors.black, + ), + ); + if (widget.enableEdit) { + child = AppFlowyPopover( + offset: const Offset(0, 8), + controller: controller, + direction: PopoverDirection.bottomWithLeftAligned, + constraints: BoxConstraints.loose(const Size(360, 380)), + clickHandler: PopoverClickHandler.gestureDetector, + popupBuilder: (BuildContext popoverContext) { + return FlowyIconPicker( + onSelected: (result) { + context.read().add( + UserWorkspaceEvent.updateWorkspaceIcon( + widget.workspace.workspaceId, + result.emoji, + ), + ); + controller.close(); + }, + ); + }, + child: MouseRegion( + cursor: SystemMouseCursors.click, + child: child, ), - ), - ); + ); + } + return child; } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart index b2ab95638e..6f94c31a95 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart @@ -1,6 +1,5 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart'; @@ -62,6 +61,7 @@ class WorkspacesMenu extends StatelessWidget { ), for (final workspace in workspaces) ...[ WorkspaceMenuItem( + key: ValueKey(workspace.workspaceId), workspace: workspace, userProfile: userProfile, isSelected: workspace.workspaceId == currentWorkspace.workspaceId, @@ -89,7 +89,7 @@ class WorkspacesMenu extends StatelessWidget { final workspaceBloc = context.read(); await CreateWorkspaceDialog( onConfirm: (name) { - workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name, '')); + workspaceBloc.add(UserWorkspaceEvent.createWorkspace(name)); }, ).show(context); } @@ -120,57 +120,72 @@ class WorkspaceMenuItem extends StatelessWidget { // settings right icon inside the flowy button will // cause the popover dismiss intermediately when click the right icon. // so using the stack to put the right icon on the flowy button. - return Stack( - alignment: Alignment.center, - children: [ - FlowyButton( - onTap: () { - if (!isSelected) { - context.read().add( - UserWorkspaceEvent.openWorkspace( - workspace.workspaceId, - ), - ); - } - }, - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), - iconPadding: 10.0, - leftIconSize: const Size.square(32), - leftIcon: const SizedBox.square( - dimension: 32, - ), - rightIcon: const HSpace(42.0), - text: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.medium( - workspace.name, - fontSize: 14.0, - overflow: TextOverflow.ellipsis, - ), - if (members.length > 1) + return SizedBox( + height: 52, + child: Stack( + alignment: Alignment.center, + children: [ + FlowyButton( + onTap: () { + if (!isSelected) { + context.read().add( + UserWorkspaceEvent.openWorkspace( + workspace.workspaceId, + ), + ); + PopoverContainer.of(context).closeAll(); + } + }, + margin: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + iconPadding: 10.0, + leftIconSize: const Size.square(32), + leftIcon: const SizedBox.square( + dimension: 32, + ), + rightIcon: const HSpace(42.0), + text: Column( + crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisAlignment: MainAxisAlignment.center, + children: [ + FlowyText.medium( + workspace.name, + fontSize: 14.0, + overflow: TextOverflow.ellipsis, + ), FlowyText( - '${members.length} ${LocaleKeys.settings_appearance_members_members.tr()}', + state.isLoading + ? '' + : LocaleKeys + .settings_appearance_members_membersCount + .plural( + members.length, + ), fontSize: 10.0, color: Theme.of(context).hintColor, ), - ], - ), - ), - Positioned( - left: 12, - child: SizedBox.square( - dimension: 32, - child: WorkspaceIcon( - workspace: workspace, + ], ), ), - ), - Positioned( - right: 12.0, - child: Align(child: _buildRightIcon(context)), - ), - ], + Positioned( + left: 8, + child: SizedBox.square( + dimension: 32, + child: WorkspaceIcon( + workspace: workspace, + iconSize: 26, + enableEdit: true, + ), + ), + ), + Positioned( + right: 12.0, + child: Align( + child: _buildRightIcon(context), + ), + ), + ], + ), ); }, ), @@ -180,8 +195,7 @@ class WorkspaceMenuItem extends StatelessWidget { Widget _buildRightIcon(BuildContext context) { // only the owner can update or delete workspace. // only show the more action button when the workspace is selected. - if (!isSelected || - !context.read().state.myRole.isOwner) { + if (!isSelected || context.read().state.isLoading) { return const SizedBox.shrink(); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart index 2fcf4ce098..658d60bfe7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/draggable_view_item.dart @@ -1,3 +1,4 @@ +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/draggable_item/draggable_item.dart'; @@ -25,6 +26,7 @@ class DraggableViewItem extends StatefulWidget { this.topHighlightColor, this.bottomHighlightColor, this.onDragging, + this.onMove, }); final Widget child; @@ -35,6 +37,7 @@ class DraggableViewItem extends StatefulWidget { final Color? topHighlightColor; final Color? bottomHighlightColor; final void Function(bool isDragging)? onDragging; + final void Function(ViewPB from, ViewPB to)? onMove; @override State createState() => _DraggableViewItemState(); @@ -188,6 +191,14 @@ class _DraggableViewItemState extends State { return; } + if (widget.onMove != null) { + widget.onMove?.call(from, to); + return; + } + + final fromSection = getViewSection(from); + final toSection = getViewSection(to); + switch (position) { case DraggableHoverPosition.top: context.read().add( @@ -195,6 +206,8 @@ class _DraggableViewItemState extends State { from, to.parentViewId, null, + fromSection, + toSection, ), ); break; @@ -204,6 +217,8 @@ class _DraggableViewItemState extends State { from, to.parentViewId, to.id, + fromSection, + toSection, ), ); break; @@ -213,6 +228,8 @@ class _DraggableViewItemState extends State { from, to.id, to.childViews.lastOrNull?.id, + fromSection, + toSection, ), ); break; @@ -251,6 +268,10 @@ class _DraggableViewItemState extends State { return true; } + + ViewSectionPB? getViewSection(ViewPB view) { + return context.read().getViewSection(view); + } } extension on ViewPB { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart index 2cdc373181..41a8cacd10 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/menu/view/view_item.dart @@ -43,6 +43,7 @@ class ViewItem extends StatelessWidget { required this.isFeedback, this.height = 28.0, this.isHoverEnabled = true, + this.isPlaceholder = false, }); final ViewPB view; @@ -78,6 +79,10 @@ class ViewItem extends StatelessWidget { final bool isHoverEnabled; + // all the view movement depends on the [ViewItem] widget, so we have to add a + // placeholder widget to receive the drop event when moving view across sections. + final bool isPlaceholder; + @override Widget build(BuildContext context) { return BlocProvider( @@ -105,6 +110,7 @@ class ViewItem extends StatelessWidget { isFeedback: isFeedback, height: height, isHoverEnabled: isHoverEnabled, + isPlaceholder: isPlaceholder, ); }, ), @@ -132,6 +138,7 @@ class InnerViewItem extends StatelessWidget { required this.isFeedback, required this.height, this.isHoverEnabled = true, + this.isPlaceholder = false, }); final ViewPB view; @@ -154,6 +161,7 @@ class InnerViewItem extends StatelessWidget { final double height; final bool isHoverEnabled; + final bool isPlaceholder; @override Widget build(BuildContext context) { @@ -170,6 +178,7 @@ class InnerViewItem extends StatelessWidget { leftPadding: leftPadding, isFeedback: isFeedback, height: height, + isPlaceholder: isPlaceholder, ); // if the view is expanded and has child views, render its child views @@ -188,6 +197,7 @@ class InnerViewItem extends StatelessWidget { isDraggable: isDraggable, leftPadding: leftPadding, isFeedback: isFeedback, + isPlaceholder: isPlaceholder, ); }).toList(); @@ -222,14 +232,17 @@ class InnerViewItem extends StatelessWidget { } // wrap the child with DraggableItem if isDraggable is true - if (isDraggable && !isReferencedDatabaseView(view, parentView)) { + if ((isDraggable || isPlaceholder) && + !isReferencedDatabaseView(view, parentView)) { child = DraggableViewItem( isFirstChild: isFirstChild, view: view, - child: child, onDragging: (isDragging) { _isDragging = isDragging; }, + onMove: isPlaceholder + ? (from, to) => _moveViewCrossSection(context, from, to) + : null, feedback: (context) { return ViewItem( view: view, @@ -243,6 +256,7 @@ class InnerViewItem extends StatelessWidget { isFeedback: true, ); }, + child: child, ); } else { // keep the same height of the DraggableItem @@ -254,6 +268,37 @@ class InnerViewItem extends StatelessWidget { return child; } + + void _moveViewCrossSection( + BuildContext context, + ViewPB from, + ViewPB to, + ) { + if (isReferencedDatabaseView(view, parentView)) { + return; + } + final fromSection = categoryType == FolderCategoryType.public + ? ViewSectionPB.Private + : ViewSectionPB.Public; + final toSection = categoryType == FolderCategoryType.public + ? ViewSectionPB.Public + : ViewSectionPB.Private; + context.read().add( + ViewEvent.move( + from, + to.parentViewId, + null, + fromSection, + toSection, + ), + ); + context.read().add( + ViewEvent.updateViewVisibility( + from, + categoryType == FolderCategoryType.public, + ), + ); + } } class SingleInnerViewItem extends StatefulWidget { @@ -272,6 +317,7 @@ class SingleInnerViewItem extends StatefulWidget { required this.isFeedback, required this.height, this.isHoverEnabled = true, + this.isPlaceholder = false, }); final ViewPB view; @@ -291,6 +337,7 @@ class SingleInnerViewItem extends StatefulWidget { final double height; final bool isHoverEnabled; + final bool isPlaceholder; @override State createState() => _SingleInnerViewItemState(); @@ -305,6 +352,13 @@ class _SingleInnerViewItemState extends State { final isSelected = getIt().latestOpenView?.id == widget.view.id; + if (widget.isPlaceholder) { + return const SizedBox( + height: 4, + width: double.infinity, + ); + } + if (widget.isFeedback || !widget.isHoverEnabled) { return _buildViewItem( false, @@ -475,6 +529,7 @@ class _SingleInnerViewItemState extends State { viewName, pluginBuilder.layoutType!, openAfterCreated: openAfterCreated, + section: widget.categoryType.toViewSectionPB, ), ); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart index c890aea90c..abccd04056 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/toast.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/size.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; class FlowyMessageToast extends StatelessWidget { @@ -70,6 +69,7 @@ void showSnackBarMessage( content: FlowyText( message, color: Colors.white, + maxLines: 2, ), ), ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart index dec66ea408..1394758fe1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/notification_dialog.dart @@ -35,6 +35,8 @@ class _NotificationDialogState extends State @override void initState() { super.initState(); + // Get all the past and upcoming reminders + _reminderBloc.add(const ReminderEvent.started()); _controller.addListener(_updateState); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart index 45ac056a4f..a7925dc3f7 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/notifications/widgets/notification_button.dart @@ -2,8 +2,8 @@ import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; import 'package:appflowy/workspace/presentation/notifications/notification_dialog.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; @@ -13,12 +13,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class NotificationButton extends StatelessWidget { - const NotificationButton({super.key, required this.views}); - - final List views; + const NotificationButton({ + super.key, + }); @override Widget build(BuildContext context) { + final views = context.watch().state.section.views; final mutex = PopoverMutex(); return BlocProvider.value( diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart index 484212a011..195037dd4c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/settings_dialog.dart @@ -50,6 +50,7 @@ class SettingsDialog extends StatelessWidget { color: Theme.of(context).colorScheme.tertiary, ), ), + width: MediaQuery.of(context).size.width * 0.7, child: ScaffoldMessenger( child: Scaffold( backgroundColor: Colors.transparent, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart index da5b16a7fa..9865372105 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart @@ -13,9 +13,11 @@ class FeatureFlagsPage extends StatelessWidget { return SingleChildScrollView( child: SeparatedColumn( children: [ - ...FeatureFlag.data.entries.map( - (e) => _FeatureFlagItem(featureFlag: e.key), - ), + ...FeatureFlag.data.entries + .where((e) => e.key != FeatureFlag.unknown) + .map( + (e) => _FeatureFlagItem(featureFlag: e.key), + ), FlowyTextButton( 'Restart the app to apply changes', fontSize: 16.0, @@ -57,7 +59,7 @@ class _FeatureFlagItemState extends State<_FeatureFlagItem> { widget.featureFlag.description, maxLines: 3, ), - trailing: Switch( + trailing: Switch.adaptive( value: widget.featureFlag.isOn, onChanged: (value) { setState(() { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart new file mode 100644 index 0000000000..c55522b7d3 --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart @@ -0,0 +1,20 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart'; +import 'package:flutter/material.dart'; + +class FeatureFlagScreen extends StatelessWidget { + const FeatureFlagScreen({ + super.key, + }); + + static const routeName = '/feature_flag'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Feature Flags'), + ), + body: const FeatureFlagsPage(), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart index aaa63c3b9b..6b148b8f2b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart @@ -1,9 +1,13 @@ +import 'package:appflowy/user/application/user_service.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:appflowy_result/appflowy_result.dart'; import 'package:collection/collection.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:protobuf/protobuf.dart'; part 'workspace_member_bloc.freezed.dart'; @@ -21,47 +25,118 @@ class WorkspaceMemberBloc WorkspaceMemberBloc({ required this.userProfile, this.workspace, - }) : super(WorkspaceMemberState.initial()) { + }) : _userBackendService = UserBackendService(userId: userProfile.id), + super(WorkspaceMemberState.initial()) { on((event, emit) async { await event.when( initial: () async { - if (workspace != null) { - workspaceId = workspace!.workspaceId; - } else { - final currentWorkspace = - await FolderEventReadCurrentWorkspace().send(); - currentWorkspace.fold((s) { - workspaceId = s.id; - }, (e) { - assert(false, 'Failed to read current workspace: $e'); - Log.error('Failed to read current workspace: $e'); - workspaceId = ''; - }); - } + await _setCurrentWorkspaceId(); - add(const WorkspaceMemberEvent.getWorkspaceMembers()); - }, - getWorkspaceMembers: () async { - final members = await _getWorkspaceMembers(); + final result = await _userBackendService.getWorkspaceMembers( + _workspaceId, + ); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); final myRole = _getMyRole(members); emit( state.copyWith( members: members, myRole: myRole, + isLoading: false, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), + ), + ); + }, + getWorkspaceMembers: () async { + final result = await _userBackendService.getWorkspaceMembers( + _workspaceId, + ); + final members = result.fold>( + (s) => s.items, + (e) => [], + ); + final myRole = _getMyRole(members); + emit( + state.copyWith( + members: members, + myRole: myRole, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.get, + result: result, + ), ), ); }, addWorkspaceMember: (email) async { - await _addWorkspaceMember(email); - add(const WorkspaceMemberEvent.getWorkspaceMembers()); + final result = await _userBackendService.addWorkspaceMember( + _workspaceId, + email, + ); + emit( + state.copyWith( + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.add, + result: result, + ), + ), + ); + // the addWorkspaceMember doesn't return the updated members, + // so we need to get the members again + result.onSuccess((s) { + add(const WorkspaceMemberEvent.getWorkspaceMembers()); + }); }, removeWorkspaceMember: (email) async { - await _removeWorkspaceMember(email); - add(const WorkspaceMemberEvent.getWorkspaceMembers()); + final result = await _userBackendService.removeWorkspaceMember( + _workspaceId, + email, + ); + final members = result.fold( + (s) => state.members.where((e) => e.email != email).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.remove, + result: result, + ), + ), + ); }, updateWorkspaceMember: (email, role) async { - await _updateWorkspaceMember(email, role); - add(const WorkspaceMemberEvent.getWorkspaceMembers()); + final result = await _userBackendService.updateWorkspaceMember( + _workspaceId, + email, + role, + ); + final members = result.fold( + (s) => state.members.map((e) { + if (e.email == email) { + e.freeze(); + return e.rebuild((p0) { + p0.role = role; + }); + } + return e; + }).toList(), + (e) => state.members, + ); + emit( + state.copyWith( + members: members, + actionResult: WorkspaceMemberActionResult( + actionType: WorkspaceMemberActionType.updateRole, + result: result, + ), + ), + ); }, ); }); @@ -72,16 +147,8 @@ class WorkspaceMemberBloc // if the workspace is null, use the current workspace final UserWorkspacePB? workspace; - late final String workspaceId; - - Future> _getWorkspaceMembers() async { - final data = QueryWorkspacePB()..workspaceId = workspaceId; - final result = await UserEventGetWorkspaceMember(data).send(); - return result.fold((s) => s.items, (e) { - Log.error('Failed to read workspace members: $e'); - return []; - }); - } + late final String _workspaceId; + final UserBackendService _userBackendService; AFRolePB _getMyRole(List members) { final role = members @@ -96,41 +163,19 @@ class WorkspaceMemberBloc return role; } - Future _addWorkspaceMember(String email) async { - final data = AddWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email; - final result = await UserEventAddWorkspaceMember(data).send(); - result.fold((s) { - Log.info('Added workspace member: $data'); - }, (e) { - Log.error('Failed to add workspace member: $e'); - }); - } - - Future _removeWorkspaceMember(String email) async { - final data = RemoveWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email; - final result = await UserEventRemoveWorkspaceMember(data).send(); - result.fold((s) { - Log.info('Removed workspace member: $data'); - }, (e) { - Log.error('Failed to remove workspace member: $e'); - }); - } - - Future _updateWorkspaceMember(String email, AFRolePB role) async { - final data = UpdateWorkspaceMemberPB() - ..workspaceId = workspaceId - ..email = email - ..role = role; - final result = await UserEventUpdateWorkspaceMember(data).send(); - result.fold((s) { - Log.info('Updated workspace member: $data'); - }, (e) { - Log.error('Failed to update workspace member: $e'); - }); + Future _setCurrentWorkspaceId() async { + if (workspace != null) { + _workspaceId = workspace!.workspaceId; + } else { + final currentWorkspace = await FolderEventReadCurrentWorkspace().send(); + currentWorkspace.fold((s) { + _workspaceId = s.id; + }, (e) { + assert(false, 'Failed to read current workspace: $e'); + Log.error('Failed to read current workspace: $e'); + _workspaceId = ''; + }); + } } } @@ -149,12 +194,47 @@ class WorkspaceMemberEvent with _$WorkspaceMemberEvent { ) = UpdateWorkspaceMember; } +enum WorkspaceMemberActionType { + none, + get, + add, + remove, + updateRole, +} + +class WorkspaceMemberActionResult { + const WorkspaceMemberActionResult({ + required this.actionType, + required this.result, + }); + + final WorkspaceMemberActionType actionType; + final FlowyResult result; +} + @freezed class WorkspaceMemberState with _$WorkspaceMemberState { + const WorkspaceMemberState._(); + const factory WorkspaceMemberState({ @Default([]) List members, @Default(AFRolePB.Guest) AFRolePB myRole, + @Default(null) WorkspaceMemberActionResult? actionResult, + @Default(true) bool isLoading, }) = _WorkspaceMemberState; factory WorkspaceMemberState.initial() => const WorkspaceMemberState(); + + @override + int get hashCode => runtimeType.hashCode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is WorkspaceMemberState && + other.members == members && + other.myRole == myRole && + identical(other.actionResult, actionResult); + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart index 14ff872cdb..9779ed7632 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/members/workspace_member_page.dart @@ -3,12 +3,14 @@ import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/shared/af_role_pb_extension.dart'; import 'package:appflowy/workspace/presentation/home/toast.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart'; +import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; +import 'package:appflowy_backend/log.dart'; +import 'package:appflowy_backend/protobuf/flowy-error/code.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/rounded_button.dart'; import 'package:flutter/material.dart'; @@ -28,7 +30,8 @@ class WorkspaceMembersPage extends StatelessWidget { return BlocProvider( create: (context) => WorkspaceMemberBloc(userProfile: userProfile) ..add(const WorkspaceMemberEvent.initial()), - child: BlocBuilder( + child: BlocConsumer( + listener: _showResultDialog, builder: (context, state) { return SingleChildScrollView( child: Column( @@ -46,6 +49,7 @@ class WorkspaceMembersPage extends StatelessWidget { userProfile: userProfile, myRole: state.myRole, ), + const VSpace(48.0), ], ), ); @@ -53,6 +57,43 @@ class WorkspaceMembersPage extends StatelessWidget { ), ); } + + void _showResultDialog(BuildContext context, WorkspaceMemberState state) { + final actionResult = state.actionResult; + if (actionResult == null) { + return; + } + + final actionType = actionResult.actionType; + final result = actionResult.result; + + // only show the result dialog when the action is WorkspaceMemberActionType.add + if (actionType == WorkspaceMemberActionType.add) { + result.fold( + (s) { + showSnackBarMessage( + context, + LocaleKeys.settings_appearance_members_addMemberSuccess.tr(), + ); + }, + (f) { + final message = f.code == ErrorCode.WorkspaceMemberLimitExceeded + ? LocaleKeys.settings_appearance_members_memberLimitExceeded.tr() + : LocaleKeys.settings_appearance_members_failedToAddMember.tr(); + showDialog( + context: context, + builder: (context) => NavigatorOkCancelDialog(message: message), + ); + }, + ); + } + + result.onFailure((f) { + Log.error( + '[Member] Failed to perform ${actionType.toString()} action: $f', + ); + }); + } } class _InviteMember extends StatefulWidget { @@ -111,6 +152,7 @@ class _InviteMemberState extends State<_InviteMember> { ], ), const VSpace(16.0), + /* Enable this when the feature is ready PrimaryButton( backgroundColor: const Color(0xFFE0E0E0), child: Padding( @@ -140,6 +182,7 @@ class _InviteMemberState extends State<_InviteMember> { }, ), const VSpace(16.0), + */ const Divider( height: 1.0, thickness: 1.0, @@ -160,10 +203,6 @@ class _InviteMemberState extends State<_InviteMember> { context .read() .add(WorkspaceMemberEvent.addWorkspaceMember(email)); - showSnackBarMessage( - context, - LocaleKeys.settings_appearance_members_emailSent.tr(), - ); } } @@ -310,14 +349,24 @@ class _MemberMoreActionList extends StatelessWidget { }, ); }, - onSelected: (action, controller) async { + onSelected: (action, controller) { switch (action.inner) { case _MemberMoreAction.delete: - context.read().add( - WorkspaceMemberEvent.removeWorkspaceMember( - action.member.email, - ), - ); + showDialog( + context: context, + builder: (_) => NavigatorOkCancelDialog( + title: LocaleKeys.settings_appearance_members_removeMember.tr(), + message: LocaleKeys + .settings_appearance_members_areYouSureToRemoveMember + .tr(), + onOkPressed: () => context.read().add( + WorkspaceMemberEvent.removeWorkspaceMember( + action.member.email, + ), + ), + okTitle: LocaleKeys.button_yes.tr(), + ), + ); break; } controller.close(); @@ -353,7 +402,7 @@ class _MemberRoleActionList extends StatelessWidget { return PopoverActionList<_MemberRoleActionWrapper>( asBarrier: true, direction: PopoverDirection.bottomWithLeftAligned, - actions: [AFRolePB.Member, AFRolePB.Guest] + actions: [AFRolePB.Member] .map((e) => _MemberRoleActionWrapper(e, member)) .toList(), offset: const Offset(0, 10), diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart index 93ab2b2754..c68fc76ea6 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/common/type_option_separator.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_text_field.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart index 7472045672..0c9c6aaa8e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_time_settings.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; -import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; +import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/layout.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 1458bcad5b..513f72b4ed 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/startup/tasks/app_widget.dart'; import 'package:easy_localization/easy_localization.dart'; @@ -8,7 +10,6 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart'; import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart'; @@ -114,12 +115,14 @@ class NavigatorAlertDialog extends StatefulWidget { this.cancel, this.confirm, this.hideCancelButton = false, + this.constraints, }); final String title; final void Function()? cancel; final void Function()? confirm; final bool hideCancelButton; + final BoxConstraints? constraints; @override State createState() => _CreateFlowyAlertDialog(); @@ -140,10 +143,11 @@ class _CreateFlowyAlertDialog extends State { children: [ ...[ ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 400, - maxHeight: 260, - ), + constraints: widget.constraints ?? + const BoxConstraints( + maxWidth: 400, + maxHeight: 260, + ), child: FlowyText.medium( widget.title, fontSize: FontSizes.s16, @@ -182,7 +186,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { this.okTitle, this.cancelTitle, this.title, - required this.message, + this.message, this.maxWidth, }); @@ -191,13 +195,14 @@ class NavigatorOkCancelDialog extends StatelessWidget { final String? okTitle; final String? cancelTitle; final String? title; - final String message; + final String? message; final double? maxWidth; @override Widget build(BuildContext context) { return StyledDialog( maxWidth: maxWidth ?? 500, + padding: EdgeInsets.symmetric(horizontal: Insets.xl, vertical: Insets.l), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -205,6 +210,7 @@ class NavigatorOkCancelDialog extends StatelessWidget { FlowyText.medium( title!.toUpperCase(), fontSize: FontSizes.s16, + maxLines: 3, ), VSpace(Insets.sm * 1.5), Container( @@ -213,7 +219,11 @@ class NavigatorOkCancelDialog extends StatelessWidget { ), VSpace(Insets.m * 1.5), ], - FlowyText.medium(message), + if (message != null) + FlowyText.medium( + message!, + maxLines: 3, + ), SizedBox(height: Insets.l), OkCancelButton( onOkPressed: () { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart index c8634b3df5..8750eddb0b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/more_view_actions/more_view_actions.dart @@ -1,5 +1,3 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; @@ -13,6 +11,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/hover.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MoreViewActions extends StatefulWidget { @@ -105,8 +104,8 @@ class _MoreViewActionsState extends State { builder: (context, isHovering) => Padding( padding: const EdgeInsets.all(6), child: FlowySvg( - FlowySvgs.details_s, - size: const Size(18, 18), + FlowySvgs.three_dots_vertical_s, + size: const Size.square(16), color: isHovering ? Theme.of(context).colorScheme.onPrimary : Theme.of(context).iconTheme.color, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart index 66ccbe01e0..3f86b57181 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/user_avatar.dart @@ -29,7 +29,7 @@ class UserAvatar extends StatelessWidget { if (iconUrl.isEmpty) { final String nameOrDefault = _userName(name); - final Color color = ColorGenerator.generateColorFromString(name); + final Color color = ColorGenerator(name).toColor(); const initialsCount = 2; // Taking the first letters of the name components and limiting to 2 elements @@ -50,7 +50,7 @@ class UserAvatar extends StatelessWidget { ), child: FlowyText.semibold( nameInitials, - color: Colors.white, + color: Colors.black, fontSize: isLarge ? nameInitials.length == initialsCount ? 20 diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart index 17eefc44f0..378f29d3a1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -1,8 +1,7 @@ +import 'package:appflowy_popover/src/layout.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:appflowy_popover/src/layout.dart'; - import 'mask.dart'; import 'mutex.dart'; @@ -79,7 +78,8 @@ class Popover extends StatefulWidget { /// The direction of the popover final PopoverDirection direction; - final void Function()? onClose; + final VoidCallback? onOpen; + final VoidCallback? onClose; final Future Function()? canClose; final bool asBarrier; @@ -109,6 +109,7 @@ class Popover extends StatefulWidget { this.direction = PopoverDirection.rightWithTopAligned, this.mutex, this.windowPadding, + this.onOpen, this.onClose, this.canClose, this.asBarrier = false, @@ -228,6 +229,7 @@ class PopoverState extends State { child: _buildClickHandler( widget.child, () { + widget.onOpen?.call(); if (widget.triggerActions & PopoverTriggerFlags.click != 0) { showOverlay(); } diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart index 328aa03556..94cd9a68a6 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/async_result.dart @@ -8,6 +8,10 @@ extension FlowyAsyncResultExtension return then((result) => result.getOrElse(onFailure)); } + Future toNullable() { + return then((result) => result.toNullable()); + } + Future getOrThrow() { return then((result) => result.getOrThrow()); } @@ -20,11 +24,11 @@ extension FlowyAsyncResultExtension } Future isError() { - return then((result) => result.isFailure()); + return then((result) => result.isFailure); } Future isSuccess() { - return then((result) => result.isSuccess()); + return then((result) => result.isSuccess); } FlowyAsyncResult onFailure(void Function(F failure) onFailure) { diff --git a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart index dbffef42c7..eca6726b9e 100644 --- a/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart +++ b/frontend/appflowy_flutter/packages/appflowy_result/lib/src/result.dart @@ -10,8 +10,8 @@ abstract class FlowyResult { FlowyResult map(T Function(S success) fn); FlowyResult mapError(T Function(F failure) fn); - bool isSuccess(); - bool isFailure(); + bool get isSuccess; + bool get isFailure; S? toNullable(); @@ -20,6 +20,8 @@ abstract class FlowyResult { S getOrElse(S Function(F failure) onFailure); S getOrThrow(); + + F getFailure(); } class FlowySuccess implements FlowyResult { @@ -57,14 +59,10 @@ class FlowySuccess implements FlowyResult { } @override - bool isSuccess() { - return true; - } + bool get isSuccess => true; @override - bool isFailure() { - return false; - } + bool get isFailure => false; @override S? toNullable() { @@ -88,6 +86,11 @@ class FlowySuccess implements FlowyResult { S getOrThrow() { return _value; } + + @override + F getFailure() { + throw UnimplementedError(); + } } class FlowyFailure implements FlowyResult { @@ -125,14 +128,10 @@ class FlowyFailure implements FlowyResult { } @override - bool isSuccess() { - return false; - } + bool get isSuccess => false; @override - bool isFailure() { - return true; - } + bool get isFailure => true; @override S? toNullable() { @@ -156,4 +155,9 @@ class FlowyFailure implements FlowyResult { S getOrThrow() { throw _value; } + + @override + F getFailure() { + return _value; + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index c4a889fbd9..234de2d736 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -37,6 +37,8 @@ String languageFromLocale(Locale locale) { return "Español"; case "eu": return "Euskera"; + case "el": + return "Ελληνικά"; case "fr": switch (locale.countryCode) { case "CA": diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart index 0a3ff0a119..3014d393dd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/appflowy_popover.dart @@ -1,7 +1,6 @@ -import 'package:flutter/material.dart'; - import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; class AppFlowyPopover extends StatelessWidget { final Widget child; @@ -10,7 +9,8 @@ class AppFlowyPopover extends StatelessWidget { final PopoverDirection direction; final int triggerActions; final BoxConstraints constraints; - final void Function()? onClose; + final VoidCallback? onOpen; + final VoidCallback? onClose; final Future Function()? canClose; final PopoverMutex? mutex; final Offset? offset; @@ -35,6 +35,7 @@ class AppFlowyPopover extends StatelessWidget { required this.child, required this.popupBuilder, this.direction = PopoverDirection.rightWithTopAligned, + this.onOpen, this.onClose, this.canClose, this.constraints = const BoxConstraints(maxWidth: 240, maxHeight: 600), @@ -54,6 +55,7 @@ class AppFlowyPopover extends StatelessWidget { Widget build(BuildContext context) { return Popover( controller: controller, + onOpen: onOpen, onClose: onClose, canClose: canClose, direction: direction, diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart index cd37051220..7ad09eb4f7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_dialog.dart @@ -1,6 +1,7 @@ -import 'package:flutter/material.dart'; import 'dart:math'; +import 'package:flutter/material.dart'; + const _overlayContainerPadding = EdgeInsets.symmetric(vertical: 12); const overlayContainerMaxWidth = 760.0; const overlayContainerMinWidth = 320.0; @@ -14,6 +15,7 @@ class FlowyDialog extends StatelessWidget { this.constraints, this.padding = _overlayContainerPadding, this.backgroundColor, + this.width, }); final Widget? title; @@ -22,11 +24,12 @@ class FlowyDialog extends StatelessWidget { final BoxConstraints? constraints; final EdgeInsets padding; final Color? backgroundColor; + final double? width; @override Widget build(BuildContext context) { final windowSize = MediaQuery.of(context).size; - final size = windowSize * 0.7; + final size = windowSize * 0.6; return SimpleDialog( contentPadding: EdgeInsets.zero, backgroundColor: backgroundColor ?? Theme.of(context).cardColor, @@ -38,8 +41,11 @@ class FlowyDialog extends StatelessWidget { type: MaterialType.transparency, child: Container( height: size.height, - width: max(min(size.width, overlayContainerMaxWidth), - overlayContainerMinWidth), + width: width ?? + max( + min(size.width, overlayContainerMaxWidth), + overlayContainerMinWidth, + ), constraints: constraints, child: child, ), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart index 994e427202..1fce5c0714 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_field.dart @@ -20,6 +20,7 @@ class FlowyTextField extends StatefulWidget { final bool submitOnLeave; final Duration? debounceDuration; final String? errorText; + final Widget? error; final int? maxLines; final bool showCounter; final Widget? prefixIcon; @@ -30,7 +31,7 @@ class FlowyTextField extends StatefulWidget { final TextStyle? hintStyle; final InputDecoration? decoration; final TextAlignVertical? textAlignVertical; - final TextInputAction? textInputAction; + final TextInputType? keyboardType; final List? inputFormatters; const FlowyTextField({ @@ -50,6 +51,7 @@ class FlowyTextField extends StatefulWidget { this.submitOnLeave = false, this.debounceDuration, this.errorText, + this.error, this.maxLines = 1, this.showCounter = true, this.prefixIcon, @@ -60,7 +62,7 @@ class FlowyTextField extends StatefulWidget { this.hintStyle, this.decoration, this.textAlignVertical, - this.textInputAction, + this.keyboardType = TextInputType.multiline, this.inputFormatters, }); @@ -145,7 +147,6 @@ class FlowyTextFieldState extends State { _onChanged(text); } }, - textInputAction: widget.textInputAction, onSubmitted: (text) => _onSubmitted(text), onEditingComplete: widget.onEditingComplete, minLines: 1, @@ -154,7 +155,7 @@ class FlowyTextFieldState extends State { maxLengthEnforcement: MaxLengthEnforcement.truncateAfterCompositionEnds, style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, textAlignVertical: widget.textAlignVertical ?? TextAlignVertical.center, - keyboardType: TextInputType.multiline, + keyboardType: widget.keyboardType, inputFormatters: widget.inputFormatters, decoration: widget.decoration ?? InputDecoration( @@ -177,6 +178,7 @@ class FlowyTextFieldState extends State { isDense: false, hintText: widget.hintText, errorText: widget.errorText, + error: widget.error, errorStyle: Theme.of(context) .textTheme .bodySmall! diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index a401564159..29eb4a04ca 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -53,8 +53,8 @@ packages: dependency: "direct main" description: path: "." - ref: cbd92c4 - resolved-ref: cbd92c4cd13844a5a34a73ef7614e8e79e374a16 + ref: b927ec0 + resolved-ref: b927ec0685c870c731c5b6d9688a031d0cd31e76 url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git version: "2.3.3" @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + avatar_stack: + dependency: "direct main" + description: + name: avatar_stack + sha256: e4a1576f7478add964bbb8aa5e530db39288fbbf81c30c4fb4b81162dd68aa49 + url: "https://pub.dev" + source: hosted + version: "1.2.0" bloc: dependency: "direct main" description: @@ -459,7 +467,7 @@ packages: source: hosted version: "2.1.2" file: - dependency: transitive + dependency: "direct main" description: name: file sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" @@ -565,10 +573,11 @@ packages: flutter_cache_manager: dependency: "direct main" description: - name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" - url: "https://pub.dev" - source: hosted + path: flutter_cache_manager + ref: HEAD + resolved-ref: fbab857b1b1d209240a146d32f496379b9f62276 + url: "https://github.com/LucasXu0/flutter_cache_manager.git" + source: git version: "3.3.1" flutter_chat_types: dependency: transitive @@ -1605,11 +1614,11 @@ packages: dependency: "direct main" description: path: sheet - ref: "8ee20bd" - resolved-ref: "8ee20bd36acaeb36996a09ba9d0f9e7059bb49df" - url: "https://github.com/richardshiue/modal_bottom_sheet" + ref: e44458d + resolved-ref: e44458d2359565324e117bb3d41da04f5e60362e + url: "https://github.com/jamesblasco/modal_bottom_sheet" source: git - version: "1.0.0-pre" + version: "1.0.0" shelf: dependency: transitive description: diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index d0d7de421b..cb569c2331 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,11 +15,11 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.5.1 +version: 0.5.4 environment: flutter: ">=3.19.0" - sdk: ">=3.1.5 <4.0.0" + sdk: ">=3.3.0 <4.0.0" # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -131,6 +131,8 @@ dependencies: flutter_cache_manager: ^3.3.1 share_plus: ^7.2.1 sheet: + file: ^7.0.0 + avatar_stack: ^1.2.0 dev_dependencies: flutter_lints: ^3.0.1 @@ -167,16 +169,22 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "cbd92c4" + ref: "b927ec0" sheet: git: - url: https://github.com/richardshiue/modal_bottom_sheet - ref: 8ee20bd + url: https://github.com/jamesblasco/modal_bottom_sheet + ref: e44458d path: sheet uuid: ^4.1.0 + flutter_cache_manager: + git: + url: https://github.com/LucasXu0/flutter_cache_manager.git + commit: fbab857b1b1d209240a146d32f496379b9f62276 + path: flutter_cache_manager + # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is # activated in the `analysis_options.yaml` file located at the root of your diff --git a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart index dd07369d9d..2385373d14 100644 --- a/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/board_test/group_by_multi_select_field_test.dart @@ -2,7 +2,7 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/setting/group_bloc.dart'; import 'package:appflowy/plugins/database/board/application/board_bloc.dart'; -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -49,12 +49,6 @@ void main() { boardBloc.groupControllers.values.length == 1, "Expected 1, but receive ${boardBloc.groupControllers.values.length}", ); - final expectedGroupName = "No ${multiSelectField.name}"; - assert( - boardBloc.groupControllers.values.first.group.groupName == - expectedGroupName, - "Expected $expectedGroupName, but receive ${boardBloc.groupControllers.values.first.group.groupName}", - ); }); test('group by multi select with no options test', () async { @@ -71,13 +65,13 @@ void main() { context.makeCellControllerFromFieldId(multiSelectField.id) as SelectOptionCellController; - final multiSelectOptionBloc = - SelectOptionCellEditorBloc(cellController: cellController); - multiSelectOptionBloc.add(const SelectOptionEditorEvent.initial()); + final bloc = SelectOptionCellEditorBloc(cellController: cellController); await boardResponseFuture(); - multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await boardResponseFuture(); - multiSelectOptionBloc.add(const SelectOptionEditorEvent.newOption("B")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await boardResponseFuture(); // set grouped by the new multi-select field" @@ -105,11 +99,5 @@ void main() { boardBloc.groupControllers.values.length == 3, "Expected 3, but receive ${boardBloc.groupControllers.values.length}", ); - - final groups = - boardBloc.groupControllers.values.map((e) => e.group).toList(); - assert(groups[0].groupName == "No ${multiSelectField.name}"); - assert(groups[1].groupName == "B"); - assert(groups[2].groupName == "A"); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart index e12fe5e23e..7801fed3e7 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/cell/select_option_cell_test.dart @@ -1,4 +1,4 @@ -import 'package:appflowy/plugins/database/application/cell/bloc/select_option_editor_bloc.dart'; +import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -21,10 +21,10 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); expect(bloc.state.options.length, 1); @@ -40,16 +40,16 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); final SelectOptionPB optionUpdate = bloc.state.options[0] ..color = SelectOptionColorPB.Aqua ..name = "B"; - bloc.add(SelectOptionEditorEvent.updateOption(optionUpdate)); + bloc.add(SelectOptionCellEditorEvent.updateOption(optionUpdate)); expect(bloc.state.options.length, 1); expect(bloc.state.options[0].name, "B"); @@ -65,31 +65,33 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); assert( bloc.state.options.length == 1, "Expect 1 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionEditorEvent.newOption("B")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); assert( bloc.state.options.length == 2, "Expect 2 but receive ${bloc.state.options.length}, Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionEditorEvent.newOption("C")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("C")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); assert( bloc.state.options.length == 3, "Expect 3 but receive ${bloc.state.options.length}. Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionEditorEvent.deleteAllOptions()); + bloc.add(const SelectOptionCellEditorEvent.deleteAllOptions()); await gridResponseFuture(); assert( @@ -107,18 +109,18 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); final optionId = bloc.state.options[0].id; - bloc.add(SelectOptionEditorEvent.unSelectOption(optionId)); + bloc.add(SelectOptionCellEditorEvent.unSelectOption(optionId)); await gridResponseFuture(); assert(bloc.state.selectedOptions.isEmpty); - bloc.add(SelectOptionEditorEvent.selectOption(optionId)); + bloc.add(SelectOptionCellEditorEvent.selectOption(optionId)); await gridResponseFuture(); assert(bloc.state.selectedOptions.length == 1); @@ -134,20 +136,22 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.trySelectOption("B")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); + bloc.add(const SelectOptionCellEditorEvent.submitTextField()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.trySelectOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.submitTextField()); await gridResponseFuture(); - assert(bloc.state.selectedOptions.length == 1); - assert(bloc.state.options.length == 2); + expect(bloc.state.selectedOptions.length, 1); + expect(bloc.state.options.length, 1); expect(bloc.state.selectedOptions[0].name, "A"); }); @@ -160,17 +164,18 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("A")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("A")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("B")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("B")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); bloc.add( - const SelectOptionEditorEvent.selectMultipleOptions( + const SelectOptionCellEditorEvent.selectMultipleOptions( ["A", "B", "C"], "x", ), @@ -179,7 +184,7 @@ void main() { assert(bloc.state.selectedOptions.length == 1); expect(bloc.state.selectedOptions[0].name, "A"); - expect(bloc.state.filter, "x"); + expect(bloc.filter, "x"); }); test('filter options', () async { @@ -191,10 +196,10 @@ void main() { ); final bloc = SelectOptionCellEditorBloc(cellController: cellController); - bloc.add(const SelectOptionEditorEvent.initial()); await gridResponseFuture(); - bloc.add(const SelectOptionEditorEvent.newOption("abcd")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("abcd")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); expect( bloc.state.options.length, @@ -202,7 +207,8 @@ void main() { reason: "Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionEditorEvent.newOption("aaaa")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("aaaa")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); expect( bloc.state.options.length, @@ -210,7 +216,8 @@ void main() { reason: "Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionEditorEvent.newOption("defg")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("defg")); + bloc.add(const SelectOptionCellEditorEvent.createOption()); await gridResponseFuture(); expect( bloc.state.options.length, @@ -218,7 +225,7 @@ void main() { reason: "Options: ${bloc.state.options}", ); - bloc.add(const SelectOptionEditorEvent.filterOption("a")); + bloc.add(const SelectOptionCellEditorEvent.filterOption("a")); await gridResponseFuture(); expect( @@ -227,12 +234,12 @@ void main() { reason: "Options: ${bloc.state.options}", ); expect( - bloc.state.allOptions.length, + bloc.allOptions.length, 3, reason: "Options: ${bloc.state.options}", ); - expect(bloc.state.createOption, "a"); - expect(bloc.state.filter, "a"); + expect(bloc.state.createSelectOptionSuggestion!.name, "a"); + expect(bloc.filter, "a"); }); }); } diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart index e27f9b6b6c..d02a319ab1 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/create_filter_test.dart @@ -42,7 +42,6 @@ void main() { await service.deleteFilter( fieldId: textField.id, filterId: filterInfo.filter.id, - fieldType: textField.fieldType, ); await gridResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart index 4b35a1dc7a..a5aa0c9f58 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_menu_test.dart @@ -13,10 +13,10 @@ void main() { test('test filter menu after create a text filter)', () async { final context = await gridTest.createTestGrid(); - final menuBloc = GridFilterMenuBloc( + final menuBloc = DatabaseFilterMenuBloc( viewId: context.gridView.id, fieldController: context.fieldController, - )..add(const GridFilterMenuEvent.initial()); + )..add(const DatabaseFilterMenuEvent.initial()); await gridResponseFuture(); assert(menuBloc.state.creatableFields.length == 3); @@ -28,15 +28,15 @@ void main() { content: "", ); await gridResponseFuture(); - assert(menuBloc.state.creatableFields.length == 2); + assert(menuBloc.state.creatableFields.length == 3); }); test('test filter menu after update existing text filter)', () async { final context = await gridTest.createTestGrid(); - final menuBloc = GridFilterMenuBloc( + final menuBloc = DatabaseFilterMenuBloc( viewId: context.gridView.id, fieldController: context.fieldController, - )..add(const GridFilterMenuEvent.initial()); + )..add(const DatabaseFilterMenuEvent.initial()); await gridResponseFuture(); final service = FilterBackendService(viewId: context.gridView.id); @@ -55,13 +55,13 @@ void main() { await service.insertTextFilter( fieldId: textField.id, filterId: textFilter.filter.id, - condition: TextFilterConditionPB.Is, + condition: TextFilterConditionPB.TextIs, content: "ABC", ); await gridResponseFuture(); assert( menuBloc.state.filters.first.textFilter()!.condition == - TextFilterConditionPB.Is, + TextFilterConditionPB.TextIs, ); assert(menuBloc.state.filters.first.textFilter()!.content == "ABC"); }); diff --git a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart index 432fd339ae..0af6b18092 100644 --- a/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/grid_test/filter/filter_rows_by_text_test.dart @@ -37,7 +37,6 @@ void main() { await service.deleteFilter( fieldId: textField.id, filterId: textFilter.filter.id, - fieldType: textField.fieldType, ); await gridResponseFuture(); assert(context.rowInfos.length == 3); @@ -65,7 +64,6 @@ void main() { await service.deleteFilter( fieldId: textField.id, filterId: textFilter.filter.id, - fieldType: textField.fieldType, ); await gridResponseFuture(); assert(context.rowInfos.length == 3); @@ -107,7 +105,6 @@ void main() { await service.deleteFilter( fieldId: textField.id, filterId: textFilter.filter.id, - fieldType: textField.fieldType, ); await gridResponseFuture(); assert(context.rowInfos.length == 3); @@ -121,7 +118,7 @@ void main() { // create a new filter await service.insertTextFilter( fieldId: textField.id, - condition: TextFilterConditionPB.Is, + condition: TextFilterConditionPB.TextIs, content: "A", ); await gridResponseFuture(); @@ -135,7 +132,7 @@ void main() { await service.insertTextFilter( fieldId: textField.id, filterId: textFilter.filter.id, - condition: TextFilterConditionPB.Is, + condition: TextFilterConditionPB.TextIs, content: "B", ); await gridResponseFuture(); @@ -145,7 +142,7 @@ void main() { await service.insertTextFilter( fieldId: textField.id, filterId: textFilter.filter.id, - condition: TextFilterConditionPB.Is, + condition: TextFilterConditionPB.TextIs, content: "b", ); await gridResponseFuture(); @@ -155,7 +152,7 @@ void main() { await service.insertTextFilter( fieldId: textField.id, filterId: textFilter.filter.id, - condition: TextFilterConditionPB.Is, + condition: TextFilterConditionPB.TextIs, content: "C", ); await gridResponseFuture(); @@ -165,7 +162,6 @@ void main() { await service.deleteFilter( fieldId: textField.id, filterId: textFilter.filter.id, - fieldType: textField.fieldType, ); await gridResponseFuture(); assert(context.rowInfos.length == 3); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart index ec7a357ee6..1027d2a719 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/home_bloc_test.dart @@ -38,8 +38,13 @@ void main() { final appBloc = ViewBloc(view: app)..add(const ViewEvent.initial()); assert(appBloc.state.lastCreatedView == null); - appBloc - .add(const ViewEvent.createView("New document", ViewLayoutPB.Document)); + appBloc.add( + const ViewEvent.createView( + "New document", + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); assert(appBloc.state.lastCreatedView != null); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart deleted file mode 100644 index 7c2e115524..0000000000 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/menu_bloc_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import '../../util.dart'; - -void main() { - late AppFlowyUnitTest testContext; - setUpAll(() async { - testContext = await AppFlowyUnitTest.ensureInitialized(); - }); - - test('assert initial apps is the build-in app', () async { - final menuBloc = SidebarRootViewsBloc() - ..add( - SidebarRootViewsEvent.initial( - testContext.userProfile, - testContext.currentWorkspace.id, - ), - ); - - await blocResponseFuture(); - - assert(menuBloc.state.views.length == 1); - }); - - test('reorder apps', () async { - final menuBloc = SidebarRootViewsBloc() - ..add( - SidebarRootViewsEvent.initial( - testContext.userProfile, - testContext.currentWorkspace.id, - ), - ); - await blocResponseFuture(); - menuBloc.add(const SidebarRootViewsEvent.createRootView("App 1")); - await blocResponseFuture(); - menuBloc.add(const SidebarRootViewsEvent.createRootView("App 2")); - await blocResponseFuture(); - menuBloc.add(const SidebarRootViewsEvent.createRootView("App 3")); - await blocResponseFuture(); - - assert(menuBloc.state.views[1].name == 'App 1'); - assert(menuBloc.state.views[2].name == 'App 2'); - assert(menuBloc.state.views[3].name == 'App 3'); - }); -} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart new file mode 100644 index 0000000000..75ade70a87 --- /dev/null +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/sidebar_section_bloc_test.dart @@ -0,0 +1,57 @@ +import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../../util.dart'; + +void main() { + late AppFlowyUnitTest testContext; + setUpAll(() async { + testContext = await AppFlowyUnitTest.ensureInitialized(); + }); + + test('assert initial apps is the build-in app', () async { + final menuBloc = SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + testContext.userProfile, + testContext.currentWorkspace.id, + ), + ); + + await blocResponseFuture(); + + assert(menuBloc.state.section.publicViews.length == 1); + assert(menuBloc.state.section.privateViews.isEmpty); + }); + + test('create views', () async { + final menuBloc = SidebarSectionsBloc() + ..add( + SidebarSectionsEvent.initial( + testContext.userProfile, + testContext.currentWorkspace.id, + ), + ); + await blocResponseFuture(); + + final names = ['View 1', 'View 2', 'View 3']; + for (final name in names) { + menuBloc.add( + SidebarSectionsEvent.createRootViewInSection( + name: name, + index: 0, + viewSection: ViewSectionPB.Public, + ), + ); + await blocResponseFuture(); + } + + final reversedNames = names.reversed.toList(); + for (var i = 0; i < names.length; i++) { + assert( + menuBloc.state.section.publicViews[i].name == reversedNames[i], + ); + } + }); +} diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart index 0bd464e1b0..189a32cbac 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/trash_bloc_test.dart @@ -22,6 +22,7 @@ class TrashTestContext { const ViewEvent.createView( "Document 1", ViewLayoutPB.Document, + section: ViewSectionPB.Public, ), ); await blocResponseFuture(); @@ -30,6 +31,7 @@ class TrashTestContext { const ViewEvent.createView( "Document 2", ViewLayoutPB.Document, + section: ViewSectionPB.Public, ), ); await blocResponseFuture(); @@ -38,6 +40,7 @@ class TrashTestContext { const ViewEvent.createView( "Document 3", ViewLayoutPB.Document, + section: ViewSectionPB.Public, ), ); await blocResponseFuture(); diff --git a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart index f70a8e5ec1..868a003d5b 100644 --- a/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart +++ b/frontend/appflowy_flutter/test/bloc_test/home_test/view_bloc_test.dart @@ -36,7 +36,11 @@ void main() { final viewBloc = await createTestViewBloc(); // create a nested view viewBloc.add( - const ViewEvent.createView(name, ViewLayoutPB.Document), + const ViewEvent.createView( + name, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(viewBloc.state.view.childViews.length, 1); @@ -52,7 +56,11 @@ void main() { test('delete view test', () async { final viewBloc = await createTestViewBloc(); viewBloc.add( - const ViewEvent.createView(name, ViewLayoutPB.Document), + const ViewEvent.createView( + name, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(viewBloc.state.view.childViews.length, 1); @@ -69,7 +77,11 @@ void main() { test('create nested view test', () async { final viewBloc = await createTestViewBloc(); viewBloc.add( - const ViewEvent.createView('Document 1', ViewLayoutPB.Document), + const ViewEvent.createView( + 'Document 1', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); final document1Bloc = ViewBloc(view: viewBloc.state.view.childViews.first) @@ -79,7 +91,11 @@ void main() { await blocResponseFuture(); const name = 'Document 1 - 1'; document1Bloc.add( - const ViewEvent.createView('Document 1 - 1', ViewLayoutPB.Document), + const ViewEvent.createView( + 'Document 1 - 1', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(document1Bloc.state.view.childViews.length, 1); @@ -91,7 +107,11 @@ void main() { final names = ['1', '2', '3']; for (final name in names) { viewBloc.add( - ViewEvent.createView(name, ViewLayoutPB.Document), + ViewEvent.createView( + name, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); } @@ -106,7 +126,13 @@ void main() { final viewBloc = await createTestViewBloc(); expect(viewBloc.state.lastCreatedView, isNull); - viewBloc.add(const ViewEvent.createView('1', ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + '1', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); expect( viewBloc.state.lastCreatedView!.id, @@ -117,7 +143,13 @@ void main() { '1', ); - viewBloc.add(const ViewEvent.createView('2', ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + '2', + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); expect( viewBloc.state.lastCreatedView!.name, @@ -128,13 +160,25 @@ void main() { test('open latest document test', () async { const name1 = 'document'; final viewBloc = await createTestViewBloc(); - viewBloc.add(const ViewEvent.createView(name1, ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + name1, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); final document = viewBloc.state.lastCreatedView!; assert(document.name == name1); const gird = 'grid'; - viewBloc.add(const ViewEvent.createView(gird, ViewLayoutPB.Document)); + viewBloc.add( + const ViewEvent.createView( + gird, + ViewLayoutPB.Document, + section: ViewSectionPB.Public, + ), + ); await blocResponseFuture(); assert(viewBloc.state.lastCreatedView!.name == gird); @@ -170,7 +214,11 @@ void main() { for (var i = 0; i < layouts.length; i++) { final layout = layouts[i]; viewBloc.add( - ViewEvent.createView('Test $layout', layout), + ViewEvent.createView( + 'Test $layout', + layout, + section: ViewSectionPB.Public, + ), ); await blocResponseFuture(); expect(viewBloc.state.view.childViews.length, i + 1); diff --git a/frontend/appflowy_flutter/test/util.dart b/frontend/appflowy_flutter/test/util.dart index 2cf163688d..65303cb789 100644 --- a/frontend/appflowy_flutter/test/util.dart +++ b/frontend/appflowy_flutter/test/util.dart @@ -74,7 +74,10 @@ class AppFlowyUnitTest { } Future createWorkspace() async { - final result = await workspaceService.createApp(name: "Test App"); + final result = await workspaceService.createView( + name: "Test App", + viewSection: ViewSectionPB.Public, + ); return result.fold( (app) => app, (error) => throw Exception(error), @@ -82,7 +85,7 @@ class AppFlowyUnitTest { } Future> loadApps() async { - final result = await workspaceService.getViews(); + final result = await workspaceService.getPublicViews(); return result.fold( (apps) => apps, diff --git a/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart b/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart index e4c0cff7e2..b1e2e4ccea 100644 --- a/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/select_option_text_field_test.dart @@ -17,11 +17,13 @@ void main() { String remainder = ''; List select = []; + final textController = TextEditingController(); + final textField = SelectOptionTextField( options: const [], selectedOptionMap: LinkedHashMap(), distanceToText: 0.0, - onSubmitted: (text) => submit = text, + onSubmitted: () => submit = textController.text, onPaste: (options, remaining) { remainder = remaining; select = options; @@ -29,7 +31,8 @@ void main() { onRemove: (_) {}, newText: (text) => remainder = text, textSeparators: const [','], - textController: TextEditingController(), + textController: textController, + focusNode: FocusNode(), ); testWidgets('SelectOptionTextField callback outputs', @@ -57,11 +60,6 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); expect(submit, 'an option'); - submit = ''; - await tester.enterText(find.byType(TextField), ' '); - await tester.testTextInput.receiveAction(TextInputAction.done); - expect(submit, ''); - // test inputs containing commas await tester.enterText(find.byType(TextField), 'a a, bbbb , c'); expect(remainder, 'c'); diff --git a/frontend/appflowy_tauri/.gitignore b/frontend/appflowy_tauri/.gitignore index 6a6338d33e..32a3d59bc2 100644 --- a/frontend/appflowy_tauri/.gitignore +++ b/frontend/appflowy_tauri/.gitignore @@ -28,4 +28,6 @@ dist-ssr **/src/appflowy_app/i18n/translations/ coverage -**/AppFlowy-Collab \ No newline at end of file +**/AppFlowy-Collab + +.env \ No newline at end of file diff --git a/frontend/appflowy_tauri/package.json b/frontend/appflowy_tauri/package.json index c9f8327f83..30c7978771 100644 --- a/frontend/appflowy_tauri/package.json +++ b/frontend/appflowy_tauri/package.json @@ -52,6 +52,7 @@ "react-beautiful-dnd": "^13.1.1", "react-big-calendar": "^1.8.5", "react-color": "^2.19.3", + "react-custom-scrollbars": "^4.2.1", "react-datepicker": "^4.23.0", "react-dom": "^18.2.0", "react-error-boundary": "^3.1.4", @@ -79,8 +80,8 @@ "yjs": "^13.5.51" }, "devDependencies": { - "@tauri-apps/cli": "^1.5.6", "@svgr/plugin-svgo": "^8.0.1", + "@tauri-apps/cli": "^1.5.6", "@types/google-protobuf": "^3.15.12", "@types/is-hotkey": "^0.1.7", "@types/jest": "^29.5.3", @@ -92,6 +93,7 @@ "@types/react": "^18.0.15", "@types/react-beautiful-dnd": "^13.1.3", "@types/react-color": "^3.0.6", + "@types/react-custom-scrollbars": "^4.0.13", "@types/react-datepicker": "^4.19.3", "@types/react-dom": "^18.0.6", "@types/react-katex": "^3.0.0", diff --git a/frontend/appflowy_tauri/pnpm-lock.yaml b/frontend/appflowy_tauri/pnpm-lock.yaml index a6d544c10a..d670b8b312 100644 --- a/frontend/appflowy_tauri/pnpm-lock.yaml +++ b/frontend/appflowy_tauri/pnpm-lock.yaml @@ -103,6 +103,9 @@ dependencies: react-color: specifier: ^2.19.3 version: 2.19.3(react@18.2.0) + react-custom-scrollbars: + specifier: ^4.2.1 + version: 4.2.1(react-dom@18.2.0)(react@18.2.0) react-datepicker: specifier: ^4.23.0 version: 4.23.0(react-dom@18.2.0)(react@18.2.0) @@ -219,6 +222,9 @@ devDependencies: '@types/react-color': specifier: ^3.0.6 version: 3.0.6 + '@types/react-custom-scrollbars': + specifier: ^4.0.13 + version: 4.0.13 '@types/react-datepicker': specifier: ^4.19.3 version: 4.19.3(react-dom@18.2.0)(react@18.2.0) @@ -2346,6 +2352,12 @@ packages: '@types/reactcss': 1.2.6 dev: true + /@types/react-custom-scrollbars@4.0.13: + resolution: {integrity: sha512-t+15reWgAE1jXlrhaZoxjuH/SQf+EG0rzAzSCzTIkSiP5CDT7KhoExNPwIa6uUxtPkjc3gdW/ry7GetLEwCfGA==} + dependencies: + '@types/react': 18.2.6 + dev: true + /@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==} dependencies: @@ -2655,6 +2667,10 @@ packages: hasBin: true dev: true + /add-px-to-style@1.0.0: + resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==} + dev: false + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -3343,6 +3359,14 @@ packages: esutils: 2.0.3 dev: true + /dom-css@2.1.0: + resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} + dependencies: + add-px-to-style: 1.0.0 + prefix-style: 2.0.1 + to-camel-case: 1.0.0 + dev: false + /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: @@ -5426,6 +5450,10 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -5518,6 +5546,10 @@ packages: source-map-js: 1.0.2 dev: true + /prefix-style@2.0.1: + resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==} + dev: false + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -5686,6 +5718,12 @@ packages: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} dev: false + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} peerDependencies: @@ -5746,6 +5784,19 @@ packages: tinycolor2: 1.6.0 dev: false + /react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} peerDependencies: @@ -6627,16 +6678,32 @@ packages: /tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + /to-camel-case@1.0.0: + resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==} + dependencies: + to-space-case: 1.0.0 + dev: false + /to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: false + /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: false + /tough-cookie@4.1.3: resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} engines: {node: '>=6'} diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/OFL.txt b/frontend/appflowy_tauri/public/google_fonts/Poppins/OFL.txt new file mode 100644 index 0000000000..246c977c9f --- /dev/null +++ b/frontend/appflowy_tauri/public/google_fonts/Poppins/OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Poppins Project Authors (https://github.com/itfoundry/Poppins) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Black.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Black.ttf new file mode 100644 index 0000000000..71c0f995ee Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Black.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BlackItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BlackItalic.ttf new file mode 100644 index 0000000000..7aeb58bd1b Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BlackItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Bold.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Bold.ttf new file mode 100644 index 0000000000..00559eeb29 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Bold.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BoldItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BoldItalic.ttf new file mode 100644 index 0000000000..e61e8e88bd Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-BoldItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBold.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBold.ttf new file mode 100644 index 0000000000..df7093608a Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBold.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..14d2b375dc Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraBoldItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLight.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLight.ttf new file mode 100644 index 0000000000..e76ec69a65 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLight.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf new file mode 100644 index 0000000000..89513d9469 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ExtraLightItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Italic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Italic.ttf new file mode 100644 index 0000000000..12b7b3c40b Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Italic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Light.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Light.ttf new file mode 100644 index 0000000000..bc36bcc242 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Light.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-LightItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-LightItalic.ttf new file mode 100644 index 0000000000..9e70be6a9e Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-LightItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Medium.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Medium.ttf new file mode 100644 index 0000000000..6bcdcc27f2 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Medium.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-MediumItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-MediumItalic.ttf new file mode 100644 index 0000000000..be67410fd0 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-MediumItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Regular.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Regular.ttf new file mode 100644 index 0000000000..9f0c71b70a Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Regular.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBold.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBold.ttf new file mode 100644 index 0000000000..74c726e327 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBold.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf new file mode 100644 index 0000000000..3e6c942233 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-SemiBoldItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Thin.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Thin.ttf new file mode 100644 index 0000000000..03e736613a Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-Thin.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ThinItalic.ttf b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ThinItalic.ttf new file mode 100644 index 0000000000..e26db5dd3d Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Poppins/Poppins-ThinItalic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/LICENSE.txt b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/LICENSE.txt new file mode 100644 index 0000000000..75b52484ea --- /dev/null +++ b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf new file mode 100644 index 0000000000..61e5303325 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Italic.ttf differ diff --git a/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf new file mode 100644 index 0000000000..6df2b25360 Binary files /dev/null and b/frontend/appflowy_tauri/public/google_fonts/Roboto_Mono/RobotoMono-Regular.ttf differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg b/frontend/appflowy_tauri/public/launch_splash.jpg similarity index 100% rename from frontend/appflowy_tauri/src/appflowy_app/assets/launch_splash.jpg rename to frontend/appflowy_tauri/public/launch_splash.jpg diff --git a/frontend/appflowy_tauri/src-tauri/.gitignore b/frontend/appflowy_tauri/src-tauri/.gitignore index f4dfb82b2c..61e1bdd46a 100644 --- a/frontend/appflowy_tauri/src-tauri/.gitignore +++ b/frontend/appflowy_tauri/src-tauri/.gitignore @@ -1,4 +1,4 @@ # Generated by Cargo # will have compiled files and executables /target/ - +.env diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 221283296f..e5ab31fa91 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -132,12 +132,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -162,7 +156,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "bincode", @@ -173,8 +167,10 @@ dependencies = [ "serde_repr", "thiserror", "tokio", + "tsify", "url", "uuid", + "wasm-bindgen", ] [[package]] @@ -182,6 +178,7 @@ name = "appflowy_tauri" version = "0.0.0" dependencies = [ "bytes", + "dotenv", "flowy-config", "flowy-core", "flowy-date", @@ -194,6 +191,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-deep-link", "tauri-utils", "tracing", "uuid", @@ -714,7 +712,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "again", "anyhow", @@ -724,8 +722,11 @@ dependencies = [ "brotli", "bytes", "chrono", + "client-websocket", "collab", "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", "database-entity", "futures-core", "futures-util", @@ -734,11 +735,8 @@ dependencies = [ "gotrue-entity", "governor", "mime", - "mime_guess", "parking_lot 0.12.1", "prost", - "realtime-entity", - "realtime-protocol", "reqwest", "scraper 0.17.1", "semver", @@ -754,11 +752,28 @@ dependencies = [ "url", "uuid", "wasm-bindgen-futures", - "websocket", "workspace-template", "yrs", ] +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "cmd_lib" version = "1.3.0" @@ -818,7 +833,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-trait", @@ -834,6 +849,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "unicode-segmentation", "web-sys", "yrs", ] @@ -841,7 +857,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-trait", @@ -849,12 +865,13 @@ dependencies = [ "collab", "collab-entity", "collab-plugins", + "dashmap", "getrandom 0.2.10", "js-sys", "lazy_static", - "lru", "nanoid", "parking_lot 0.12.1", + "rayon", "serde", "serde_json", "serde_repr", @@ -870,7 +887,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "collab", @@ -889,7 +906,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "bytes", @@ -904,7 +921,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "chrono", @@ -930,6 +947,7 @@ dependencies = [ "collab", "collab-entity", "collab-plugins", + "futures", "lib-infra", "parking_lot 0.12.1", "serde", @@ -941,7 +959,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-stream", @@ -977,10 +995,49 @@ dependencies = [ "yrs", ] +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "collab", + "serde", + "thiserror", + "tracing", + "yrs", +] + [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "collab", @@ -1314,7 +1371,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -1438,6 +1495,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1448,6 +1514,18 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1465,6 +1543,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "dtoa" version = "1.0.6" @@ -1742,6 +1826,7 @@ dependencies = [ "lib-infra", "lib-log", "parking_lot 0.12.1", + "semver", "serde", "serde_json", "serde_repr", @@ -1791,7 +1876,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "lru", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1852,6 +1936,7 @@ dependencies = [ "collab-entity", "collab-integrate", "collab-plugins", + "dashmap", "flowy-codegen", "flowy-derive", "flowy-document-pub", @@ -1863,7 +1948,6 @@ dependencies = [ "indexmap 2.1.0", "lib-dispatch", "lib-infra", - "lru", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -2021,6 +2105,7 @@ dependencies = [ "mime_guess", "parking_lot 0.12.1", "postgrest", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -2115,6 +2200,7 @@ dependencies = [ "once_cell", "parking_lot 0.12.1", "protobuf", + "semver", "serde", "serde_json", "serde_repr", @@ -2574,6 +2660,19 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gobject-sys" version = "0.15.10" @@ -2588,7 +2687,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "futures-util", @@ -2605,7 +2704,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -2722,10 +2821,6 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash 0.8.6", - "allocator-api2", -] [[package]] name = "heck" @@ -3060,7 +3155,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "reqwest", @@ -3086,6 +3181,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version", + "to_method", + "winapi", +] + [[package]] name = "ipnet" version = "2.8.0" @@ -3405,15 +3513,6 @@ dependencies = [ "tracing-subscriber", ] -[[package]] -name = "lru" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" -dependencies = [ - "hashbrown 0.14.3", -] - [[package]] name = "mac" version = "0.1.1" @@ -3829,6 +3928,28 @@ dependencies = [ "objc_id", ] +[[package]] +name = "objc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + [[package]] name = "objc_exception" version = "0.1.2" @@ -3932,6 +4053,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "os_pipe" version = "0.9.2" @@ -4781,9 +4908,9 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -4799,43 +4926,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "realtime-entity" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "anyhow", - "bincode", - "bytes", - "collab", - "collab-entity", - "database-entity", - "prost", - "prost-build", - "protoc-bin-vendored", - "realtime-protocol", - "serde", - "serde_json", - "serde_repr", - "thiserror", - "tokio-tungstenite", - "websocket", - "yrs", -] - -[[package]] -name = "realtime-protocol" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "anyhow", - "bincode", - "collab", - "serde", - "thiserror", - "yrs", -] - [[package]] name = "redox_syscall" version = "0.1.57" @@ -5346,6 +5436,17 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "serde_json" version = "1.0.111" @@ -5498,7 +5599,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -6014,6 +6115,22 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8" +dependencies = [ + "dirs", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + [[package]] name = "tauri-runtime" version = "0.14.1" @@ -6240,6 +6357,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + [[package]] name = "tokio" version = "1.36.0" @@ -6532,6 +6655,31 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.47", +] + [[package]] name = "tungstenite" version = "0.20.1" @@ -6991,24 +7139,6 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" -[[package]] -name = "websocket" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "futures-channel", - "futures-util", - "http", - "httparse", - "js-sys", - "percent-encoding", - "thiserror", - "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "webview2-com" version = "0.19.1" @@ -7457,7 +7587,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "async-trait", @@ -7555,8 +7685,7 @@ dependencies = [ [[package]] name = "yrs" version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68aea14c6c33f2edd8a5ff9415360cfa5b98d90cce30c5ee3be59a8419fb15a9" +source = "git+https://github.com/appflowy/y-crdt?rev=3f25bb510ca5274e7657d3713fbed41fb46b4487#3f25bb510ca5274e7657d3713fbed41fb46b4487" dependencies = [ "atomic_refcell", "rand 0.7.3", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 5afe966665..073af93458 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -29,12 +29,12 @@ tokio = "1.34.0" tokio-stream = "0.1.14" async-trait = "0.1.74" chrono = { version = "0.4.31", default-features = false, features = ["clock"] } -lru = "0.12.0" [dependencies] serde_json.workspace = true serde.workspace = true -tauri = { version = "1.5", features = [ "dialog-all", +tauri = { version = "1.5", features = [ + "dialog-all", "clipboard-all", "fs-all", "shell-open", @@ -66,7 +66,10 @@ flowy-document = { path = "../../rust-lib/flowy-document", features = [ flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ "tauri_ts", ] } + uuid = "1.5.0" +tauri-plugin-deep-link = "0.1.2" +dotenv = "0.15.0" [features] # by default Tauri runs in production mode @@ -77,12 +80,14 @@ default = ["custom-protocol"] custom-protocol = ["tauri/custom-protocol"] [patch.crates-io] +yrs = { git = "https://github.com/appflowy/y-crdt", rev = "3f25bb510ca5274e7657d3713fbed41fb46b4487" } + # Please using the following command to update the revision id # Current directory: frontend # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" } # Please use the following script to update collab. # Working directory: frontend # @@ -92,10 +97,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0be # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } diff --git a/frontend/appflowy_tauri/src-tauri/Info.plist b/frontend/appflowy_tauri/src-tauri/Info.plist new file mode 100644 index 0000000000..25b430c049 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/Info.plist @@ -0,0 +1,19 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + + appflowy-flutter + CFBundleURLSchemes + + appflowy-flutter + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src-tauri/env.development b/frontend/appflowy_tauri/src-tauri/env.development new file mode 100644 index 0000000000..188835e3d0 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/env.development @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://test.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://test.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://test.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_tauri/src-tauri/env.production b/frontend/appflowy_tauri/src-tauri/env.production new file mode 100644 index 0000000000..b03c328b84 --- /dev/null +++ b/frontend/appflowy_tauri/src-tauri/env.production @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://beta.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://beta.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://beta.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index 7f7c2726d3..40c0e5d47b 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -3,10 +3,33 @@ use flowy_core::{AppFlowyCore, DEFAULT_NAME}; use lib_dispatch::runtime::AFPluginRuntime; use std::sync::Arc; +use dotenv::dotenv; + +pub fn read_env() { + dotenv().ok(); + + let env = if cfg!(debug_assertions) { + include_str!("../env.development") + } else { + include_str!("../env.production") + }; + + for line in env.lines() { + if let Some((key, value)) = line.split_once('=') { + // Check if the environment variable is not already set in the system + let current_value = std::env::var(key).unwrap_or_default(); + if current_value.is_empty() { + std::env::set_var(key, value); + } + } + } +} + pub fn init_flowy_core() -> AppFlowyCore { let config_json = include_str!("../tauri.conf.json"); let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); + let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string()); let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); if cfg!(debug_assertions) { data_path.push("data_dev"); @@ -18,10 +41,11 @@ pub fn init_flowy_core() -> AppFlowyCore { let application_path = data_path.to_str().unwrap().to_string(); let device_id = uuid::Uuid::new_v4().to_string(); + read_env(); std::env::set_var("RUST_LOG", "trace"); - // TODO(nathan): pass the real version here + let config = AppFlowyCoreConfig::new( - "1.0.0".to_string(), + app_version, custom_application_path, application_path, device_id, diff --git a/frontend/appflowy_tauri/src-tauri/src/main.rs b/frontend/appflowy_tauri/src-tauri/src/main.rs index 111de889f1..6a69de07fd 100644 --- a/frontend/appflowy_tauri/src-tauri/src/main.rs +++ b/frontend/appflowy_tauri/src-tauri/src/main.rs @@ -3,6 +3,10 @@ windows_subsystem = "windows" )] +#[allow(dead_code)] +pub const DEEP_LINK_SCHEME: &str = "appflowy-flutter"; +pub const OPEN_DEEP_LINK: &str = "open_deep_link"; + mod init; mod notification; mod request; @@ -12,8 +16,11 @@ use init::*; use notification::*; use request::*; use tauri::Manager; +extern crate dotenv; fn main() { + tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME); + let flowy_core = init_flowy_core(); tauri::Builder::default() .invoke_handler(tauri::generate_handler![invoke_request]) @@ -26,16 +33,37 @@ fn main() { unregister_all_notification_sender(); register_notification_sender(TSNotificationSender::new(app_handler.clone())); // tauri::async_runtime::spawn(async move {}); + window.listen_global(AF_EVENT, move |event| { on_event(app_handler.clone(), event); }); }) .setup(|_app| { - #[cfg(debug_assertions)] - { - let window = _app.get_window("main").unwrap(); - window.open_devtools(); - } + let splashscreen_window = _app.get_window("splashscreen").unwrap(); + let window = _app.get_window("main").unwrap(); + let handle = _app.handle(); + + // we perform the initialization code on a new task so the app doesn't freeze + tauri::async_runtime::spawn(async move { + // initialize your app here instead of sleeping :) + std::thread::sleep(std::time::Duration::from_secs(2)); + + // After it's done, close the splashscreen and display the main window + splashscreen_window.close().unwrap(); + window.show().unwrap(); + // If you need macOS support this must be called in .setup() ! + // Otherwise this could be called right after prepare() but then you don't have access to tauri APIs + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + tauri_plugin_deep_link::register( + DEEP_LINK_SCHEME, + move |request| { + dbg!(&request); + handle.emit_all(OPEN_DEEP_LINK, request).unwrap(); + }, + ) + .unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */); + }); + Ok(()) }) .run(tauri::generate_context!()) diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index 293da0ec70..11dd7c206c 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "AppFlowy", - "version": "0.0.0" + "version": "0.0.1" }, "tauri": { "allowlist": { @@ -95,7 +95,18 @@ "title": "AppFlowy", "width": 1200, "minWidth": 800, - "minHeight": 600 + "minHeight": 600, + "visible": false, + "label": "main" + }, + { + "height": 300, + "width": 549, + "decorations": false, + "url": "launch_splash.jpg", + "label": "splashscreen", + "center": true, + "visible": true } ] } diff --git a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts index 48c8194d27..9c46b8ab38 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts @@ -1,6 +1,6 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { useEffect, useMemo } from 'react'; -import { currentUserActions } from '$app_reducers/current-user/slice'; +import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; import { Theme as ThemeType, ThemeMode } from '$app/stores/reducers/current-user/slice'; import { createTheme } from '@mui/material/styles'; import { getDesignTokens } from '$app/utils/mui'; @@ -10,6 +10,8 @@ import { UserService } from '$app/application/user/user.service'; export function useUserSetting() { const dispatch = useAppDispatch(); const { i18n } = useTranslation(); + const loginState = useAppSelector((state) => state.currentUser.loginState); + const { themeMode = ThemeMode.System, theme: themeType = ThemeType.Default } = useAppSelector((state) => { return { themeMode: state.currentUser.userSetting.themeMode, @@ -22,6 +24,7 @@ export function useUserSetting() { (themeMode === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches); useEffect(() => { + if (loginState !== LoginState.Success && loginState !== undefined) return; void (async () => { const settings = await UserService.getAppearanceSetting(); @@ -29,7 +32,7 @@ export function useUserSetting() { dispatch(currentUserActions.setUserSetting(settings)); await i18n.changeLanguage(settings.language); })(); - }, [dispatch, i18n]); + }, [dispatch, i18n, loginState]); useEffect(() => { const html = document.documentElement; diff --git a/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx b/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx index b9fd53130a..76bdb167b0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/AppMain.tsx @@ -8,6 +8,7 @@ import { useUserSetting } from '$app/AppMain.hooks'; import TrashPage from '$app/views/TrashPage'; import DocumentPage from '$app/views/DocumentPage'; import { Toaster } from 'react-hot-toast'; +import AppFlowyDevTool from '$app/components/_shared/devtool/AppFlowyDevTool'; function AppMain() { const { muiTheme } = useUserSetting(); @@ -22,6 +23,7 @@ function AppMain() { + {process.env.NODE_ENV === 'development' && } ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts index 5d9c9f9be0..72526b577f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_data.ts @@ -3,6 +3,7 @@ import { ChecklistFilterConditionPB, FieldType, NumberFilterConditionPB, + SelectOptionFilterConditionPB, TextFilterConditionPB, } from '@/services/backend'; import { UndeterminedFilter } from '$app/application/database'; @@ -12,7 +13,7 @@ export function getDefaultFilter(fieldType: FieldType): UndeterminedFilter['data case FieldType.RichText: case FieldType.URL: return { - condition: TextFilterConditionPB.Contains, + condition: TextFilterConditionPB.TextContains, content: '', }; case FieldType.Number: @@ -27,6 +28,14 @@ export function getDefaultFilter(fieldType: FieldType): UndeterminedFilter['data return { condition: ChecklistFilterConditionPB.IsIncomplete, }; + case FieldType.SingleSelect: + return { + condition: SelectOptionFilterConditionPB.OptionIs, + }; + case FieldType.MultiSelect: + return { + condition: SelectOptionFilterConditionPB.OptionContains, + }; default: return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts index 33cea9c45a..323f8dac82 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_listeners.ts @@ -1,35 +1,8 @@ import { Database, pbToFilter } from '$app/application/database'; import { FilterChangesetNotificationPB } from '@/services/backend'; -const deleteFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => { - const deleteIds = changeset.delete_filters.map((pb) => pb.id); - - if (deleteIds.length) { - database.filters = database.filters.filter((item) => !deleteIds.includes(item.id)); - } -}; - -const insertFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => { - changeset.insert_filters.forEach((pb) => { - database.filters.push(pbToFilter(pb)); - }); -}; - -const updateFiltersFromChange = (database: Database, changeset: FilterChangesetNotificationPB) => { - changeset.update_filters.forEach((pb) => { - const found = database.filters.find((item) => item.id === pb.filter_id); - - if (found) { - const newFilter = pbToFilter(pb.filter); - - Object.assign(found, newFilter); - database.filters = [...database.filters]; - } - }); -}; - export const didUpdateFilter = (database: Database, changeset: FilterChangesetNotificationPB) => { - deleteFiltersFromChange(database, changeset); - insertFiltersFromChange(database, changeset); - updateFiltersFromChange(database, changeset); + const filters = changeset.filters.items.map((pb) => pbToFilter(pb)); + + database.filters = filters; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts index 97fcb6e505..6283763d28 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_service.ts @@ -21,22 +21,20 @@ export async function insertFilter({ fieldId, fieldType, data, - filterId, }: { viewId: string; fieldId: string; fieldType: FieldType; data?: UndeterminedFilter['data']; - filterId?: string; }): Promise { const payload = DatabaseSettingChangesetPB.fromObject({ view_id: viewId, - update_filter: { - view_id: viewId, - field_id: fieldId, - field_type: fieldType, - filter_id: filterId, - data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined, + insert_filter: { + data: { + field_id: fieldId, + field_type: fieldType, + data: data ? filterDataToPB(data, fieldType)?.serialize() : undefined, + }, }, }); @@ -52,12 +50,13 @@ export async function insertFilter({ export async function updateFilter(viewId: string, filter: UndeterminedFilter): Promise { const payload = DatabaseSettingChangesetPB.fromObject({ view_id: viewId, - update_filter: { - view_id: viewId, + update_filter_data: { filter_id: filter.id, - field_id: filter.fieldId, - field_type: filter.fieldType, - data: filterDataToPB(filter.data, filter.fieldType)?.serialize(), + data: { + field_id: filter.fieldId, + field_type: filter.fieldType, + data: filterDataToPB(filter.data, filter.fieldType)?.serialize(), + }, }, }); @@ -74,10 +73,8 @@ export async function deleteFilter(viewId: string, filter: Omit) const payload = DatabaseSettingChangesetPB.fromObject({ view_id: viewId, delete_filter: { - view_id: viewId, filter_id: filter.id, field_id: filter.fieldId, - field_type: filter.fieldType, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts index 9e6f9f87ce..f9f80985e5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/filter/filter_types.ts @@ -5,7 +5,7 @@ import { FilterPB, NumberFilterConditionPB, NumberFilterPB, - SelectOptionConditionPB, + SelectOptionFilterConditionPB, SelectOptionFilterPB, TextFilterConditionPB, TextFilterPB, @@ -66,7 +66,7 @@ export interface ChecklistFilterData { } export interface SelectFilterData { - condition?: SelectOptionConditionPB; + condition?: SelectOptionFilterConditionPB; optionIds?: string[]; } @@ -195,8 +195,8 @@ export function bytesToFilterData(bytes: Uint8Array, fieldType: FieldType) { export function pbToFilter(pb: FilterPB): Filter { return { id: pb.id, - fieldId: pb.field_id, - fieldType: pb.field_type, - data: bytesToFilterData(pb.data, pb.field_type), + fieldId: pb.data.field_id, + fieldType: pb.data.field_type, + data: bytesToFilterData(pb.data.data, pb.data.field_type), }; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts index ad0c8af542..b75ecc0bd4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/group/group_types.ts @@ -8,7 +8,6 @@ export interface GroupSetting { export interface Group { id: string; - name: string; isDefault: boolean; isVisible: boolean; fieldId: string; @@ -18,7 +17,6 @@ export interface Group { export function pbToGroup(pb: GroupPB): Group { return { id: pb.group_id, - name: pb.group_name, isDefault: pb.is_default, isVisible: pb.is_visible, fieldId: pb.field_id, diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts index 0e06199b89..029da3b0c9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/row/row_service.ts @@ -30,7 +30,7 @@ export async function createRow(viewId: string, params?: { object_id: params?.rowId, }, group_id: params?.groupId, - data: params?.data ? { cell_data_by_field_id: params.data } : undefined, + data: params?.data, }); const result = await DatabaseEventCreateRow(payload); diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts index 3b85063604..0db128ec7a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.service.ts @@ -262,9 +262,11 @@ function flattenBlockJson(block: BlockJSON) { slateNode.children = block.children.map((child) => traverse(child)); if (textNode) { - if (!LIST_TYPES.includes(block.type as EditorNodeType) && slateNode.type !== EditorNodeType.Page) { + const texts = CustomEditor.getNodeTextContent(textNode); + + if (texts && !LIST_TYPES.includes(block.type as EditorNodeType) && slateNode.type !== EditorNodeType.Page) { slateNode.children.unshift(textNode); - } else { + } else if (texts) { slateNode.children.unshift({ type: EditorNodeType.Paragraph, children: [textNode], diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts index bdc4b23600..e6eb1d6923 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/document/document.types.ts @@ -74,6 +74,9 @@ export interface QuoteNode extends Element { export interface NumberedListNode extends Element { type: EditorNodeType.NumberedListBlock; blockId: string; + data: { + number?: number; + } & BlockData; } export interface BulletedListNode extends Element { diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts index 25aa0033a4..7d988b9866 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/page.service.ts @@ -19,6 +19,7 @@ import { FolderEventMoveNestedView, FolderEventUpdateView, FolderEventUpdateViewIcon, + FolderEventSetLatestView, } from '@/services/backend/events/flowy-folder'; export async function getPage(id: string) { @@ -149,3 +150,17 @@ export const updatePageIcon = async (viewId: string, icon?: PageIcon) => { return Promise.reject(result.err); }; + +export async function setLatestOpenedPage(id: string) { + const payload = new ViewIdPB({ + value: id, + }); + + const res = await FolderEventSetLatestView(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts index 0a1ac683af..fe066b7377 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/folder/workspace.service.ts @@ -1,5 +1,12 @@ -import { CreateViewPayloadPB, UserWorkspaceIdPB, WorkspaceIdPB } from '@/services/backend'; -import { UserEventOpenWorkspace } from '@/services/backend/events/flowy-user'; +import { parserViewPBToPage } from '$app_reducers/pages/slice'; +import { + ChangeWorkspaceIconPB, + CreateViewPayloadPB, + GetWorkspaceViewPB, + RenameWorkspacePB, + UserWorkspaceIdPB, + WorkspaceIdPB, +} from '@/services/backend'; import { FolderEventCreateView, FolderEventDeleteWorkspace, @@ -7,7 +14,12 @@ import { FolderEventReadCurrentWorkspace, FolderEventReadWorkspaceViews, } from '@/services/backend/events/flowy-folder'; -import { parserViewPBToPage } from '$app_reducers/pages/slice'; +import { + UserEventChangeWorkspaceIcon, + UserEventGetAllWorkspace, + UserEventOpenWorkspace, + UserEventRenameWorkspace, +} from '@/services/backend/events/flowy-user'; export async function openWorkspace(id: string) { const payload = new UserWorkspaceIdPB({ @@ -38,7 +50,7 @@ export async function deleteWorkspace(id: string) { } export async function getWorkspaceChildViews(id: string) { - const payload = new WorkspaceIdPB({ + const payload = new GetWorkspaceViewPB({ value: id, }); @@ -52,17 +64,13 @@ export async function getWorkspaceChildViews(id: string) { } export async function getWorkspaces() { - const result = await FolderEventReadCurrentWorkspace(); + const result = await UserEventGetAllWorkspace(); if (result.ok) { - const item = result.val; - - return [ - { - id: item.id, - name: item.name, - }, - ]; + return result.val.items.map((workspace) => ({ + id: workspace.workspace_id, + name: workspace.name, + })); } return []; @@ -82,12 +90,7 @@ export async function getCurrentWorkspace() { const result = await FolderEventReadCurrentWorkspace(); if (result.ok) { - const workspace = result.val; - - return { - id: workspace.id, - name: workspace.name, - }; + return result.val.id; } return null; @@ -101,9 +104,37 @@ export async function createCurrentWorkspaceChildView( const result = await FolderEventCreateView(payload); if (result.ok) { - const view = result.val; - - return view; + return result.val; + } + + return Promise.reject(result.err); +} + +export async function renameWorkspace(id: string, name: string) { + const payload = new RenameWorkspacePB({ + workspace_id: id, + new_name: name, + }); + + const result = await UserEventRenameWorkspace(payload); + + if (result.ok) { + return result.val; + } + + return Promise.reject(result.err); +} + +export async function changeWorkspaceIcon(id: string, icon: string) { + const payload = new ChangeWorkspaceIconPB({ + workspace_id: id, + new_icon: icon, + }); + + const result = await UserEventChangeWorkspaceIcon(payload); + + if (result.ok) { + return result.val; } return Promise.reject(result.err); diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts b/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts index 91bb15f069..c63a5d9823 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/notification.ts @@ -22,6 +22,7 @@ import { ViewPB, RepeatedTrashPB, ChildViewUpdatePB, + WorkspacePB, } from '@/services/backend'; import { AsyncQueue } from '$app/utils/async_queue'; @@ -40,11 +41,12 @@ const Notification = { [DatabaseNotification.DidUpdateFieldSettings]: FieldSettingsPB, [DatabaseNotification.DidUpdateFilter]: FilterChangesetNotificationPB, [DocumentNotification.DidReceiveUpdate]: DocEventPB, - [UserNotification.DidUpdateUserProfile]: UserProfilePB, + [FolderNotification.DidUpdateWorkspace]: WorkspacePB, [FolderNotification.DidUpdateWorkspaceViews]: RepeatedViewPB, [FolderNotification.DidUpdateView]: ViewPB, [FolderNotification.DidUpdateChildViews]: ChildViewUpdatePB, [FolderNotification.DidUpdateTrash]: RepeatedTrashPB, + [UserNotification.DidUpdateUserProfile]: UserProfilePB, }; type NotificationMap = typeof Notification; @@ -106,7 +108,7 @@ export function subscribeNotifications( callbacks: { [K in NotificationEnum]?: NotificationHandler; }, - options?: { id?: string } + options?: { id?: string | number } ): Promise<() => void> { const handler = async (subject: SubscribeObject) => { const { id, ty } = subject; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts index 82c0a6779b..ec258abc87 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/user/auth.service.ts @@ -1,33 +1,63 @@ -import { SignInPayloadPB, SignUpPayloadPB } from '@/services/backend'; import { - UserEventSignInWithEmailPassword, + SignUpPayloadPB, + OauthProviderPB, + ProviderTypePB, + OauthSignInPB, + AuthenticatorPB, + SignInPayloadPB, +} from '@/services/backend'; +import { UserEventSignOut, UserEventSignUp, + UserEventGetOauthURLWithProvider, + UserEventOauthSignIn, + UserEventSignInWithEmailPassword, } from '@/services/backend/events/flowy-user'; -import { nanoid } from '@reduxjs/toolkit'; import { Log } from '$app/utils/log'; export const AuthService = { - signIn: async (params: { email: string; password: string }) => { - const payload = SignInPayloadPB.fromObject({ email: params.email, password: params.password }); + getOAuthURL: async (provider: ProviderTypePB) => { + const providerDataRes = await UserEventGetOauthURLWithProvider( + OauthProviderPB.fromObject({ + provider, + }) + ); - const res = await UserEventSignInWithEmailPassword(payload); - - if (res.ok) { - return res.val; + if (!providerDataRes.ok) { + Log.error(providerDataRes.val.msg); + throw new Error(providerDataRes.val.msg); } - Log.error(res.val.msg); - throw new Error(res.val.msg); + const providerData = providerDataRes.val; + + return providerData.oauth_url; }, - signUp: async (params: { name: string; email: string; password: string }) => { - const deviceId = nanoid(8); + signInWithOAuth: async ({ uri, deviceId }: { uri: string; deviceId: string }) => { + const payload = OauthSignInPB.fromObject({ + authenticator: AuthenticatorPB.AppFlowyCloud, + map: { + sign_in_url: uri, + device_id: deviceId, + }, + }); + + const res = await UserEventOauthSignIn(payload); + + if (!res.ok) { + Log.error(res.val.msg); + throw new Error(res.val.msg); + } + + return res.val; + }, + + signUp: async (params: { deviceId: string; name: string; email: string; password: string }) => { const payload = SignUpPayloadPB.fromObject({ name: params.name, email: params.email, password: params.password, - device_id: deviceId, + device_id: params.deviceId, }); const res = await UserEventSignUp(payload); @@ -43,4 +73,20 @@ export const AuthService = { signOut: () => { return UserEventSignOut(); }, + + signIn: async (email: string, password: string) => { + const payload = SignInPayloadPB.fromObject({ + email, + password, + }); + + const res = await UserEventSignInWithEmailPassword(payload); + + if (!res.ok) { + Log.error(res.val.msg); + throw new Error(res.val.msg); + } + + return res.val; + }, }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts index f91c39cb71..ec64fb810c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/user/user.service.ts @@ -1,9 +1,10 @@ import { Theme, ThemeMode, UserSetting } from '$app_reducers/current-user/slice'; -import { AppearanceSettingsPB } from '@/services/backend'; +import { AppearanceSettingsPB, UpdateUserProfilePayloadPB } from '@/services/backend'; import { UserEventGetAppearanceSetting, UserEventGetUserProfile, UserEventSetAppearanceSetting, + UserEventUpdateUserProfile, } from '@/services/backend/events/flowy-user'; export const UserService = { @@ -52,4 +53,16 @@ export const UserService = { return; }, + + updateUserProfile: async (params: ReturnType) => { + const payload = UpdateUserProfilePayloadPB.fromObject(params); + + const res = await UserEventUpdateUserProfile(payload); + + if (res.ok) { + return res.val; + } + + return Promise.reject(res.err); + }, }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/dark-logo.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/dark-logo.svg new file mode 100644 index 0000000000..80d8c4132e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/dark-logo.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/information.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/information.svg new file mode 100644 index 0000000000..37ca4d5837 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/information.svg @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/light-logo.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/light-logo.svg new file mode 100644 index 0000000000..f5cd761ba7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/light-logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/logo.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/logo.svg new file mode 100644 index 0000000000..b1ac8d66fb --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg index 4e4a8c039a..05caec861a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/select-check.svg @@ -1,3 +1,3 @@ - + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/account.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/account.svg new file mode 100644 index 0000000000..fddfca7575 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/account.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/check_circle.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/check_circle.svg new file mode 100644 index 0000000000..c6fa56067b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/check_circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/dark.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/dark.png new file mode 100644 index 0000000000..15a2db5eb8 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/dark.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/discord.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/discord.png new file mode 100644 index 0000000000..f71e68c6ed Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/discord.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/github.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/github.png new file mode 100644 index 0000000000..597883b7a3 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/github.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/google.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/google.png new file mode 100644 index 0000000000..60032628a8 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/google.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/light.png b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/light.png new file mode 100644 index 0000000000..09b2d9c475 Binary files /dev/null and b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/light.png differ diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/settings/workplace.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/workplace.svg new file mode 100644 index 0000000000..2076ea3e2c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/settings/workplace.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/ProfileAvatar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/ProfileAvatar.tsx new file mode 100644 index 0000000000..1248882238 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/ProfileAvatar.tsx @@ -0,0 +1,33 @@ +import { stringToColor, stringToShortName } from '$app/utils/avatar'; +import { Avatar } from '@mui/material'; +import { useAppSelector } from '$app/stores/store'; + +export const ProfileAvatar = ({ + onClick, + className, + width, + height, +}: { + onClick?: (e: React.MouseEvent) => void; + width?: number; + height?: number; + className?: string; +}) => { + const { displayName = 'Me', iconUrl } = useAppSelector((state) => state.currentUser); + + return ( + + {iconUrl ? iconUrl : stringToShortName(displayName)} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/WorkplaceAvatar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/WorkplaceAvatar.tsx new file mode 100644 index 0000000000..079342b528 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/WorkplaceAvatar.tsx @@ -0,0 +1,34 @@ +import { Avatar } from '@mui/material'; +import { stringToColor, stringToShortName } from '$app/utils/avatar'; + +export const WorkplaceAvatar = ({ + workplaceName, + icon, + onClick, + width, + height, + className, +}: { + workplaceName: string; + width: number; + height: number; + className?: string; + icon?: string; + onClick?: (e: React.MouseEvent) => void; +}) => { + return ( + + {icon ? icon : stringToShortName(workplaceName)} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/index.ts new file mode 100644 index 0000000000..772056737a --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/avatar/index.ts @@ -0,0 +1,2 @@ +export * from './WorkplaceAvatar'; +export * from './ProfileAvatar'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/AppFlowyDevTool.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/AppFlowyDevTool.tsx new file mode 100644 index 0000000000..5d3ed1e3de --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/AppFlowyDevTool.tsx @@ -0,0 +1,61 @@ +import * as React from 'react'; +import SpeedDial from '@mui/material/SpeedDial'; +import SpeedDialIcon from '@mui/material/SpeedDialIcon'; +import SpeedDialAction from '@mui/material/SpeedDialAction'; +import { useMemo } from 'react'; +import { CloseOutlined, BuildOutlined, LoginOutlined, VisibilityOff } from '@mui/icons-material'; +import ManualSignInDialog from '$app/components/_shared/devtool/ManualSignInDialog'; +import { Portal } from '@mui/material'; + +function AppFlowyDevTool() { + const [openManualSignIn, setOpenManualSignIn] = React.useState(false); + const [hidden, setHidden] = React.useState(false); + const actions = useMemo( + () => [ + { + icon: , + name: 'Manual SignIn', + onClick: () => { + setOpenManualSignIn(true); + }, + }, + { + icon: , + name: 'Hide Dev Tool', + onClick: () => { + setHidden(true); + }, + }, + ], + [] + ); + + return ( + + + + ); +} + +export default AppFlowyDevTool; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx new file mode 100644 index 0000000000..364b334a07 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/devtool/ManualSignInDialog.tsx @@ -0,0 +1,114 @@ +import React from 'react'; +import { CircularProgress, DialogActions, DialogProps, Tab, Tabs, TextareaAutosize } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import Button from '@mui/material/Button'; +import { useAuth } from '$app/components/auth/auth.hooks'; +import TextField from '@mui/material/TextField'; + +function ManualSignInDialog(props: DialogProps) { + const [uri, setUri] = React.useState(''); + const [loading, setLoading] = React.useState(false); + const { signInWithOAuth, signInWithEmailPassword } = useAuth(); + const [tab, setTab] = React.useState(0); + const [email, setEmail] = React.useState(''); + const [password, setPassword] = React.useState(''); + const [domain, setDomain] = React.useState(''); + const handleSignIn = async () => { + setLoading(true); + try { + if (tab === 1) { + if (!email || !password) return; + await signInWithEmailPassword(email, password, domain); + } else { + await signInWithOAuth(uri); + } + } finally { + setLoading(false); + } + + props?.onClose?.({}, 'backdropClick'); + }; + + return ( + { + if (e.key === 'Enter') { + e.preventDefault(); + void handleSignIn(); + } + }} + > + + { + setTab(value); + }} + > + + + + {tab === 1 ? ( +
+ setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + setDomain(e.target.value)} + /> +
+ ) : ( + { + setUri(e.target.value); + }} + /> + )} +
+ + + + +
+ ); +} + +export default ManualSignInDialog; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts index 7554c21bb0..0fc1b5e61e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/popover/Popover.hooks.ts @@ -202,7 +202,12 @@ const usePopoverAutoPosition = ({ newPosition.anchorPosition.top += anchorRect.height; } - if (newPosition.anchorOrigin.vertical === 'top' && newPosition.transformOrigin.vertical === 'bottom') { + if ( + isExceedViewportTop && + isExceedViewportBottom && + newPosition.anchorOrigin.vertical === 'top' && + newPosition.transformOrigin.vertical === 'bottom' + ) { newPosition.paperHeight = newPaperHeight - anchorRect.height; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/AFScroller.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/AFScroller.tsx new file mode 100644 index 0000000000..0527b6cc26 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/AFScroller.tsx @@ -0,0 +1,55 @@ +import { Scrollbars } from 'react-custom-scrollbars'; +import React from 'react'; + +export interface AFScrollerProps { + children: React.ReactNode; + overflowXHidden?: boolean; + overflowYHidden?: boolean; + className?: string; + style?: React.CSSProperties; +} +export const AFScroller = ({ style, children, overflowXHidden, overflowYHidden, className }: AFScrollerProps) => { + return ( +
} + renderThumbVertical={(props) =>
} + {...(overflowXHidden && { + renderTrackHorizontal: (props) => ( +
+ ), + })} + {...(overflowYHidden && { + renderTrackVertical: (props) => ( +
+ ), + })} + style={style} + renderView={(props) => ( +
+ )} + > + {children} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/index.ts new file mode 100644 index 0000000000..7a740a5bb0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/scroller/index.ts @@ -0,0 +1 @@ +export * from './AFScroller'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AddSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AddSvg.tsx deleted file mode 100644 index 495a0151c1..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AddSvg.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export default () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogo.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogo.tsx deleted file mode 100644 index 0625424c76..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogo.tsx +++ /dev/null @@ -1,42 +0,0 @@ -export const AppflowyLogo = () => { - return ( - - - - - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogoDark.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogoDark.tsx deleted file mode 100644 index f43ce1f495..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogoDark.tsx +++ /dev/null @@ -1,77 +0,0 @@ -export const AppflowyLogoDark = () => { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogoLight.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogoLight.tsx deleted file mode 100644 index 1c9b3dcbb2..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/AppflowyLogoLight.tsx +++ /dev/null @@ -1,53 +0,0 @@ -export const AppflowyLogoLight = () => ( - - - - - - - - - - - - - - - - - - - -); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowLeftSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowLeftSvg.tsx deleted file mode 100644 index 9c4d68be75..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowLeftSvg.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const ArrowLeftSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowRightSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowRightSvg.tsx deleted file mode 100644 index 8b9501c508..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ArrowRightSvg.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const ArrowRightSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/BoardSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/BoardSvg.tsx deleted file mode 100644 index 11c29fae58..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/BoardSvg.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export const BoardSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckboxSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckboxSvg.tsx deleted file mode 100644 index 862badd2a2..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckboxSvg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export const CheckboxSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ChecklistTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ChecklistTypeSvg.tsx deleted file mode 100644 index ea4f168737..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ChecklistTypeSvg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export const ChecklistTypeSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckmarkSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckmarkSvg.tsx deleted file mode 100644 index 7ab64e7e28..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CheckmarkSvg.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const CheckmarkSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ClockSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ClockSvg.tsx deleted file mode 100644 index b66f7bfe18..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ClockSvg.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export const ClockSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CloseSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CloseSvg.tsx deleted file mode 100644 index 50e76a68c5..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CloseSvg.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export const CloseSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CopySvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CopySvg.tsx deleted file mode 100644 index 9d4eb5bfca..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/CopySvg.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export const CopySvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DateTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DateTypeSvg.tsx deleted file mode 100644 index 7bc133b2af..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DateTypeSvg.tsx +++ /dev/null @@ -1,15 +0,0 @@ -export const DateTypeSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/Details2Svg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/Details2Svg.tsx deleted file mode 100644 index 47e8cd9a00..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/Details2Svg.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const Details2Svg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DocumentSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DocumentSvg.tsx deleted file mode 100644 index 52843553d4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DocumentSvg.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export const DocumentSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DragElementSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DragElementSvg.tsx deleted file mode 100644 index 9d58ccde0f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DragElementSvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const DragElementSvg = () => { - return ( - - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DragSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DragSvg.tsx deleted file mode 100644 index cce8d09199..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DragSvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const DragSvg = () => { - return ( - - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DropDownShowSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DropDownShowSvg.tsx deleted file mode 100644 index b3956a77d1..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/DropDownShowSvg.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const DropDownShowSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EarthSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EarthSvg.tsx deleted file mode 100644 index f2911a940c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EarthSvg.tsx +++ /dev/null @@ -1,21 +0,0 @@ -export const EarthSvg = () => { - return ( - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditSvg.tsx deleted file mode 100644 index 7174c1c373..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditSvg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export const EditSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorCheckSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorCheckSvg.tsx deleted file mode 100644 index c784aa0be6..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorCheckSvg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export const EditorCheckSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorUncheckSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorUncheckSvg.tsx deleted file mode 100644 index 3f62b51eac..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EditorUncheckSvg.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export const EditorUncheckSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx deleted file mode 100644 index d4d50668d3..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeClosedSvg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export const EyeClosedSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx deleted file mode 100644 index c775d233cc..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/EyeOpenSvg.tsx +++ /dev/null @@ -1,20 +0,0 @@ -export const EyeOpenSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/FilterSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/FilterSvg.tsx deleted file mode 100644 index d46600610a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/FilterSvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const FilterSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/FullView.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/FullView.tsx deleted file mode 100644 index aa79420d5a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/FullView.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const FullView = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GridSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GridSvg.tsx deleted file mode 100644 index 5fbf0d86d7..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GridSvg.tsx +++ /dev/null @@ -1,30 +0,0 @@ -export const GridSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GroupByFieldSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GroupByFieldSvg.tsx deleted file mode 100644 index 960e1bad2a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GroupByFieldSvg.tsx +++ /dev/null @@ -1,26 +0,0 @@ -export const GroupByFieldSvg = () => { - return ( - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GroupBySvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GroupBySvg.tsx deleted file mode 100644 index 7ac7e37303..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/GroupBySvg.tsx +++ /dev/null @@ -1,31 +0,0 @@ -export const GroupBySvg = () => { - return ( - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/HideMenuSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/HideMenuSvg.tsx deleted file mode 100644 index af69b2ab5c..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/HideMenuSvg.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const HideMenuSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ImageSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ImageSvg.tsx deleted file mode 100644 index 488c170656..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ImageSvg.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export const ImageSvg = () => { - return ( - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/InformationSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/InformationSvg.tsx deleted file mode 100644 index 8217fe0f82..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/InformationSvg.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const InformationSvg = () => { - return ( - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/LogoutSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/LogoutSvg.tsx deleted file mode 100644 index 86e69c08c1..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/LogoutSvg.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const LogoutSvg = () => { - return ( - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MoreSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MoreSvg.tsx deleted file mode 100644 index 20dd851302..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MoreSvg.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const MoreSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MultiSelectTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MultiSelectTypeSvg.tsx deleted file mode 100644 index ec9c56d868..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/MultiSelectTypeSvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const MultiSelectTypeSvg = () => { - return ( - - - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/NumberTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/NumberTypeSvg.tsx deleted file mode 100644 index b41a8704e4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/NumberTypeSvg.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const NumberTypeSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/PropertiesSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/PropertiesSvg.tsx deleted file mode 100644 index cef2527c72..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/PropertiesSvg.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export const PropertiesSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SearchSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SearchSvg.tsx deleted file mode 100644 index 28717c95a0..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SearchSvg.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export const SearchSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SettingsSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SettingsSvg.tsx deleted file mode 100644 index 84f449bc27..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SettingsSvg.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const SettingsSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShareSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShareSvg.tsx deleted file mode 100644 index 217a995f62..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShareSvg.tsx +++ /dev/null @@ -1,18 +0,0 @@ -export const ShareSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShowMenuSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShowMenuSvg.tsx deleted file mode 100644 index 736e9a8b50..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/ShowMenuSvg.tsx +++ /dev/null @@ -1,10 +0,0 @@ -export const ShowMenuSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SingleSelectTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SingleSelectTypeSvg.tsx deleted file mode 100644 index 82b847681d..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SingleSelectTypeSvg.tsx +++ /dev/null @@ -1,16 +0,0 @@ -export const SingleSelectTypeSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipLeftSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipLeftSvg.tsx deleted file mode 100644 index 4e84c77e06..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipLeftSvg.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export const SkipLeftSvg = () => { - return ( - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipRightSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipRightSvg.tsx deleted file mode 100644 index 6bcf2ebe7e..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SkipRightSvg.tsx +++ /dev/null @@ -1,9 +0,0 @@ -export const SkipRightSvg = () => { - return ( - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortAscSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortAscSvg.tsx deleted file mode 100644 index 7304ef7cbb..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortAscSvg.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export const SortAscSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortDescSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortDescSvg.tsx deleted file mode 100644 index c0d310b6de..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortDescSvg.tsx +++ /dev/null @@ -1,22 +0,0 @@ -export const SortDescSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortSvg.tsx deleted file mode 100644 index 7fb7d9564f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/SortSvg.tsx +++ /dev/null @@ -1,8 +0,0 @@ -export const SortSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/TextTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/TextTypeSvg.tsx deleted file mode 100644 index 5ace944b18..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/TextTypeSvg.tsx +++ /dev/null @@ -1,14 +0,0 @@ -export const TextTypeSvg = () => { - return ( - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/TrashSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/TrashSvg.tsx deleted file mode 100644 index cb445e4704..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/TrashSvg.tsx +++ /dev/null @@ -1,34 +0,0 @@ -export const TrashSvg = () => { - return ( - - - - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/UrlTypeSvg.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/UrlTypeSvg.tsx deleted file mode 100644 index 0c7a268ec3..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/svg/UrlTypeSvg.tsx +++ /dev/null @@ -1,13 +0,0 @@ -export const UrlTypeSvg = () => { - return ( - - - - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx index 2888c07231..95e44ae9c2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewBanner.tsx @@ -19,10 +19,10 @@ function ViewBanner({ onUpdateCover?: (cover?: PageCover) => void; }) { return ( -
+
{showCover && cover && } -
+
+
{showAddIcon && ( - )} {showAddCover && ( - )} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx index ff3923109e..2c69bb4d76 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/ViewTitleInput.tsx @@ -23,7 +23,7 @@ function ViewTitleInput({ value, onChange }: { value: string; onChange?: (value: autoFocus value={value} onInput={onTitleChange} - className={`min-h-[40px] resize-none text-4xl font-bold leading-[50px] caret-text-title`} + className={`min-h-[40px] resize-none text-5xl font-bold leading-[50px] caret-text-title`} /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx index 97615804fb..fbf8063f44 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/view_title/cover/ViewCoverActions.tsx @@ -10,30 +10,32 @@ function ViewCoverActions( const { t } = useTranslation(); return ( -
-
- - +
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/LoginButtonGroup.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/LoginButtonGroup.tsx new file mode 100644 index 0000000000..481b80a532 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/LoginButtonGroup.tsx @@ -0,0 +1,51 @@ +import Button from '@mui/material/Button'; +import GoogleIcon from '$app/assets/settings/google.png'; +import GithubIcon from '$app/assets/settings/github.png'; +import DiscordIcon from '$app/assets/settings/discord.png'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from '$app/components/auth/auth.hooks'; +import { ProviderTypePB } from '@/services/backend'; + +export const LoginButtonGroup = () => { + const { t } = useTranslation(); + + const { signIn } = useAuth(); + + return ( +
+ + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx index 341eff871e..523f0b5188 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/ProtectedRoutes.tsx @@ -2,37 +2,104 @@ import { Outlet } from 'react-router-dom'; import { useAuth } from './auth.hooks'; import Layout from '$app/components/layout/Layout'; import { useCallback, useEffect, useState } from 'react'; -import { GetStarted } from '$app/components/auth/get_started/GetStarted'; -import { AppflowyLogo } from '../_shared/svg/AppflowyLogo'; +import { Welcome } from '$app/components/auth/Welcome'; +import { isTauri } from '$app/utils/env'; +import { notify } from '$app/components/_shared/notify'; +import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; +import { CircularProgress, Portal } from '@mui/material'; +import { ReactComponent as Logo } from '$app/assets/logo.svg'; +import { useAppDispatch } from '$app/stores/store'; export const ProtectedRoutes = () => { - const { currentUser, checkUser } = useAuth(); - const [isLoading, setIsLoading] = useState(true); + const { currentUser, checkUser, subscribeToUser, signInWithOAuth } = useAuth(); + const dispatch = useAppDispatch(); + + const isLoading = currentUser?.loginState === LoginState.Loading; + + const [checked, setChecked] = useState(false); const checkUserStatus = useCallback(async () => { await checkUser(); - setIsLoading(false); + setChecked(true); }, [checkUser]); useEffect(() => { void checkUserStatus(); }, [checkUserStatus]); - if (isLoading) { - // It's better to make a fading effect to disappear the loading page - return ; - } else { - return ; - } + useEffect(() => { + if (currentUser.isAuthenticated) { + return subscribeToUser(); + } + }, [currentUser.isAuthenticated, subscribeToUser]); + + const onDeepLink = useCallback(async () => { + if (!isTauri()) return; + const { event } = await import('@tauri-apps/api'); + + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + return await event.listen('open_deep_link', async (e) => { + const payload = e.payload as string; + + const [, hash] = payload.split('//#'); + const obj = parseHash(hash); + + if (!obj.access_token) { + notify.error('Failed to sign in, the access token is missing'); + dispatch(currentUserActions.setLoginState(LoginState.Error)); + return; + } + + try { + await signInWithOAuth(payload); + } catch (e) { + notify.error('Failed to sign in, please try again'); + } + }); + }, [dispatch, signInWithOAuth]); + + useEffect(() => { + void onDeepLink(); + }, [onDeepLink]); + + return ( +
+ {checked ? ( + + ) : ( +
+ +
+ )} + + {isLoading && } +
+ ); }; const StartLoading = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + const preventDefault = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + dispatch(currentUserActions.resetLoginState()); + } + }; + + document.addEventListener('keydown', preventDefault, true); + + return () => { + document.removeEventListener('keydown', preventDefault, true); + }; + }, [dispatch]); return ( -
-
- + +
+
-
+ ); }; @@ -44,6 +111,17 @@ const SplashScreen = ({ isAuthenticated }: { isAuthenticated: boolean }) => { ); } else { - return ; + return ; } }; + +function parseHash(hash: string) { + const hashParams = new URLSearchParams(hash); + const hashObject: Record = {}; + + for (const [key, value] of hashParams) { + hashObject[key] = value; + } + + return hashObject; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/Welcome.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Welcome.tsx new file mode 100644 index 0000000000..eadcf08c21 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/Welcome.tsx @@ -0,0 +1,55 @@ +import { ReactComponent as AppflowyLogo } from '$app/assets/logo.svg'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup'; +import { useAuth } from '$app/components/auth/auth.hooks'; +import { Log } from '$app/utils/log'; + +export const Welcome = () => { + const { signInAsAnonymous } = useAuth(); + const { t } = useTranslation(); + + return ( + <> +
e.preventDefault()} method='POST'> +
+
+ +
+ +
+ + {t('welcomeTo')} {t('appName')} + +
+ +
+ +
+
+ {t('signIn.or')} +
+
+
+ +
+
+
+ + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts index b9342fb210..89b7388e64 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/auth/auth.hooks.ts @@ -1,78 +1,80 @@ -import { currentUserActions } from '$app_reducers/current-user/slice'; -import { UserProfilePB } from '@/services/backend/events/flowy-user'; +import { currentUserActions, LoginState, parseWorkspaceSettingPBToSetting } from '$app_reducers/current-user/slice'; +import { AuthenticatorPB, ProviderTypePB, UserNotification, UserProfilePB } from '@/services/backend/events/flowy-user'; import { UserService } from '$app/application/user/user.service'; import { AuthService } from '$app/application/user/auth.service'; -import { useAppSelector, useAppDispatch } from '$app/stores/store'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { getCurrentWorkspaceSetting } from '$app/application/folder/workspace.service'; import { useCallback } from 'react'; +import { subscribeNotifications } from '$app/application/notification'; +import { nanoid } from 'nanoid'; +import { open } from '@tauri-apps/api/shell'; export const useAuth = () => { const dispatch = useAppDispatch(); const currentUser = useAppSelector((state) => state.currentUser); - const checkUser = useCallback(async () => { - const userProfile = await UserService.getUserProfile(); - - if (!userProfile) return; - const workspaceSetting = await getCurrentWorkspaceSetting(); - - dispatch( - currentUserActions.checkUser({ - id: userProfile.id, - token: userProfile.token, - email: userProfile.email, - displayName: userProfile.name, - isAuthenticated: true, - workspaceSetting: workspaceSetting, - }) - ); - - return userProfile; - }, [dispatch]); - - const register = useCallback( - async (email: string, password: string, name: string): Promise => { - const userProfile = await AuthService.signUp({ email, password, name }); - - // Get the workspace setting after user registered. The workspace setting - // contains the latest visiting page and the current workspace data. - const workspaceSetting = await getCurrentWorkspaceSetting(); - - if (workspaceSetting) { + // Subscribe to user update events + const subscribeToUser = useCallback(() => { + const unsubscribePromise = subscribeNotifications({ + [UserNotification.DidUpdateUserProfile]: async (changeset) => { dispatch( currentUserActions.updateUser({ - id: userProfile.id, - token: userProfile.token, - email: userProfile.email, - displayName: userProfile.name, - isAuthenticated: true, - workspaceSetting: workspaceSetting, + email: changeset.email, + displayName: changeset.name, + iconUrl: changeset.icon_url, }) ); - } + }, + }); - return userProfile; + return () => { + void unsubscribePromise.then((fn) => fn()); + }; + }, [dispatch]); + + const setUser = useCallback( + async (userProfile?: Partial) => { + if (!userProfile) return; + + const workspaceSetting = await getCurrentWorkspaceSetting(); + + const isLocal = userProfile.authenticator === AuthenticatorPB.Local; + + dispatch( + currentUserActions.updateUser({ + id: userProfile.id, + token: userProfile.token, + email: userProfile.email, + displayName: userProfile.name, + iconUrl: userProfile.icon_url, + isAuthenticated: true, + workspaceSetting: workspaceSetting ? parseWorkspaceSettingPBToSetting(workspaceSetting) : undefined, + isLocal, + }) + ); }, [dispatch] ); - const login = useCallback( - async (email: string, password: string): Promise => { - const user = await AuthService.signIn({ email, password }); - const { id, token, name } = user; + // Check if the user is authenticated + const checkUser = useCallback(async () => { + const userProfile = await UserService.getUserProfile(); - dispatch( - currentUserActions.updateUser({ - id: id, - token: token, - email, - displayName: name, - isAuthenticated: true, - }) - ); - return user; + await setUser(userProfile); + + return userProfile; + }, [setUser]); + + const register = useCallback( + async (email: string, password: string, name: string): Promise => { + const deviceId = currentUser?.deviceId ?? nanoid(8); + const userProfile = await AuthService.signUp({ deviceId, email, password, name }); + + await setUser(userProfile); + + return userProfile; }, - [dispatch] + [setUser, currentUser?.deviceId] ); const logout = useCallback(async () => { @@ -80,5 +82,105 @@ export const useAuth = () => { dispatch(currentUserActions.logout()); }, [dispatch]); - return { currentUser, checkUser, register, login, logout }; + const signInAsAnonymous = useCallback(async () => { + const fakeEmail = nanoid(8) + '@appflowy.io'; + const fakePassword = 'AppFlowy123@'; + const fakeName = 'Me'; + + await register(fakeEmail, fakePassword, fakeName); + }, [register]); + + const signIn = useCallback( + async (provider: ProviderTypePB) => { + dispatch(currentUserActions.setLoginState(LoginState.Loading)); + try { + const url = await AuthService.getOAuthURL(provider); + + await open(url); + } catch { + dispatch(currentUserActions.setLoginState(LoginState.Error)); + } + }, + [dispatch] + ); + + const signInWithOAuth = useCallback( + async (uri: string) => { + dispatch(currentUserActions.setLoginState(LoginState.Loading)); + try { + const deviceId = currentUser?.deviceId ?? nanoid(8); + + await AuthService.signInWithOAuth({ uri, deviceId }); + const userProfile = await UserService.getUserProfile(); + + await setUser(userProfile); + + return userProfile; + } catch (e) { + dispatch(currentUserActions.setLoginState(LoginState.Error)); + return Promise.reject(e); + } + }, + [dispatch, currentUser?.deviceId, setUser] + ); + + // Only for development purposes + const signInWithEmailPassword = useCallback( + async (email: string, password: string, domain?: string) => { + dispatch(currentUserActions.setLoginState(LoginState.Loading)); + + try { + const response = await fetch( + `https://${domain ? domain : 'test.appflowy.cloud'}/gotrue/token?grant_type=password`, + { + method: 'POST', + mode: 'cors', + cache: 'no-cache', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json', + }, + redirect: 'follow', + referrerPolicy: 'no-referrer', + body: JSON.stringify({ + email, + password, + }), + } + ); + + const data = await response.json(); + + let uri = `appflowy-flutter://#`; + const params: string[] = []; + + Object.keys(data).forEach((key) => { + if (typeof data[key] === 'object') { + return; + } + + params.push(`${key}=${data[key]}`); + }); + uri += params.join('&'); + + return signInWithOAuth(uri); + } catch (e) { + dispatch(currentUserActions.setLoginState(LoginState.Error)); + return Promise.reject(e); + } + }, + [dispatch, signInWithOAuth] + ); + + return { + currentUser, + checkUser, + register, + logout, + subscribeToUser, + signInAsAnonymous, + signIn, + signInWithOAuth, + signInWithEmailPassword, + }; }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/get_started/GetStarted.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/auth/get_started/GetStarted.tsx deleted file mode 100644 index 6bb693768a..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/get_started/GetStarted.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo'; -import Button from '@mui/material/Button'; -import { useLogin } from '$app/components/auth/get_started/useLogin'; -import { useTranslation } from 'react-i18next'; - -export const GetStarted = () => { - const { onAutoSignInClick } = useLogin(); - const { t } = useTranslation(); - - return ( - <> -
e.preventDefault()} method='POST'> -
-
- -
- -
- - {t('signIn.loginTitle').replace('@:appName', 'AppFlowy')} - -
- -
- -
-
-
- - ); -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/auth/get_started/useLogin.ts b/frontend/appflowy_tauri/src/appflowy_app/components/auth/get_started/useLogin.ts deleted file mode 100644 index 15d607e812..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/auth/get_started/useLogin.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { useState } from 'react'; -import { currentUserActions } from '$app_reducers/current-user/slice'; -import { useAppDispatch } from '$app/stores/store'; -import { useNavigate } from 'react-router-dom'; -import { useAuth } from '../auth.hooks'; -import { nanoid } from 'nanoid'; - -export const useLogin = () => { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [showPassword, setShowPassword] = useState(false); - const appDispatch = useAppDispatch(); - const navigate = useNavigate(); - const { login, register } = useAuth(); - const [authError, setAuthError] = useState(false); - - function onTogglePassword() { - setShowPassword(!showPassword); - } - - // reset error - function _setEmail(v: string) { - setAuthError(false); - setEmail(v); - } - - function _setPassword(v: string) { - setAuthError(false); - setPassword(v); - } - - async function onAutoSignInClick() { - try { - const fakeEmail = nanoid(8) + '@appflowy.io'; - const fakePassword = 'AppFlowy123@'; - const userProfile = await register(fakeEmail, fakePassword, 'Me'); - const { id, name, token } = userProfile; - - appDispatch( - currentUserActions.updateUser({ - id: id, - displayName: name, - email: email, - token: token, - isAuthenticated: true, - }) - ); - navigate('/'); - } catch (e) { - setAuthError(true); - } - } - - async function onSignInClick() { - try { - const userProfile = await login(email, password); - const { id, name, token } = userProfile; - - appDispatch( - currentUserActions.updateUser({ - id: id, - displayName: name, - email: email, - token: token, - isAuthenticated: true, - }) - ); - navigate('/'); - } catch (e) { - setAuthError(true); - } - } - - return { - showPassword, - onTogglePassword, - onSignInClick, - onAutoSignInClick, - email, - setEmail: _setEmail, - password, - setPassword: _setPassword, - authError, - }; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx index 793e14c2e0..cd94947d8d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/DatabaseTitle.tsx @@ -22,7 +22,7 @@ export const DatabaseTitle = () => { return (
0; - const [filterAnchorEl, setFilterAnchorEl] = useState(null); const open = Boolean(filterAnchorEl); @@ -31,6 +30,10 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen open={open} anchorEl={filterAnchorEl} onClose={() => setFilterAnchorEl(null)} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} anchorOrigin={{ vertical: 'bottom', horizontal: 'right', diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx index 09a0f48129..7f978120df 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/database_settings/SortSettings.tsx @@ -39,6 +39,10 @@ function SortSettings({ onToggleCollection }: Props) { open={open} anchorEl={sortAnchorEl} onClose={handleClose} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} anchorOrigin={{ vertical: 'bottom', horizontal: 'right', diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx index b14a3a9783..13f29a7dfc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/EditRecord.tsx @@ -60,4 +60,4 @@ function EditRecord({ rowId }: Props) { ); } -export default React.memo(EditRecord); +export default EditRecord; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx index cf3b1878dd..7056cd353d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/edit_record/ExpandRecordModal.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { DialogProps, IconButton, Portal } from '@mui/material'; -import DialogContent from '@mui/material/DialogContent'; import Dialog from '@mui/material/Dialog'; import { ReactComponent as DetailsIcon } from '$app/assets/details.svg'; import RecordActions from '$app/components/database/components/edit_record/RecordActions'; import EditRecord from '$app/components/database/components/edit_record/EditRecord'; +import { AFScroller } from '$app/components/_shared/scroller'; interface Props extends DialogProps { rowId: string; @@ -25,9 +25,9 @@ function ExpandRecordModal({ open, onClose, rowId }: Props) { className: 'h-[calc(100%-144px)] w-[80%] max-w-[960px] overflow-visible', }} > - + - + ; } -export default React.memo(RecordDocument); +export default RecordDocument; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx index e95ac20c8f..89a9ad1756 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/date/TimeFormat.tsx @@ -22,7 +22,7 @@ function TimeFormat({ value, onChange }: Props) { return (
{title}
- {value === option && } + {value === option && }
); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx index 13571244b3..0f9be6a21a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/number/NumberFormatMenu.tsx @@ -29,7 +29,7 @@ function NumberFormatMenu({ return ( <> {formatText(format)} - {value === format && } + {value === format && } ); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx index 63b5872c2e..6c6cf37aae 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/SelectOptionModifyMenu.tsx @@ -156,7 +156,7 @@ export const SelectOptionModifyMenu: FC = ({ fieldId, opt > {t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)} - {option.color === color && } + {option.color === color && } ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx index 27461932b3..2a855a4085 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem.tsx @@ -32,7 +32,7 @@ export const SelectOptionItem: FC = ({ isSelected, fieldI
- {isSelected && !hovered && } + {isSelected && !hovered && } {hovered && ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx index aaff29287b..8b793942da 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/FilterConditionSelect.tsx @@ -6,7 +6,7 @@ import { DateFilterConditionPB, FieldType, NumberFilterConditionPB, - SelectOptionConditionPB, + SelectOptionFilterConditionPB, TextFilterConditionPB, } from '@/services/backend'; @@ -30,27 +30,27 @@ function FilterConditionSelect({ case FieldType.URL: return [ { - value: TextFilterConditionPB.Contains, + value: TextFilterConditionPB.TextContains, text: t('grid.textFilter.contains'), }, { - value: TextFilterConditionPB.DoesNotContain, + value: TextFilterConditionPB.TextDoesNotContain, text: t('grid.textFilter.doesNotContain'), }, { - value: TextFilterConditionPB.StartsWith, + value: TextFilterConditionPB.TextStartsWith, text: t('grid.textFilter.startWith'), }, { - value: TextFilterConditionPB.EndsWith, + value: TextFilterConditionPB.TextEndsWith, text: t('grid.textFilter.endsWith'), }, { - value: TextFilterConditionPB.Is, + value: TextFilterConditionPB.TextIs, text: t('grid.textFilter.is'), }, { - value: TextFilterConditionPB.IsNot, + value: TextFilterConditionPB.TextIsNot, text: t('grid.textFilter.isNot'), }, { @@ -63,26 +63,51 @@ function FilterConditionSelect({ }, ]; case FieldType.SingleSelect: + return [ + { + value: SelectOptionFilterConditionPB.OptionIs, + text: t('grid.selectOptionFilter.is'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), + }, + ]; case FieldType.MultiSelect: return [ { - value: SelectOptionConditionPB.OptionIs, - text: t('grid.singleSelectOptionFilter.is'), + value: SelectOptionFilterConditionPB.OptionIs, + text: t('grid.selectOptionFilter.is'), }, { - value: SelectOptionConditionPB.OptionIsNot, - text: t('grid.singleSelectOptionFilter.isNot'), + value: SelectOptionFilterConditionPB.OptionIsNot, + text: t('grid.selectOptionFilter.isNot'), }, { - value: SelectOptionConditionPB.OptionIsEmpty, - text: t('grid.singleSelectOptionFilter.isEmpty'), + value: SelectOptionFilterConditionPB.OptionContains, + text: t('grid.selectOptionFilter.contains'), }, { - value: SelectOptionConditionPB.OptionIsNotEmpty, - text: t('grid.singleSelectOptionFilter.isNotEmpty'), + value: SelectOptionFilterConditionPB.OptionDoesNotContain, + text: t('grid.selectOptionFilter.doesNotContain'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsEmpty, + text: t('grid.selectOptionFilter.isEmpty'), + }, + { + value: SelectOptionFilterConditionPB.OptionIsNotEmpty, + text: t('grid.selectOptionFilter.isNotEmpty'), }, ]; - case FieldType.Number: return [ { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx index 3bf0453256..bd1d1f239a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilter.tsx @@ -7,7 +7,7 @@ import { } from '$app/application/database'; import { Tag } from '$app/components/database/components/field_types/select/Tag'; import { ReactComponent as SelectCheckSvg } from '$app/assets/select-check.svg'; -import { SelectOptionConditionPB } from '@/services/backend'; +import { SelectOptionFilterConditionPB } from '@/services/backend'; import { useTypeOption } from '$app/components/database'; import KeyboardNavigation, { KeyboardNavigationOption, @@ -32,7 +32,7 @@ function SelectFilter({ onClose, filter, field, onChange }: Props) { content: (
- {filter.data.optionIds?.includes(option.id) && } + {filter.data.optionIds?.includes(option.id) && }
), }; @@ -42,8 +42,8 @@ function SelectFilter({ onClose, filter, field, onChange }: Props) { const showOptions = options.length > 0 && - condition !== SelectOptionConditionPB.OptionIsEmpty && - condition !== SelectOptionConditionPB.OptionIsNotEmpty; + condition !== SelectOptionFilterConditionPB.OptionIsEmpty && + condition !== SelectOptionFilterConditionPB.OptionIsNotEmpty; const handleChange = ({ condition, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx index 4b6d29d79d..72576deae1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/select_filter/SelectFilterValue.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { SelectFilterData, SelectTypeOption } from '$app/application/database'; import { useStaticTypeOption } from '$app/components/database'; import { useTranslation } from 'react-i18next'; -import { SelectOptionConditionPB } from '@/services/backend'; +import { SelectOptionFilterConditionPB } from '@/services/backend'; function SelectFilterValue({ data, fieldId }: { data: SelectFilterData; fieldId: string }) { const typeOption = useStaticTypeOption(fieldId); @@ -19,13 +19,13 @@ function SelectFilterValue({ data, fieldId }: { data: SelectFilterData; fieldId: .join(', '); switch (data.condition) { - case SelectOptionConditionPB.OptionIs: + case SelectOptionFilterConditionPB.OptionIs: return `: ${options}`; - case SelectOptionConditionPB.OptionIsNot: + case SelectOptionFilterConditionPB.OptionIsNot: return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${options}`; - case SelectOptionConditionPB.OptionIsEmpty: + case SelectOptionFilterConditionPB.OptionIsEmpty: return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; - case SelectOptionConditionPB.OptionIsNotEmpty: + case SelectOptionFilterConditionPB.OptionIsNotEmpty: return `: ${t('grid.textFilter.choicechipPrefix.isNotEmpty')}`; default: return ''; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx index 9377050bf1..5718a3e2b8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/filter/text_filter/TextFilterValue.tsx @@ -9,15 +9,15 @@ function TextFilterValue({ data }: { data: TextFilterData }) { const value = useMemo(() => { if (!data.content) return ''; switch (data.condition) { - case TextFilterConditionPB.Contains: - case TextFilterConditionPB.Is: + case TextFilterConditionPB.TextContains: + case TextFilterConditionPB.TextIs: return `: ${data.content}`; - case TextFilterConditionPB.DoesNotContain: - case TextFilterConditionPB.IsNot: + case TextFilterConditionPB.TextDoesNotContain: + case TextFilterConditionPB.TextIsNot: return `: ${t('grid.textFilter.choicechipPrefix.isNot')} ${data.content}`; - case TextFilterConditionPB.StartsWith: + case TextFilterConditionPB.TextStartsWith: return `: ${t('grid.textFilter.choicechipPrefix.startWith')} ${data.content}`; - case TextFilterConditionPB.EndsWith: + case TextFilterConditionPB.TextEndsWith: return `: ${t('grid.textFilter.choicechipPrefix.endWith')} ${data.content}`; case TextFilterConditionPB.TextIsEmpty: return `: ${t('grid.textFilter.choicechipPrefix.isEmpty')}`; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx index 890a4e2a33..e3021249ee 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/database/components/property/property_type/PropertyTypeMenu.tsx @@ -39,7 +39,7 @@ export const PropertyTypeMenu: FC< - {type === field.type && } + {type === field.type && } ); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts index 91e6ecd76e..557b91f936 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/index.ts @@ -11,9 +11,10 @@ import { Path, EditorBeforeOptions, Text, + addMark, } from 'slate'; import { LIST_TYPES, tabBackward, tabForward } from '$app/components/editor/command/tab'; -import { isMarkActive, removeMarks, toggleMark } from '$app/components/editor/command/mark'; +import { getAllMarks, isMarkActive, removeMarks, toggleMark } from '$app/components/editor/command/mark'; import { deleteFormula, insertFormula, @@ -31,6 +32,7 @@ import { inlineNodeTypes, FormulaNode, ImageNode, + EditorMarkFormat, } from '$app/application/document/document.types'; import cloneDeep from 'lodash-es/cloneDeep'; import { generateId } from '$app/components/editor/provider/utils/convert'; @@ -75,16 +77,66 @@ export const CustomEditor = { if (!afterPoint) return false; return CustomEditor.isInlineNode(editor, afterPoint); }, - blockEqual: (editor: ReactEditor, point: Point, anotherPoint: Point) => { - const match = CustomEditor.getBlock(editor, point); - const anotherMatch = CustomEditor.getBlock(editor, anotherPoint); - if (!match || !anotherMatch) return false; + /** + * judge if the selection is multiple block + * @param editor + * @param filterEmptyEndSelection if the filterEmptyEndSelection is true, the function will filter the empty end selection + */ + isMultipleBlockSelected: (editor: ReactEditor, filterEmptyEndSelection?: boolean): boolean => { + const { selection } = editor; - const [node] = match; - const [anotherNode] = anotherMatch; + if (!selection) return false; - return node === anotherNode; + if (Range.isCollapsed(selection)) return false; + const start = Range.start(selection); + const end = Range.end(selection); + const isBackward = Range.isBackward(selection); + const startBlock = CustomEditor.getBlock(editor, start); + const endBlock = CustomEditor.getBlock(editor, end); + + if (!startBlock || !endBlock) return false; + + const [, startPath] = startBlock; + const [, endPath] = endBlock; + + const isSomePath = Path.equals(startPath, endPath); + + // if the start and end path is the same, return false + if (isSomePath) { + return false; + } + + if (!filterEmptyEndSelection) { + return true; + } + + // The end point is at the start of the end block + const focusEndStart = Point.equals(end, editor.start(endPath)); + + if (!focusEndStart) { + return true; + } + + // find the previous block + const previous = editor.previous({ + at: endPath, + match: (n) => Element.isElement(n) && n.blockId !== undefined, + }); + + if (!previous) { + return true; + } + + // backward selection + const newEnd = editor.end(editor.range(previous[1])); + + editor.select({ + anchor: isBackward ? newEnd : start, + focus: isBackward ? start : newEnd, + }); + + return false; }, /** @@ -109,6 +161,10 @@ export const CustomEditor = { const cloneNode = CustomEditor.cloneBlock(editor, node); Object.assign(cloneNode, newProperties); + cloneNode.data = { + ...(node.data || {}), + ...(newProperties.data || {}), + }; const isEmbed = editor.isEmbed(cloneNode); @@ -181,6 +237,10 @@ export const CustomEditor = { }, toggleAlign(editor: ReactEditor, format: string) { + const isIncludeRoot = CustomEditor.selectionIncludeRoot(editor); + + if (isIncludeRoot) return; + const matchNodes = Array.from( Editor.nodes(editor, { // Note: we need to select the text node instead of the element node, otherwise the parent node will be selected @@ -273,18 +333,35 @@ export const CustomEditor = { }); }, - toggleTodo(editor: ReactEditor, node: TodoListNode) { - const checked = node.data.checked; - const path = ReactEditor.findPath(editor, node); - const data = node.data || {}; - const newProperties = { - data: { - ...data, - checked: !checked, - }, - } as Partial; + toggleTodo(editor: ReactEditor, at?: Location) { + const selection = at || editor.selection; - Transforms.setNodes(editor, newProperties, { at: path }); + if (!selection) return; + + const nodes = Array.from( + editor.nodes({ + at: selection, + match: (n) => Element.isElement(n) && n.type === EditorNodeType.TodoListBlock, + }) + ); + + const matchUnChecked = nodes.some(([node]) => { + return !(node as TodoListNode).data.checked; + }); + + const checked = Boolean(matchUnChecked); + + nodes.forEach(([node, path]) => { + const data = (node as TodoListNode).data || {}; + const newProperties = { + data: { + ...data, + checked: checked, + }, + } as Partial; + + Transforms.setNodes(editor, newProperties, { at: path }); + }); }, toggleToggleList(editor: ReactEditor, node: ToggleListNode) { @@ -575,4 +652,64 @@ export const CustomEditor = { isEmbedNode(node: Element): boolean { return EmbedTypes.includes(node.type); }, + + getListLevel(editor: ReactEditor, type: EditorNodeType, path: Path) { + let level = 0; + let currentPath = path; + + while (currentPath.length > 0) { + const parent = editor.parent(currentPath); + + if (!parent) { + break; + } + + const [parentNode, parentPath] = parent as NodeEntry; + + if (parentNode.type !== type) { + break; + } + + level += 1; + currentPath = parentPath; + } + + return level; + }, + + getLinks(editor: ReactEditor): string[] { + const marks = getAllMarks(editor); + + if (!marks) return []; + + return Object.entries(marks) + .filter(([key]) => key === 'href') + .map(([_, val]) => val as string); + }, + + extendLineBackward(editor: ReactEditor) { + Transforms.move(editor, { + unit: 'line', + edge: 'focus', + reverse: true, + }); + }, + + extendLineForward(editor: ReactEditor) { + Transforms.move(editor, { unit: 'line', edge: 'focus' }); + }, + + insertPlainText(editor: ReactEditor, text: string) { + const [appendText, ...lines] = text.split('\n'); + + editor.insertText(appendText); + lines.forEach((line) => { + editor.insertBreak(); + editor.insertText(line); + }); + }, + + highlight(editor: ReactEditor) { + addMark(editor, EditorMarkFormat.BgColor, 'appflowy_them_color_tint5'); + }, }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts index 45f3362f53..649eaca564 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/command/mark.ts @@ -1,6 +1,7 @@ import { ReactEditor } from 'slate-react'; import { Editor, Text, Range, Element } from 'slate'; import { EditorInlineNodeType, EditorMarkFormat } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command/index'; export function toggleMark( editor: ReactEditor, @@ -9,6 +10,10 @@ export function toggleMark( value: string | boolean; } ) { + if (CustomEditor.selectionIncludeRoot(editor)) { + return; + } + const { key, value } = mark; const isActive = isMarkActive(editor, key); @@ -48,7 +53,7 @@ export function isMarkActive(editor: ReactEditor, format: EditorMarkFormat | Edi return marks ? !!marks[format] : false; } -function getSelectionTexts(editor: ReactEditor) { +export function getSelectionTexts(editor: ReactEditor) { const selection = editor.selection; if (!selection) return []; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx index c10dde829a..91645e0051 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/_shared/PlaceholderContent.tsx @@ -24,7 +24,7 @@ function PlaceholderContent({ node, ...attributes }: { node: Element; className? }, [editor, node]); const className = useMemo(() => { - return `text-placeholder ${attributes.className ?? ''}`; + return `text-placeholder select-none ${attributes.className ?? ''}`; }, [attributes.className]); const unSelectedPlaceholder = useMemo(() => { @@ -38,15 +38,15 @@ function PlaceholderContent({ node, ...attributes }: { node: Element; className? } case EditorNodeType.ToggleListBlock: - return t('document.plugins.toggleList'); + return t('blockPlaceholders.bulletList'); case EditorNodeType.QuoteBlock: - return t('editor.quote'); + return t('blockPlaceholders.quote'); case EditorNodeType.TodoListBlock: - return t('document.plugins.todoList'); + return t('blockPlaceholders.todoList'); case EditorNodeType.NumberedListBlock: - return t('document.plugins.numberedList'); + return t('blockPlaceholders.numberList'); case EditorNodeType.BulletedListBlock: - return t('document.plugins.bulletedList'); + return t('blockPlaceholders.bulletList'); case EditorNodeType.HeadingBlock: { const level = (block as HeadingNode).data.level; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx index 0e712b3269..ea0de80f55 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/bulleted_list/BulletedListIcon.tsx @@ -1,14 +1,49 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { BulletedListNode } from '$app/application/document/document.types'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { CustomEditor } from '$app/components/editor/command'; + +enum Letter { + Disc, + Circle, + Square, +} + +function BulletedListIcon({ block, className }: { block: BulletedListNode; className: string }) { + const staticEditor = useSlateStatic(); + const path = ReactEditor.findPath(staticEditor, block); + + const letter = useMemo(() => { + const level = CustomEditor.getListLevel(staticEditor, block.type, path); + + if (level % 3 === 0) { + return Letter.Disc; + } else if (level % 3 === 1) { + return Letter.Circle; + } else { + return Letter.Square; + } + }, [block.type, staticEditor, path]); + + const dataLetter = useMemo(() => { + switch (letter) { + case Letter.Disc: + return '•'; + case Letter.Circle: + return '◦'; + case Letter.Square: + return '▪'; + } + }, [letter]); -function BulletedListIcon({ block: _, className }: { block: BulletedListNode; className: string }) { return ( { e.preventDefault(); }} + data-letter={dataLetter} contentEditable={false} - className={`${className} bulleted-icon flex min-w-[23px] justify-center pr-1 font-medium`} + className={`${className} bulleted-icon flex min-w-[24px] justify-center pr-1 font-medium`} /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx index 0fc8601f73..4805233e1d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/code/SelectLanguage.tsx @@ -100,6 +100,11 @@ function SelectLanguage({ ref={ref} size={'small'} variant={'standard'} + sx={{ + '& .MuiInputBase-root, & .MuiInputBase-input': { + userSelect: 'none', + }, + }} className={'w-[150px]'} value={language} onClick={() => { @@ -115,6 +120,7 @@ function SelectLanguage({ {open && ( -
{item.name || t('document.title.placeholder')}
+
{item.name.trim() || t('menuAppHeader.defaultNewPageName')}
); }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx index 763a0983fa..d7d475199b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/divider/DividerNode.tsx @@ -15,7 +15,7 @@ export const DividerNode = memo( return (
-
+

diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx index 9d2b4fdac0..661eb3e3de 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageBlock.tsx @@ -1,4 +1,4 @@ -import React, { forwardRef, memo, useCallback, useRef } from 'react'; +import React, { forwardRef, memo, useCallback, useMemo, useRef } from 'react'; import { EditorElementProps, ImageNode } from '$app/application/document/document.types'; import { ReactEditor, useSelected, useSlateStatic } from 'slate-react'; import ImageRender from '$app/components/editor/components/blocks/image/ImageRender'; @@ -7,7 +7,7 @@ import ImageEmpty from '$app/components/editor/components/blocks/image/ImageEmpt export const ImageBlock = memo( forwardRef>(({ node, children, className, ...attributes }, ref) => { const selected = useSelected(); - const { url, align } = node.data; + const { url, align } = useMemo(() => node.data || {}, [node.data]); const containerRef = useRef(null); const editor = useSlateStatic(); const onFocusNode = useCallback(() => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx index 153e6f6a1c..07310b05be 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/image/ImageRender.tsx @@ -20,7 +20,7 @@ function ImageRender({ selected, node }: { selected: boolean; node: ImageNode }) const imgRef = useRef(null); const editor = useSlateStatic(); - const { url = '', width: imageWidth, image_type: source } = node.data; + const { url = '', width: imageWidth, image_type: source } = useMemo(() => node.data || {}, [node.data]); const { t } = useTranslation(); const blockId = node.blockId; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx index c0ee4f3ead..888b46c980 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/numbered_list/NumberListIcon.tsx @@ -1,15 +1,35 @@ import React, { useMemo } from 'react'; -import { ReactEditor, useSlate } from 'slate-react'; +import { ReactEditor, useSlate, useSlateStatic } from 'slate-react'; import { Element, Path } from 'slate'; import { NumberedListNode } from '$app/application/document/document.types'; +import { letterize, romanize } from '$app/utils/list'; +import { CustomEditor } from '$app/components/editor/command'; + +enum Letter { + Number = 'number', + Letter = 'letter', + Roman = 'roman', +} + +function getLetterNumber(index: number, letter: Letter) { + if (letter === Letter.Number) { + return index; + } else if (letter === Letter.Letter) { + return letterize(index); + } else { + return romanize(index); + } +} function NumberListIcon({ block, className }: { block: NumberedListNode; className: string }) { const editor = useSlate(); + const staticEditor = useSlateStatic(); const path = ReactEditor.findPath(editor, block); const index = useMemo(() => { let index = 1; + let topNode; let prevPath = Path.previous(path); while (prevPath) { @@ -19,6 +39,7 @@ function NumberListIcon({ block, className }: { block: NumberedListNode; classNa if (prevNode.type === block.type) { index += 1; + topNode = prevNode; } else { break; } @@ -26,17 +47,39 @@ function NumberListIcon({ block, className }: { block: NumberedListNode; classNa prevPath = Path.previous(prevPath); } - return index; + if (!topNode) { + return Number(block.data?.number ?? 1); + } + + const startIndex = (topNode as NumberedListNode).data?.number ?? 1; + + return index + Number(startIndex) - 1; }, [editor, block, path]); + const letter = useMemo(() => { + const level = CustomEditor.getListLevel(staticEditor, block.type, path); + + if (level % 3 === 0) { + return Letter.Number; + } else if (level % 3 === 1) { + return Letter.Letter; + } else { + return Letter.Roman; + } + }, [block.type, staticEditor, path]); + + const dataNumber = useMemo(() => { + return getLetterNumber(index, letter); + }, [index, letter]); + return ( { e.preventDefault(); }} contentEditable={false} - data-number={index} - className={`${className} numbered-icon flex w-[23px] min-w-[23px] justify-center pr-1 font-medium`} + data-number={dataNumber} + className={`${className} numbered-icon flex w-[24px] min-w-[24px] justify-center pr-1 font-medium`} /> ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx index 6d04a77c2e..f93cb897ba 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/page/Page.tsx @@ -4,7 +4,7 @@ import { EditorElementProps, PageNode } from '$app/application/document/document export const Page = memo( forwardRef>(({ node: _, children, ...attributes }, ref) => { const className = useMemo(() => { - return `${attributes.className ?? ''} document-title pb-3 text-4xl font-bold`; + return `${attributes.className ?? ''} document-title pb-3 text-5xl font-bold`; }, [attributes.className]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx index b745530acc..acf16581f4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/StartIcon.hooks.tsx @@ -36,7 +36,7 @@ export function useStartIcon(node: TextNode) { return null; } - return ; + return ; }, [Component, block]); return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx index 6ff97c2836..768524394e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/text/Text.tsx @@ -14,14 +14,13 @@ export const Text = memo( {renderIcon()} - - {children} + {children} ); }) diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx index 630aa93fb7..d98990c886 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/todo_list/CheckboxIcon.tsx @@ -1,7 +1,8 @@ import React, { useCallback } from 'react'; import { TodoListNode } from '$app/application/document/document.types'; import { CustomEditor } from '$app/components/editor/command'; -import { useSlateStatic } from 'slate-react'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { Location } from 'slate'; import { ReactComponent as CheckboxCheckSvg } from '$app/assets/database/checkbox-check.svg'; import { ReactComponent as CheckboxUncheckSvg } from '$app/assets/database/checkbox-uncheck.svg'; @@ -9,9 +10,25 @@ function CheckboxIcon({ block, className }: { block: TodoListNode; className: st const editor = useSlateStatic(); const { checked } = block.data; - const toggleTodo = useCallback(() => { - CustomEditor.toggleTodo(editor, block); - }, [editor, block]); + const toggleTodo = useCallback( + (e: React.MouseEvent) => { + const path = ReactEditor.findPath(editor, block); + const start = editor.start(path); + let at: Location = start; + + if (e.shiftKey) { + const end = editor.end(path); + + at = { + anchor: start, + focus: end, + }; + } + + CustomEditor.toggleTodo(editor, at); + }, + [editor, block] + ); return ( >(({ node, children, ...attributes }, ref) => { - const { checked } = node.data; + const { checked = false } = useMemo(() => node.data || {}, [node.data]); const className = useMemo(() => { return `flex w-full flex-col ${checked ? 'checked' : ''} ${attributes.className ?? ''}`; }, [attributes.className, checked]); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx index 8af826ae22..809f3b750d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/toggle_list/ToggleList.tsx @@ -1,9 +1,9 @@ -import React, { forwardRef, memo } from 'react'; +import React, { forwardRef, memo, useMemo } from 'react'; import { EditorElementProps, ToggleListNode } from '$app/application/document/document.types'; export const ToggleList = memo( forwardRef>(({ node, children, ...attributes }, ref) => { - const { collapsed } = node.data; + const { collapsed } = useMemo(() => node.data || {}, [node.data]); const className = `${attributes.className ?? ''} flex w-full flex-col ${collapsed ? 'collapsed' : ''}`; return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx index 83af8fdbd1..2526df895e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CollaborativeEditor.tsx @@ -12,8 +12,9 @@ export const CollaborativeEditor = memo( const [sharedType, setSharedType] = useState(null); const provider = useMemo(() => { setSharedType(null); - return new Provider(id, showTitle); - }, [id, showTitle]); + + return new Provider(id); + }, [id]); const root = useMemo(() => { if (!showTitle || !sharedType || !sharedType.doc) return null; @@ -70,17 +71,18 @@ export const CollaborativeEditor = memo( useEffect(() => { provider.connect(); + const handleConnected = () => { setSharedType(provider.sharedType); }; provider.on('ready', handleConnected); + void provider.initialDocument(showTitle); return () => { - setSharedType(null); provider.off('ready', handleConnected); provider.disconnect(); }; - }, [provider]); + }, [provider, showTitle]); if (!sharedType || id !== provider.id) { return null; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx index 0f077b82d8..b0bbe0eb28 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/CustomEditable.tsx @@ -1,7 +1,9 @@ -import React, { ComponentProps } from 'react'; -import { Editable } from 'slate-react'; +import React, { ComponentProps, useCallback } from 'react'; +import { Editable, useSlate } from 'slate-react'; import Element from './Element'; import { Leaf } from './Leaf'; +import { useShortcuts } from '$app/components/editor/plugins/shortcuts'; +import { useInlineKeyDown } from '$app/components/editor/components/editor/Editor.hooks'; type CustomEditableProps = Omit, 'renderElement' | 'renderLeaf'> & Partial, 'renderElement' | 'renderLeaf'>> & { @@ -14,9 +16,21 @@ export function CustomEditable({ renderLeaf = Leaf, ...props }: CustomEditableProps) { + const editor = useSlate(); + const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor); + const withInlineKeyDown = useInlineKeyDown(editor); + const onKeyDown = useCallback( + (event: React.KeyboardEvent) => { + withInlineKeyDown(event); + onShortcutsKeyDown(event); + }, + [onShortcutsKeyDown, withInlineKeyDown] + ); + return ( { if (!sharedType) return null; - const e = withShortcuts(withBlockPlugins(withInlines(withReact(withYHistory(withYjs(createEditor(), sharedType)))))); + const e = withMarkdown(withBlockPlugins(withInlines(withReact(withYHistory(withYjs(createEditor(), sharedType)))))); // Ensure editor always has at least 1 valid child const { normalizeNode } = e; @@ -47,6 +47,20 @@ export function useEditor(sharedType: Y.XmlText) { }, [editor]); const handleOnClickEnd = useCallback(() => { + const path = [editor.children.length - 1]; + const node = Editor.node(editor, path) as NodeEntry; + const latestNodeIsEmpty = CustomEditor.isEmptyText(editor, node[0]); + + if (latestNodeIsEmpty) { + ReactEditor.focus(editor); + editor.select(path); + editor.collapse({ + edge: 'end', + }); + + return; + } + CustomEditor.insertEmptyLineAtEnd(editor); }, [editor]); @@ -98,7 +112,7 @@ export function useInlineKeyDown(editor: ReactEditor) { const { nativeEvent } = e; if ( - isHotkey('left', nativeEvent) && + createHotkey(HOT_KEY_NAME.LEFT)(nativeEvent) && CustomEditor.beforeIsInlineNode(editor, selection, { unit: 'offset', }) @@ -108,7 +122,10 @@ export function useInlineKeyDown(editor: ReactEditor) { return; } - if (isHotkey('right', nativeEvent) && CustomEditor.afterIsInlineNode(editor, selection, { unit: 'offset' })) { + if ( + createHotkey(HOT_KEY_NAME.RIGHT)(nativeEvent) && + CustomEditor.afterIsInlineNode(editor, selection, { unit: 'offset' }) + ) { e.preventDefault(); Transforms.move(editor, { unit: 'offset' }); return; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx index 12b198b23e..d87dbe3f35 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/editor/Editor.tsx @@ -1,13 +1,8 @@ -import React, { memo, useCallback } from 'react'; -import { - useDecorateCodeHighlight, - useEditor, - useInlineKeyDown, -} from '$app/components/editor/components/editor/Editor.hooks'; +import React, { useCallback } from 'react'; +import { useDecorateCodeHighlight, useEditor } from '$app/components/editor/components/editor/Editor.hooks'; import { Slate } from 'slate-react'; import { CustomEditable } from '$app/components/editor/components/editor/CustomEditable'; import { SelectionToolbar } from '$app/components/editor/components/tools/selection_toolbar'; -import { useShortcuts } from 'src/appflowy_app/components/editor/plugins/shortcuts'; import { BlockActionsToolbar } from '$app/components/editor/components/tools/block_actions'; import { CircularProgress } from '@mui/material'; @@ -26,8 +21,7 @@ import { LocalEditorProps } from '$app/application/document/document.types'; function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: LocalEditorProps) { const { editor, initialValue, handleOnClickEnd, ...props } = useEditor(sharedType); const decorateCodeHighlight = useDecorateCodeHighlight(editor); - const { onKeyDown: onShortcutsKeyDown } = useShortcuts(editor); - const withInlineKeyDown = useInlineKeyDown(editor); + const { selectedBlocks, decorate: decorateCustomRange, @@ -47,14 +41,6 @@ function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: [decorateCodeHighlight, decorateCustomRange] ); - const onKeyDown = useCallback( - (event: React.KeyboardEvent) => { - withInlineKeyDown(event); - onShortcutsKeyDown(event); - }, - [onShortcutsKeyDown, withInlineKeyDown] - ); - if (editor.sharedRoot.length === 0) { return ; } @@ -72,7 +58,6 @@ function Editor({ sharedType, disableFocus, caretColor = 'var(--text-title)' }: onDone(text)}> - + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx index dff7b7dae6..09095480dc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/Link.tsx @@ -39,7 +39,7 @@ export const Link = memo(({ children }: { leaf: Text; children: React.ReactNode {children} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx index 3d7dff3cb4..af62a7b28f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditContent.tsx @@ -14,7 +14,7 @@ import KeyboardNavigation, { } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import isHotkey from 'is-hotkey'; import LinkEditInput from '$app/components/editor/components/inline_nodes/link/LinkEditInput'; -import { openUrl, pattern } from '$app/utils/open_url'; +import { openUrl, isUrl } from '$app/utils/open_url'; function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaultHref: string }) { const editor = useSlateStatic(); @@ -59,7 +59,7 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul if (e.key === 'Enter') { e.preventDefault(); - if (pattern.test(link)) { + if (isUrl(link)) { onClose(); setNodeMark(); } @@ -125,7 +125,7 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul return [ { key: 'open', - disabled: !pattern.test(link), + disabled: !isUrl(link), content: renderOption(, t('editor.openLink')), }, { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx index b9ca0345af..6e9a0bb497 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditInput.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { TextField } from '@mui/material'; import { useTranslation } from 'react-i18next'; -import { pattern } from '$app/utils/open_url'; +import { isUrl } from '$app/utils/open_url'; function LinkEditInput({ link, @@ -16,7 +16,7 @@ function LinkEditInput({ const [error, setError] = useState(null); useEffect(() => { - if (pattern.test(link)) { + if (isUrl(link)) { setError(null); return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx index 488784d38b..2a5e3630da 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/link/LinkEditPopover.tsx @@ -60,7 +60,7 @@ export function LinkEditPopover({ style={{ maxHeight: paperHeight, }} - className='flex flex-col p-4' + className='flex select-none flex-col p-4' >
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx index 12e5bab14d..10def395c5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/mention/MentionLeaf.tsx @@ -2,8 +2,6 @@ import React, { useCallback, useEffect, useState } from 'react'; import { Mention, MentionPage } from '$app/application/document/document.types'; import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; import { useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { pageTypeMap } from '$app_reducers/pages/slice'; import { getPage } from '$app/application/folder/page.service'; import { useSelected, useSlate } from 'slate-react'; import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg'; @@ -11,15 +9,17 @@ import { notify } from 'src/appflowy_app/components/_shared/notify'; import { subscribeNotifications } from '$app/application/notification'; import { FolderNotification } from '@/services/backend'; import { Editor, Range } from 'slate'; +import { useAppDispatch } from '$app/stores/store'; +import { openPage } from '$app_reducers/pages/async_actions'; export function MentionLeaf({ mention }: { mention: Mention }) { const { t } = useTranslation(); const [page, setPage] = useState(null); const [error, setError] = useState(false); - const navigate = useNavigate(); const editor = useSlate(); const selected = useSelected(); const isCollapsed = editor.selection && Range.isCollapsed(editor.selection); + const dispatch = useAppDispatch(); useEffect(() => { if (selected && isCollapsed && page) { @@ -56,16 +56,14 @@ export function MentionLeaf({ mention }: { mention: Mention }) { void loadPage(); }, [loadPage]); - const openPage = useCallback(() => { + const handleOpenPage = useCallback(() => { if (!page) { notify.error(t('document.mention.deletedContent')); return; } - const pageType = pageTypeMap[page.layout]; - - navigate(`/page/${pageType}/${page.id}`); - }, [navigate, page, t]); + void dispatch(openPage(page.id)); + }, [page, dispatch, t]); useEffect(() => { if (!page) return; @@ -117,7 +115,7 @@ export function MentionLeaf({ mention }: { mention: Mention }) { return ( {page.icon?.value || } - {page.name || t('document.title.placeholder')} + {page.name.trim() || t('menuAppHeader.defaultNewPageName')} ) )} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts index b35239ae21..bc1086dde9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.hooks.ts @@ -1,8 +1,9 @@ import { RefObject, useCallback, useEffect, useState } from 'react'; import { ReactEditor, useSlate } from 'slate-react'; -import { findEventRange, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils'; +import { findEventNode, getBlockActionsPosition } from '$app/components/editor/components/tools/block_actions/utils'; import { Element, Editor, Range } from 'slate'; import { EditorNodeType } from '$app/application/document/document.types'; +import { Log } from '$app/utils/log'; export function useBlockActionsToolbar(ref: RefObject, contextMenuVisible: boolean) { const editor = useSlate(); @@ -45,28 +46,55 @@ export function useBlockActionsToolbar(ref: RefObject, contextMe } let range: Range | null = null; + let node; try { range = ReactEditor.findEventRange(editor, e); } catch { - range = findEventRange(editor, e); + const editorDom = ReactEditor.toDOMNode(editor, editor); + const rect = editorDom.getBoundingClientRect(); + const isOverLeftBoundary = e.clientX < rect.left + 64; + const isOverRightBoundary = e.clientX > rect.right - 64; + let newX = e.clientX; + + if (isOverLeftBoundary) { + newX = rect.left + 64; + } + + if (isOverRightBoundary) { + newX = rect.right - 64; + } + + node = findEventNode(editor, { + x: newX, + y: e.clientY, + }); } - if (!range) return; - const match = editor.above({ - match: (n) => { - return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined; - }, - at: range, - }); + if (!range && !node) { + Log.warn('No range and node found'); + return; + } else if (range) { + const match = editor.above({ + match: (n) => { + return !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined; + }, + at: range, + }); - if (!match) { + if (!match) { + close(); + return; + } + + node = match[0] as Element; + } + + if (!node) { close(); return; } - const node = match[0] as Element; - if (node.type === EditorNodeType.Page) return; const blockElement = ReactEditor.toDOMNode(editor, node); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx index 2f5f7a19d6..729b4df144 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockActionsToolbar.tsx @@ -9,6 +9,9 @@ import { PopoverProps } from '@mui/material/Popover'; import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; import withErrorBoundary from '$app/components/_shared/error_boundary/withError'; +import { CustomEditor } from '$app/components/editor/command'; +import isEqual from 'lodash-es/isEqual'; +import { Range } from 'slate'; const Toolbar = () => { const ref = useRef(null); @@ -38,10 +41,42 @@ const Toolbar = () => { if (!node) return; const nodeDom = ReactEditor.toDOMNode(editor, node); const onContextMenu = (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); const { clientX, clientY } = e; + e.stopPropagation(); + + const { selection } = editor; + + const editorRange = ReactEditor.findEventRange(editor, e); + + if (!editorRange || !selection) return; + + const rangeBlock = CustomEditor.getBlock(editor, editorRange); + const selectedBlock = CustomEditor.getBlock(editor, selection); + + if ( + Range.intersection(selection, editorRange) || + (rangeBlock && selectedBlock && isEqual(rangeBlock[1], selectedBlock[1])) + ) { + const windowSelection = window.getSelection(); + const range = windowSelection?.rangeCount ? windowSelection?.getRangeAt(0) : null; + const isCollapsed = windowSelection?.isCollapsed; + + if (windowSelection && !isCollapsed) { + if (range && range.endOffset === 0 && range.startContainer !== range.endContainer) { + const newRange = range.cloneRange(); + + newRange.setEnd(range.startContainer, range.startOffset); + windowSelection.removeAllRanges(); + windowSelection.addRange(newRange); + } + } + + return; + } + + e.preventDefault(); + popoverPropsRef.current = { transformOrigin: { vertical: 'top', diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts index 4166022182..b63afe9dc1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/utils.ts @@ -34,63 +34,25 @@ export function getBlockCssProperty(node: Element) { } /** - * Resolve can not find the range when the drop occurs on the icon. * @param editor * @param e */ -export function findEventRange(editor: ReactEditor, e: MouseEvent) { - const { clientX: x, clientY: y } = e; +export function findEventNode( + editor: ReactEditor, + { + x, + y, + }: { + x: number; + y: number; + } +) { + const element = document.elementFromPoint(x, y); + const nodeDom = element?.closest('[data-block-type]'); - // Else resolve a range from the caret position where the drop occured. - let domRange; - const { document } = ReactEditor.getWindow(editor); - - // COMPAT: In Firefox, `caretRangeFromPoint` doesn't exist. (2016/07/25) - if (document.caretRangeFromPoint) { - domRange = document.caretRangeFromPoint(x, y); - } else if ('caretPositionFromPoint' in document && typeof document.caretPositionFromPoint === 'function') { - const position = document.caretPositionFromPoint(x, y); - - if (position) { - domRange = document.createRange(); - domRange.setStart(position.offsetNode, position.offset); - domRange.setEnd(position.offsetNode, position.offset); - } + if (nodeDom) { + return ReactEditor.toSlateNode(editor, nodeDom) as Element; } - if (domRange && domRange.startContainer) { - const startContainer = domRange.startContainer; - - let element: HTMLElement | null = startContainer as HTMLElement; - const nodeType = element.nodeType; - - if (nodeType === 3 || typeof element === 'string') { - const parent = element.parentElement?.closest('.text-block-icon') as HTMLElement; - - element = parent; - } - - if (element && element.nodeType < 3) { - if (element.classList?.contains('text-block-icon')) { - const sibling = domRange.startContainer.parentElement; - - if (sibling) { - domRange.selectNode(sibling); - } - } - } - } - - if (!domRange) { - return null; - } - - try { - return ReactEditor.toSlateRange(editor, domRange, { - exactMatch: false, - suppressThrow: false, - }); - } catch { - return null; - } + return null; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx index 806a2a3788..5d83870719 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks.tsx @@ -1,51 +1,26 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSlate } from 'slate-react'; import { MentionPage, MentionType } from '$app/application/document/document.types'; import { CustomEditor } from '$app/components/editor/command'; -import { useAppSelector } from '$app/stores/store'; import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; +// import dayjs from 'dayjs'; +// enum DateKey { +// Today = 'today', +// Tomorrow = 'tomorrow', +// } export function useMentionPanel({ closePanel, - searchText, + pages, }: { - searchText: string; + pages: MentionPage[]; closePanel: (deleteText?: boolean) => void; }) { const { t } = useTranslation(); const editor = useSlate(); - const pagesMap = useAppSelector((state) => state.pages.pageMap); - - const pagesRef = useRef([]); - const [recentPages, setPages] = useState([]); - - const loadPages = useCallback(async () => { - const pages = Object.values(pagesMap); - - pagesRef.current = pages; - setPages(pages); - }, [pagesMap]); - - useEffect(() => { - void loadPages(); - }, [loadPages]); - - useEffect(() => { - if (!searchText) { - setPages(pagesRef.current); - return; - } - - const filteredPages = pagesRef.current.filter((page) => { - return page.name.toLowerCase().includes(searchText.toLowerCase()); - }); - - setPages(filteredPages); - }, [searchText]); - const onConfirm = useCallback( (key: string) => { const [, id] = key.split(','); @@ -67,7 +42,7 @@ export function useMentionPanel({
{page.icon?.value || }
-
{page.name || t('document.title.placeholder')}
+
{page.name.trim() || t('menuAppHeader.defaultNewPageName')}
), }; @@ -75,15 +50,62 @@ export function useMentionPanel({ [t] ); + // const renderDate = useCallback(() => { + // return [ + // { + // key: DateKey.Today, + // content: ( + //
+ // {t('relativeDates.today')} -{' '} + // {dayjs().format('MMM D, YYYY')} + //
+ // ), + // + // children: [], + // }, + // { + // key: DateKey.Tomorrow, + // content: ( + //
+ // {t('relativeDates.tomorrow')} + //
+ // ), + // children: [], + // }, + // ]; + // }, [t]); + const options: KeyboardNavigationOption[] = useMemo(() => { return [ + // { + // key: MentionType.Date, + // content:
{t('editor.date')}
, + // children: renderDate(), + // }, + { + key: 'divider', + content:
, + children: [], + }, + { key: MentionType.PageRef, content:
{t('document.mention.page.label')}
, - children: recentPages.map(renderPage), + children: + pages.length > 0 + ? pages.map(renderPage) + : [ + { + key: 'noPage', + content: ( +
{t('findAndReplace.noResult')}
+ ), + children: [], + }, + ], }, - ].filter((option) => option.children.length > 0); - }, [recentPages, renderPage, t]); + ]; + }, [pages, renderPage, t]); return { options, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx index eba84e4ac4..6ca0225579 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.tsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { initialAnchorOrigin, initialTransformOrigin, @@ -9,10 +9,39 @@ import Popover from '@mui/material/Popover'; import MentionPanelContent from '$app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent'; import usePopoverAutoPosition from '$app/components/_shared/popover/Popover.hooks'; +import { useAppSelector } from '$app/stores/store'; +import { MentionPage } from '$app/application/document/document.types'; export function MentionPanel({ anchorPosition, closePanel, searchText }: PanelProps) { const ref = useRef(null); + const pagesMap = useAppSelector((state) => state.pages.pageMap); + const pagesRef = useRef([]); + const [recentPages, setPages] = useState([]); + + const loadPages = useCallback(async () => { + const pages = Object.values(pagesMap); + + pagesRef.current = pages; + setPages(pages); + }, [pagesMap]); + + useEffect(() => { + void loadPages(); + }, [loadPages]); + + useEffect(() => { + if (!searchText) { + setPages(pagesRef.current); + return; + } + + const filteredPages = pagesRef.current.filter((page) => { + return page.name.toLowerCase().includes(searchText.toLowerCase()); + }); + + setPages(filteredPages); + }, [searchText]); const open = Boolean(anchorPosition); const { @@ -42,12 +71,7 @@ export function MentionPanel({ anchorPosition, closePanel, searchText }: PanelPr transformOrigin={transformOrigin} onClose={() => closePanel(false)} > - + )}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx index 9664479fbd..36b00ca2b6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/mention_panel/MentionPanelContent.tsx @@ -2,15 +2,16 @@ import React, { useRef } from 'react'; import { useMentionPanel } from '$app/components/editor/components/tools/command_panel/mention_panel/MentionPanel.hooks'; import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import { MentionPage } from '$app/application/document/document.types'; function MentionPanelContent({ closePanel, - searchText, + pages, maxHeight, width, }: { closePanel: (deleteText?: boolean) => void; - searchText: string; + pages: MentionPage[]; maxHeight: number; width: number; }) { @@ -18,7 +19,7 @@ function MentionPanelContent({ const { options, onConfirm } = useMentionPanel({ closePanel, - searchText, + pages, }); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx index ddab776abc..c2d9445b56 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks.tsx @@ -20,87 +20,16 @@ import { CustomEditor } from '$app/components/editor/command'; import { KeyboardNavigationOption } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import { YjsEditor } from '@slate-yjs/core'; import { useEditorBlockDispatch } from '$app/components/editor/stores/block'; - -enum SlashCommandPanelTab { - BASIC = 'basic', - MEDIA = 'media', - DATABASE = 'database', - ADVANCED = 'advanced', -} - -export enum SlashOptionType { - Paragraph, - TodoList, - Heading1, - Heading2, - Heading3, - BulletedList, - NumberedList, - Quote, - ToggleList, - Divider, - Callout, - Code, - Grid, - MathEquation, - Image, -} -const slashOptionGroup = [ - { - key: SlashCommandPanelTab.BASIC, - options: [ - SlashOptionType.Paragraph, - SlashOptionType.TodoList, - SlashOptionType.Heading1, - SlashOptionType.Heading2, - SlashOptionType.Heading3, - SlashOptionType.BulletedList, - SlashOptionType.NumberedList, - SlashOptionType.Quote, - SlashOptionType.ToggleList, - SlashOptionType.Divider, - SlashOptionType.Callout, - ], - }, - { - key: SlashCommandPanelTab.MEDIA, - options: [SlashOptionType.Code, SlashOptionType.Image], - }, - { - key: SlashCommandPanelTab.DATABASE, - options: [SlashOptionType.Grid], - }, - { - key: SlashCommandPanelTab.ADVANCED, - options: [SlashOptionType.MathEquation], - }, -]; - -const slashOptionMapToEditorNodeType = { - [SlashOptionType.Paragraph]: EditorNodeType.Paragraph, - [SlashOptionType.TodoList]: EditorNodeType.TodoListBlock, - [SlashOptionType.Heading1]: EditorNodeType.HeadingBlock, - [SlashOptionType.Heading2]: EditorNodeType.HeadingBlock, - [SlashOptionType.Heading3]: EditorNodeType.HeadingBlock, - [SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock, - [SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock, - [SlashOptionType.Quote]: EditorNodeType.QuoteBlock, - [SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock, - [SlashOptionType.Divider]: EditorNodeType.DividerBlock, - [SlashOptionType.Callout]: EditorNodeType.CalloutBlock, - [SlashOptionType.Code]: EditorNodeType.CodeBlock, - [SlashOptionType.Grid]: EditorNodeType.GridBlock, - [SlashOptionType.MathEquation]: EditorNodeType.EquationBlock, - [SlashOptionType.Image]: EditorNodeType.ImageBlock, -}; - -const headingTypeToLevelMap: Record = { - [SlashOptionType.Heading1]: 1, - [SlashOptionType.Heading2]: 2, - [SlashOptionType.Heading3]: 3, -}; - -const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3]; +import { + headingTypes, + headingTypeToLevelMap, + reorderSlashOptions, + SlashAliases, + SlashCommandPanelTab, + slashOptionGroup, + slashOptionMapToEditorNodeType, + SlashOptionType, +} from '$app/components/editor/components/tools/command_panel/slash_command_panel/const'; export function useSlashCommandPanel({ searchText, @@ -157,7 +86,7 @@ export function useSlashCommandPanel({ if (!newNode || !path) return; - const isEmpty = CustomEditor.isEmptyText(editor, newNode) && node.type === EditorNodeType.Paragraph; + const isEmpty = CustomEditor.isEmptyText(editor, newNode); if (!isEmpty) { const nextPath = Path.next(path); @@ -281,6 +210,7 @@ export function useSlashCommandPanel({ key: group.key, content:
{groupTypeToLabelMap[group.key]}
, children: group.options + .map((type) => { return { key: type, @@ -297,8 +227,12 @@ export function useSlashCommandPanel({ newSearchText = searchText.slice(1); } - return label.toLowerCase().includes(newSearchText.toLowerCase()); - }), + return ( + label.toLowerCase().includes(newSearchText.toLowerCase()) || + SlashAliases[option.key].some((alias) => alias.startsWith(newSearchText.toLowerCase())) + ); + }) + .sort(reorderSlashOptions(searchText)), }; }) .filter((group) => group.children.length > 0); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx index c5f2df0ae5..256e82f811 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanelContent.tsx @@ -1,10 +1,8 @@ import React, { useEffect, useRef } from 'react'; import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; -import { - SlashOptionType, - useSlashCommandPanel, -} from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks'; +import { useSlashCommandPanel } from '$app/components/editor/components/tools/command_panel/slash_command_panel/SlashCommandPanel.hooks'; import { useSlateStatic } from 'slate-react'; +import { SlashOptionType } from '$app/components/editor/components/tools/command_panel/slash_command_panel/const'; const noResultBuffer = 2; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/const.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/const.ts new file mode 100644 index 0000000000..7dfaa2b4a0 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/command_panel/slash_command_panel/const.ts @@ -0,0 +1,174 @@ +import { EditorNodeType } from '$app/application/document/document.types'; + +export enum SlashCommandPanelTab { + BASIC = 'basic', + MEDIA = 'media', + DATABASE = 'database', + ADVANCED = 'advanced', +} + +export enum SlashOptionType { + Paragraph, + TodoList, + Heading1, + Heading2, + Heading3, + BulletedList, + NumberedList, + Quote, + ToggleList, + Divider, + Callout, + Code, + Grid, + MathEquation, + Image, +} + +export const slashOptionGroup = [ + { + key: SlashCommandPanelTab.BASIC, + options: [ + SlashOptionType.Paragraph, + SlashOptionType.TodoList, + SlashOptionType.Heading1, + SlashOptionType.Heading2, + SlashOptionType.Heading3, + SlashOptionType.BulletedList, + SlashOptionType.NumberedList, + SlashOptionType.Quote, + SlashOptionType.ToggleList, + SlashOptionType.Divider, + SlashOptionType.Callout, + ], + }, + { + key: SlashCommandPanelTab.MEDIA, + options: [SlashOptionType.Code, SlashOptionType.Image], + }, + { + key: SlashCommandPanelTab.DATABASE, + options: [SlashOptionType.Grid], + }, + { + key: SlashCommandPanelTab.ADVANCED, + options: [SlashOptionType.MathEquation], + }, +]; +export const slashOptionMapToEditorNodeType = { + [SlashOptionType.Paragraph]: EditorNodeType.Paragraph, + [SlashOptionType.TodoList]: EditorNodeType.TodoListBlock, + [SlashOptionType.Heading1]: EditorNodeType.HeadingBlock, + [SlashOptionType.Heading2]: EditorNodeType.HeadingBlock, + [SlashOptionType.Heading3]: EditorNodeType.HeadingBlock, + [SlashOptionType.BulletedList]: EditorNodeType.BulletedListBlock, + [SlashOptionType.NumberedList]: EditorNodeType.NumberedListBlock, + [SlashOptionType.Quote]: EditorNodeType.QuoteBlock, + [SlashOptionType.ToggleList]: EditorNodeType.ToggleListBlock, + [SlashOptionType.Divider]: EditorNodeType.DividerBlock, + [SlashOptionType.Callout]: EditorNodeType.CalloutBlock, + [SlashOptionType.Code]: EditorNodeType.CodeBlock, + [SlashOptionType.Grid]: EditorNodeType.GridBlock, + [SlashOptionType.MathEquation]: EditorNodeType.EquationBlock, + [SlashOptionType.Image]: EditorNodeType.ImageBlock, +}; +export const headingTypeToLevelMap: Record = { + [SlashOptionType.Heading1]: 1, + [SlashOptionType.Heading2]: 2, + [SlashOptionType.Heading3]: 3, +}; +export const headingTypes = [SlashOptionType.Heading1, SlashOptionType.Heading2, SlashOptionType.Heading3]; + +export const SlashAliases = { + [SlashOptionType.Paragraph]: ['paragraph', 'text', 'block', 'textblock'], + [SlashOptionType.TodoList]: [ + 'list', + 'todo', + 'todolist', + 'checkbox', + 'block', + 'todoblock', + 'checkboxblock', + 'todolistblock', + ], + [SlashOptionType.Heading1]: ['h1', 'heading1', 'block', 'headingblock', 'h1block'], + [SlashOptionType.Heading2]: ['h2', 'heading2', 'block', 'headingblock', 'h2block'], + [SlashOptionType.Heading3]: ['h3', 'heading3', 'block', 'headingblock', 'h3block'], + [SlashOptionType.BulletedList]: [ + 'list', + 'bulleted', + 'block', + 'bulletedlist', + 'bulletedblock', + 'listblock', + 'bulletedlistblock', + 'bulletelist', + ], + [SlashOptionType.NumberedList]: [ + 'list', + 'numbered', + 'block', + 'numberedlist', + 'numberedblock', + 'listblock', + 'numberedlistblock', + 'numberlist', + ], + [SlashOptionType.Quote]: ['quote', 'block', 'quoteblock'], + [SlashOptionType.ToggleList]: ['list', 'toggle', 'block', 'togglelist', 'toggleblock', 'listblock', 'togglelistblock'], + [SlashOptionType.Divider]: ['divider', 'hr', 'block', 'dividerblock', 'line', 'lineblock'], + [SlashOptionType.Callout]: ['callout', 'info', 'block', 'calloutblock'], + [SlashOptionType.Code]: ['code', 'code', 'block', 'codeblock', 'media'], + [SlashOptionType.Grid]: ['grid', 'table', 'block', 'gridblock', 'database'], + [SlashOptionType.MathEquation]: [ + 'math', + 'equation', + 'block', + 'mathblock', + 'mathequation', + 'mathequationblock', + 'advanced', + ], + [SlashOptionType.Image]: ['img', 'image', 'block', 'imageblock', 'media'], +}; + +export const reorderSlashOptions = (searchText: string) => { + return ( + a: { + key: SlashOptionType; + }, + b: { + key: SlashOptionType; + } + ) => { + const compareIndex = (option: SlashOptionType) => { + const aliases = SlashAliases[option]; + + if (aliases) { + for (const alias of aliases) { + if (alias.startsWith(searchText)) { + return -1; + } + } + } + + return 0; + }; + + const compareLength = (option: SlashOptionType) => { + const aliases = SlashAliases[option]; + + if (aliases) { + for (const alias of aliases) { + if (alias.length < searchText.length) { + return -1; + } + } + } + + return 0; + }; + + return compareIndex(a.key) - compareIndex(b.key) || compareLength(a.key) - compareLength(b.key); + }; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx index 3dab0fa182..9d7c19b999 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionActions.tsx @@ -14,7 +14,7 @@ import { Quote } from '$app/components/editor/components/tools/selection_toolbar import { ToggleList } from '$app/components/editor/components/tools/selection_toolbar/actions/toggle_list'; import { BulletedList } from '$app/components/editor/components/tools/selection_toolbar/actions/bulleted_list'; import { NumberedList } from '$app/components/editor/components/tools/selection_toolbar/actions/numbered_list'; -import { Href } from '$app/components/editor/components/tools/selection_toolbar/actions/href'; +import { Href, LinkActions } from '$app/components/editor/components/tools/selection_toolbar/actions/href'; import { Align } from '$app/components/editor/components/tools/selection_toolbar/actions/align'; import { Color } from '$app/components/editor/components/tools/selection_toolbar/actions/color'; @@ -65,6 +65,7 @@ function SelectionActions({ {!isAcrossBlocks && } +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts index 2208058b61..58834db6d5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/SelectionToolbar.hooks.ts @@ -109,7 +109,10 @@ export function useSelectionToolbar(ref: MutableRefObject useEffect(() => { const decorateState = getStaticState(); - if (decorateState) return; + if (decorateState) { + setIsAcrossBlocks(false); + return; + } const { selection } = editor; @@ -131,10 +134,7 @@ export function useSelectionToolbar(ref: MutableRefObject return; } - const start = selection.anchor; - const end = selection.focus; - - setIsAcrossBlocks(!CustomEditor.blockEqual(editor, start, end)); + setIsAcrossBlocks(CustomEditor.isMultipleBlockSelected(editor, true)); debounceRecalculatePosition(); }); @@ -169,6 +169,7 @@ export function useSelectionToolbar(ref: MutableRefObject }; }, [visible, editor, ref]); + // Close toolbar when press ESC useEffect(() => { const slateEditorDom = ReactEditor.toDOMNode(editor, editor); const onKeyDown = (e: KeyboardEvent) => { @@ -195,6 +196,39 @@ export function useSelectionToolbar(ref: MutableRefObject }; }, [closeToolbar, debounceRecalculatePosition, editor, visible]); + // Recalculate position when the scroll container is scrolled + useEffect(() => { + const slateEditorDom = ReactEditor.toDOMNode(editor, editor); + const scrollContainer = slateEditorDom.closest('.appflowy-scroll-container'); + + if (!visible) return; + if (!scrollContainer) return; + const handleScroll = () => { + if (isDraggingRef.current) return; + + const domSelection = window.getSelection(); + const rangeCount = domSelection?.rangeCount; + + if (!rangeCount) return null; + + const domRange = rangeCount > 0 ? domSelection.getRangeAt(0) : undefined; + + const rangeRect = domRange?.getBoundingClientRect(); + + // Stop calculating when the range is out of the window + if (!rangeRect?.bottom || rangeRect.bottom < 0) { + return; + } + + recalculatePosition(); + }; + + scrollContainer.addEventListener('scroll', handleScroll); + return () => { + scrollContainer.removeEventListener('scroll', handleScroll); + }; + }, [visible, editor, recalculatePosition]); + return { visible, restoreSelection, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx index 66d2839a96..23917e146b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/align/Align.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import Tooltip from '@mui/material/Tooltip'; import { ReactComponent as AlignLeftSvg } from '$app/assets/align-left.svg'; import { ReactComponent as AlignCenterSvg } from '$app/assets/align-center.svg'; @@ -6,10 +6,9 @@ import { ReactComponent as AlignRightSvg } from '$app/assets/align-right.svg'; import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; import { useTranslation } from 'react-i18next'; import { CustomEditor } from '$app/components/editor/command'; -import { ReactEditor, useSlateStatic } from 'slate-react'; +import { useSlateStatic } from 'slate-react'; import { IconButton } from '@mui/material'; import { ReactComponent as MoreSvg } from '$app/assets/more.svg'; -import { createHotkey, HOT_KEY_NAME } from '$app/utils/hotkeys'; export function Align() { const { t } = useTranslation(); @@ -61,36 +60,6 @@ export function Align() { } }, []); - useEffect(() => { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (createHotkey(HOT_KEY_NAME.ALIGN_LEFT)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleAlign(editor, 'left'); - return; - } - - if (createHotkey(HOT_KEY_NAME.ALIGN_CENTER)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleAlign(editor, 'center'); - return; - } - - if (createHotkey(HOT_KEY_NAME.ALIGN_RIGHT)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleAlign(editor, 'right'); - return; - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor]); return ( { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (createHotkey(HOT_KEY_NAME.BOLD)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleMark(editor, { - key: EditorMarkFormat.Bold, - value: true, - }); - return; - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor]); - return ( { - const range = decorateState?.range; - - if (!range) return; - - const domRange = ReactEditor.toDOMRange(editor, range); - - const rect = domRange.getBoundingClientRect(); - - return { - top: rect.top, - left: rect.left, - height: rect.height, - }; - }, [decorateState?.range, editor]); - - const defaultHref = useMemo(() => { - const range = decorateState?.range; - - if (!range) return ''; - - const marks = Editor.marks(editor); - - return marks?.href || Editor.string(editor, range); - }, [decorateState?.range, editor]); - - const { add: addDecorate, clear: clearDecorate } = useDecorateDispatch(); + const { add: addDecorate } = useDecorateDispatch(); const onClick = useCallback(() => { if (!editor.selection) return; addDecorate({ @@ -55,33 +24,6 @@ export function Href() { }); }, [addDecorate, editor]); - const handleEditPopoverClose = useCallback(() => { - const range = decorateState?.range; - - clearDecorate(); - if (range) { - ReactEditor.focus(editor); - editor.select(range); - } - }, [clearDecorate, decorateState?.range, editor]); - - useEffect(() => { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (isHotkey('mod+k', e)) { - if (editor.selection && Range.isCollapsed(editor.selection)) return; - e.preventDefault(); - e.stopPropagation(); - onClick(); - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor, onClick]); - const tooltip = useMemo(() => { const modifier = getModifier(); @@ -98,15 +40,6 @@ export function Href() { - {openEditPopover && ( - - )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/LinkActions.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/LinkActions.tsx new file mode 100644 index 0000000000..b77a249051 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/LinkActions.tsx @@ -0,0 +1,59 @@ +import React, { useCallback, useMemo } from 'react'; +import { useDecorateDispatch, useDecorateState } from '$app/components/editor/stores'; +import { ReactEditor, useSlateStatic } from 'slate-react'; +import { Editor } from 'slate'; +import { LinkEditPopover } from '$app/components/editor/components/inline_nodes/link'; + +export function LinkActions() { + const editor = useSlateStatic(); + const decorateState = useDecorateState('link'); + const openEditPopover = !!decorateState; + const { clear: clearDecorate } = useDecorateDispatch(); + + const anchorPosition = useMemo(() => { + const range = decorateState?.range; + + if (!range) return; + + const domRange = ReactEditor.toDOMRange(editor, range); + + const rect = domRange.getBoundingClientRect(); + + return { + top: rect.top, + left: rect.left, + height: rect.height, + }; + }, [decorateState?.range, editor]); + + const defaultHref = useMemo(() => { + const range = decorateState?.range; + + if (!range) return ''; + + const marks = Editor.marks(editor); + + return marks?.href || Editor.string(editor, range); + }, [decorateState?.range, editor]); + + const handleEditPopoverClose = useCallback(() => { + const range = decorateState?.range; + + clearDecorate(); + if (range) { + ReactEditor.focus(editor); + editor.select(range); + } + }, [clearDecorate, decorateState?.range, editor]); + + if (!openEditPopover) return null; + return ( + + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts index 758b3b39d3..9a7210c140 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/href/index.ts @@ -1 +1,2 @@ export * from './Href'; +export * from './LinkActions'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx index 39b48ad525..3cf9c7ed85 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/inline_code/InlineCode.tsx @@ -1,11 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; import { useTranslation } from 'react-i18next'; -import { ReactEditor, useSlateStatic } from 'slate-react'; +import { useSlateStatic } from 'slate-react'; import { CustomEditor } from '$app/components/editor/command'; import { ReactComponent as CodeSvg } from '$app/assets/inline-code.svg'; import { EditorMarkFormat } from '$app/application/document/document.types'; -import { createHotkey, createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; export function InlineCode() { const { t } = useTranslation(); @@ -20,26 +20,6 @@ export function InlineCode() { }); }, [editor]); - useEffect(() => { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (createHotkey(HOT_KEY_NAME.CODE)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleMark(editor, { - key: EditorMarkFormat.Code, - value: true, - }); - return; - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor]); - return ( { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (createHotkey(HOT_KEY_NAME.ITALIC)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleMark(editor, { - key: EditorMarkFormat.Italic, - value: true, - }); - return; - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor]); return ( { + let type = EditorNodeType.NumberedListBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + CustomEditor.turnToBlock(editor, { - type: EditorNodeType.NumberedListBlock, + type, }); - }, [editor]); + }, [editor, isActivated]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx index 2076c84b1b..29ad0de104 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/quote/Quote.tsx @@ -12,10 +12,16 @@ export function Quote() { const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.QuoteBlock); const onClick = useCallback(() => { + let type = EditorNodeType.QuoteBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + CustomEditor.turnToBlock(editor, { - type: EditorNodeType.QuoteBlock, + type, }); - }, [editor]); + }, [editor, isActivated]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx index 21a0c3697a..325f6ac55a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/strikethrough/StrikeThrough.tsx @@ -1,11 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; import { useTranslation } from 'react-i18next'; -import { ReactEditor, useSlateStatic } from 'slate-react'; +import { useSlateStatic } from 'slate-react'; import { CustomEditor } from '$app/components/editor/command'; import { ReactComponent as StrikeThroughSvg } from '$app/assets/strikethrough.svg'; import { EditorMarkFormat } from '$app/application/document/document.types'; -import { createHotkey, createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; export function StrikeThrough() { const { t } = useTranslation(); @@ -20,26 +20,6 @@ export function StrikeThrough() { }); }, [editor]); - useEffect(() => { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (createHotkey(HOT_KEY_NAME.STRIKETHROUGH)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleMark(editor, { - key: EditorMarkFormat.StrikeThrough, - value: true, - }); - return; - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor]); - return ( { + let type = EditorNodeType.TodoListBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + CustomEditor.turnToBlock(editor, { - type: EditorNodeType.TodoListBlock, + type, + data: { + checked: false, + }, }); - }, [editor]); + }, [editor, isActivated]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx index 1302a84a87..4d82652988 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/toggle_list/ToggleList.tsx @@ -12,10 +12,19 @@ export function ToggleList() { const isActivated = CustomEditor.isBlockActive(editor, EditorNodeType.ToggleListBlock); const onClick = useCallback(() => { + let type = EditorNodeType.ToggleListBlock; + + if (isActivated) { + type = EditorNodeType.Paragraph; + } + CustomEditor.turnToBlock(editor, { - type: EditorNodeType.ToggleListBlock, + type, + data: { + collapsed: false, + }, }); - }, [editor]); + }, [editor, isActivated]); return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx index 00378334a7..b0df70e30e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/selection_toolbar/actions/underline/Underline.tsx @@ -1,11 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import ActionButton from '$app/components/editor/components/tools/selection_toolbar/actions/_shared/ActionButton'; import { useTranslation } from 'react-i18next'; -import { ReactEditor, useSlateStatic } from 'slate-react'; +import { useSlateStatic } from 'slate-react'; import { CustomEditor } from '$app/components/editor/command'; import { ReactComponent as UnderlineSvg } from '$app/assets/underline.svg'; import { EditorMarkFormat } from '$app/application/document/document.types'; -import { createHotkey, createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; export function Underline() { const { t } = useTranslation(); @@ -20,26 +20,6 @@ export function Underline() { }); }, [editor]); - useEffect(() => { - const editorDom = ReactEditor.toDOMNode(editor, editor); - const handleShortcut = (e: KeyboardEvent) => { - if (createHotkey(HOT_KEY_NAME.UNDERLINE)(e)) { - e.preventDefault(); - e.stopPropagation(); - CustomEditor.toggleMark(editor, { - key: EditorMarkFormat.Underline, - value: true, - }); - return; - } - }; - - editorDom.addEventListener('keydown', handleShortcut); - return () => { - editorDom.removeEventListener('keydown', handleShortcut); - }; - }, [editor]); - return ( div > .text-element { + text-align: left; justify-content: flex-start; } } .block-element.block-align-right { - > div > .text-element { + > div > .text-element { + text-align: right; justify-content: flex-end; - } } .block-element.block-align-center { > div > .text-element { + text-align: center; justify-content: center; } @@ -39,6 +39,15 @@ display: none !important; } +[role=textbox] { + .text-element { + &::selection { + @apply bg-transparent; + } + } +} + + span[data-slate-placeholder="true"]:not(.inline-block-content) { @apply text-text-placeholder; @@ -46,7 +55,6 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } - [role="textbox"] { ::selection { @apply bg-content-blue-100; @@ -55,6 +63,9 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { &::selection { @apply bg-transparent; } + &.selected { + @apply bg-content-blue-100; + } span { &::selection { @apply bg-content-blue-100; @@ -64,7 +75,6 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } - [data-dark-mode="true"] [role="textbox"]{ ::selection { background-color: #1e79a2; @@ -74,6 +84,9 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { &::selection { @apply bg-transparent; } + &.selected { + background-color: #1e79a2; + } span { &::selection { background-color: #1e79a2; @@ -84,8 +97,8 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { .text-content, [data-dark-mode="true"] .text-content { - &.empty-content { - @apply min-w-[1px]; + @apply min-w-[1px]; + &.empty-text { span { &::selection { @apply bg-transparent; @@ -101,19 +114,63 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { } .text-placeholder { - + @apply absolute left-[5px] transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap; &:after { - @apply text-text-placeholder absolute left-1.5 top-1/2 transform -translate-y-1/2 pointer-events-none select-none whitespace-nowrap; + @apply text-text-placeholder absolute top-0; content: (attr(placeholder)); } } -.has-start-icon > .text-placeholder { - &:after { - @apply left-[30px]; +.block-align-center { + .text-placeholder { + @apply left-[calc(50%+1px)]; + &:after { + @apply left-0; + } + } + .has-start-icon .text-placeholder { + @apply left-[calc(50%+13px)]; + &:after { + @apply left-0; + } + } + +} + +.block-align-left { + .text-placeholder { + &:after { + @apply left-0; + } + } + .has-start-icon .text-placeholder { + &:after { + @apply left-[24px]; + } } } +.block-align-right { + + .text-placeholder { + + @apply relative w-fit h-0 order-2; + &:after { + @apply relative w-fit top-1/2 left-[-6px]; + } + } + .text-content { + @apply order-1; + } + + .has-start-icon .text-placeholder { + &:after { + @apply left-[-6px]; + } + } +} + + .formula-inline { &.selected { @apply rounded bg-content-blue-100; @@ -122,7 +179,7 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { .bulleted-icon { &:after { - content: "•"; + content: attr(data-letter); } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/index.ts new file mode 100644 index 0000000000..bf2b09a1c3 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/index.ts @@ -0,0 +1,2 @@ +export * from './withCopy'; +export * from './withPasted'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/utils.ts new file mode 100644 index 0000000000..cb377fece4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/utils.ts @@ -0,0 +1,311 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Node, Location, Range, Path, Element, Text, Transforms, NodeEntry } from 'slate'; +import { EditorNodeType } from '$app/application/document/document.types'; +import { CustomEditor } from '$app/components/editor/command'; +import { LIST_TYPES } from '$app/components/editor/command/tab'; + +/** + * Rewrite the insertFragment function to avoid the empty node(doesn't have text node) in the fragment + + * @param editor + * @param fragment + * @param options + */ +export function insertFragment( + editor: ReactEditor, + fragment: (Text | Element)[], + options: { + at?: Location; + hanging?: boolean; + voids?: boolean; + } = {} +) { + Editor.withoutNormalizing(editor, () => { + const { hanging = false, voids = false } = options; + let { at = getDefaultInsertLocation(editor) } = options; + + if (!fragment.length) { + return; + } + + if (Range.isRange(at)) { + if (!hanging) { + at = Editor.unhangRange(editor, at, { voids }); + } + + if (Range.isCollapsed(at)) { + at = at.anchor; + } else { + const [, end] = Range.edges(at); + + if (!voids && Editor.void(editor, { at: end })) { + return; + } + + const pointRef = Editor.pointRef(editor, end); + + Transforms.delete(editor, { at }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + at = pointRef.unref()!; + } + } else if (Path.isPath(at)) { + at = Editor.start(editor, at); + } + + if (!voids && Editor.void(editor, { at })) { + return; + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const blockMatch = Editor.above(editor, { + match: (n) => Element.isElement(n) && Editor.isBlock(editor, n) && n.blockId !== undefined, + at, + voids, + })!; + const [block, blockPath] = blockMatch as NodeEntry; + + const isEmbedBlock = Element.isElement(block) && editor.isEmbed(block); + const isPageBlock = Element.isElement(block) && block.type === EditorNodeType.Page; + const isBlockStart = Editor.isStart(editor, at, blockPath); + const isBlockEnd = Editor.isEnd(editor, at, blockPath); + const isBlockEmpty = isBlockStart && isBlockEnd; + + if (isEmbedBlock) { + insertOnEmbedBlock(editor, fragment, blockPath); + return; + } + + if (isBlockEmpty && !isPageBlock) { + const node = fragment[0] as Element; + + if (block.type !== EditorNodeType.Paragraph) { + node.type = block.type; + node.data = { + ...(node.data || {}), + ...(block.data || {}), + }; + } + + insertOnEmptyBlock(editor, fragment, blockPath); + return; + } + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const fragmentRoot: Node = { + children: fragment, + }; + const [, firstPath] = Node.first(fragmentRoot, []); + const [, lastPath] = Node.last(fragmentRoot, []); + const sameBlock = Path.equals(firstPath.slice(0, -1), lastPath.slice(0, -1)); + + if (sameBlock) { + insertTexts( + editor, + isPageBlock + ? ({ + children: [ + { + text: CustomEditor.getNodeTextContent(fragmentRoot), + }, + ], + } as Node) + : fragmentRoot, + at + ); + return; + } + + const isListTypeBlock = LIST_TYPES.includes(block.type as EditorNodeType); + const [, ...blockChildren] = block.children; + + const blockEnd = editor.end([...blockPath, 0]); + const afterRange: Range = { anchor: at, focus: blockEnd }; + + const afterTexts = getTexts(editor, { + children: editor.fragment(afterRange), + } as Node) as (Text | Element)[]; + + Transforms.delete(editor, { at: afterRange }); + + const { startTexts, startChildren, middles } = getFragmentGroup(editor, fragment); + + insertNodes( + editor, + isPageBlock + ? [ + { + text: CustomEditor.getNodeTextContent({ + children: startTexts, + } as Node), + }, + ] + : startTexts, + { + at, + } + ); + + if (isPageBlock) { + insertNodes(editor, [...startChildren, ...middles], { + at: Path.next(blockPath), + select: true, + }); + } else { + if (blockChildren.length > 0) { + const path = [...blockPath, 1]; + + insertNodes(editor, [...startChildren, ...middles], { + at: path, + select: true, + }); + } else { + const newMiddle = [...middles]; + + if (isListTypeBlock) { + const path = [...blockPath, 1]; + + insertNodes(editor, startChildren, { + at: path, + select: newMiddle.length === 0, + }); + } else { + newMiddle.unshift(...startChildren); + } + + insertNodes(editor, newMiddle, { + at: Path.next(blockPath), + select: true, + }); + } + } + + const { selection } = editor; + + if (!selection) return; + + insertNodes(editor, afterTexts, { + at: selection, + }); + }); +} + +function getFragmentGroup(editor: ReactEditor, fragment: Node[]) { + const startTexts = []; + const startChildren = []; + const middles = []; + + const [firstNode, ...otherNodes] = fragment; + const [firstNodeText, ...firstNodeChildren] = (firstNode as Element).children as Element[]; + + startTexts.push(...firstNodeText.children); + startChildren.push(...firstNodeChildren); + + for (const node of otherNodes) { + if (Element.isElement(node) && node.blockId !== undefined) { + middles.push(node); + } + } + + return { + startTexts, + startChildren, + middles, + }; +} + +function getTexts(editor: ReactEditor, fragment: Node) { + const matches = []; + const matcher = ([n]: NodeEntry) => Text.isText(n) || (Element.isElement(n) && editor.isInline(n)); + + for (const entry of Node.nodes(fragment, { pass: matcher })) { + if (matcher(entry)) { + matches.push(entry[0]); + } + } + + return matches; +} + +function insertTexts(editor: ReactEditor, fragmentRoot: Node, at: Location) { + const matches = getTexts(editor, fragmentRoot); + + insertNodes(editor, matches, { + at, + select: true, + }); +} + +function insertOnEmptyBlock(editor: ReactEditor, fragment: Node[], blockPath: Path) { + editor.removeNodes({ + at: blockPath, + }); + + insertNodes(editor, fragment, { + at: blockPath, + select: true, + }); +} + +function insertOnEmbedBlock(editor: ReactEditor, fragment: Node[], blockPath: Path) { + insertNodes(editor, fragment, { + at: Path.next(blockPath), + select: true, + }); +} + +function insertNodes(editor: ReactEditor, nodes: Node[], options: { at?: Location; select?: boolean } = {}) { + try { + Transforms.insertNodes(editor, nodes, options); + } catch (e) { + try { + editor.move({ + distance: 1, + unit: 'line', + }); + } catch (e) { + // do nothing + } + } +} + +/** + * Copy Code from slate/src/utils/get-default-insert-location.ts + * Get the default location to insert content into the editor. + * By default, use the selection as the target location. But if there is + * no selection, insert at the end of the document since that is such a + * common use case when inserting from a non-selected state. + */ +export const getDefaultInsertLocation = (editor: Editor): Location => { + if (editor.selection) { + return editor.selection; + } else if (editor.children.length > 0) { + return Editor.end(editor, []); + } else { + return [0]; + } +}; + +export function transFragment(editor: ReactEditor, fragment: Node[]) { + // flatten the fragment to avoid the empty node(doesn't have text node) in the fragment + const flatMap = (node: Node): Node[] => { + const isInputElement = + !Editor.isEditor(node) && Element.isElement(node) && node.blockId !== undefined && !editor.isEmbed(node); + + if ( + isInputElement && + node.children?.length > 0 && + Element.isElement(node.children[0]) && + node.children[0].type !== EditorNodeType.Text + ) { + return node.children.flatMap((child) => flatMap(child)); + } + + return [node]; + }; + + const fragmentFlatMap = fragment?.flatMap(flatMap); + + // clone the node to avoid the duplicated block id + return fragmentFlatMap.map((item) => CustomEditor.cloneBlock(editor, item as Element)); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withCopy.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withCopy.ts new file mode 100644 index 0000000000..c0daab0a8f --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withCopy.ts @@ -0,0 +1,40 @@ +import { ReactEditor } from 'slate-react'; +import { Editor, Element, Range } from 'slate'; + +export function withCopy(editor: ReactEditor) { + const { setFragmentData } = editor; + + editor.setFragmentData = (...args) => { + if (!editor.selection) { + setFragmentData(...args); + return; + } + + // selection is collapsed and the node is an embed, we need to set the data manually + if (Range.isCollapsed(editor.selection)) { + const match = Editor.above(editor, { + match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, + }); + const node = match ? (match[0] as Element) : undefined; + + if (node && editor.isEmbed(node)) { + const fragment = editor.getFragment(); + + if (fragment.length > 0) { + const data = args[0]; + const string = JSON.stringify(fragment); + const encoded = window.btoa(encodeURIComponent(string)); + + const dom = ReactEditor.toDOMNode(editor, node); + + data.setData(`application/x-slate-fragment`, encoded); + data.setData(`text/html`, dom.innerHTML); + } + } + } + + setFragmentData(...args); + }; + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withPasted.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withPasted.ts new file mode 100644 index 0000000000..2266ff41c7 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/copyPasted/withPasted.ts @@ -0,0 +1,59 @@ +import { ReactEditor } from 'slate-react'; +import { insertFragment, transFragment } from './utils'; +import { convertBlockToJson } from '$app/application/document/document.service'; +import { InputType } from '@/services/backend'; +import { CustomEditor } from '$app/components/editor/command'; +import { Log } from '$app/utils/log'; + +export function withPasted(editor: ReactEditor) { + const { insertData } = editor; + + editor.insertData = (data) => { + const fragment = data.getData('application/x-slate-fragment'); + + if (fragment) { + insertData(data); + return; + } + + const html = data.getData('text/html'); + const text = data.getData('text/plain'); + + if (!html && !text) { + insertData(data); + return; + } + + void (async () => { + try { + const nodes = await convertBlockToJson(html, InputType.Html); + + const htmlTransNoText = nodes.every((node) => { + return CustomEditor.getNodeTextContent(node).length === 0; + }); + + if (!htmlTransNoText) { + return editor.insertFragment(nodes); + } + } catch (e) { + Log.warn('pasted html error', e); + // ignore + } + + if (text) { + const nodes = await convertBlockToJson(text, InputType.PlainText); + + editor.insertFragment(nodes); + return; + } + })(); + }; + + editor.insertFragment = (fragment, options = {}) => { + const clonedFragment = transFragment(editor, fragment); + + insertFragment(editor, clonedFragment, options); + }; + + return editor; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts index 7cfd550743..0292784ba5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/index.ts @@ -1,2 +1,2 @@ export * from './shortcuts.hooks'; -export * from './withShortcuts'; +export * from './withMarkdown'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/markdown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/markdown.ts new file mode 100644 index 0000000000..59ff0a8593 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/markdown.ts @@ -0,0 +1,172 @@ +export type MarkdownRegex = { + [key in MarkdownShortcuts]: { + pattern: RegExp; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: Record; + }[]; +}; + +export type TriggerHotKey = { + [key in MarkdownShortcuts]: string[]; +}; + +export enum MarkdownShortcuts { + Bold, + Italic, + StrikeThrough, + Code, + Equation, + /** block */ + Heading, + BlockQuote, + CodeBlock, + Divider, + /** list */ + BulletedList, + NumberedList, + TodoList, + ToggleList, +} + +const defaultMarkdownRegex: MarkdownRegex = { + [MarkdownShortcuts.Heading]: [ + { + pattern: /^#{1,6}$/, + }, + ], + [MarkdownShortcuts.Bold]: [ + { + pattern: /(\*\*|__)(.*?)(\*\*|__)$/, + }, + ], + [MarkdownShortcuts.Italic]: [ + { + pattern: /([*_])(.*?)([*_])$/, + }, + ], + [MarkdownShortcuts.StrikeThrough]: [ + { + pattern: /(~~)(.*?)(~~)$/, + }, + { + pattern: /(~)(.*?)(~)$/, + }, + ], + [MarkdownShortcuts.Code]: [ + { + pattern: /(`)(.*?)(`)$/, + }, + ], + [MarkdownShortcuts.Equation]: [ + { + pattern: /(\$)(.*?)(\$)$/, + data: { + formula: '', + }, + }, + ], + [MarkdownShortcuts.BlockQuote]: [ + { + pattern: /^([”“"])$/, + }, + ], + [MarkdownShortcuts.CodeBlock]: [ + { + pattern: /^(`{2,})$/, + data: { + language: 'json', + }, + }, + ], + [MarkdownShortcuts.Divider]: [ + { + pattern: /^(([-*]){2,})$/, + }, + ], + + [MarkdownShortcuts.BulletedList]: [ + { + pattern: /^([*\-+])$/, + }, + ], + [MarkdownShortcuts.NumberedList]: [ + { + pattern: /^(\d+)\.$/, + }, + ], + [MarkdownShortcuts.TodoList]: [ + { + pattern: /^(-)?\[ ]$/, + data: { + checked: false, + }, + }, + { + pattern: /^(-)?\[x]$/, + data: { + checked: true, + }, + }, + { + pattern: /^(-)?\[]$/, + data: { + checked: false, + }, + }, + ], + [MarkdownShortcuts.ToggleList]: [ + { + pattern: /^>$/, + data: { + collapsed: false, + }, + }, + ], +}; + +export const defaultTriggerChar: TriggerHotKey = { + [MarkdownShortcuts.Heading]: [' '], + [MarkdownShortcuts.Bold]: ['*', '_'], + [MarkdownShortcuts.Italic]: ['*', '_'], + [MarkdownShortcuts.StrikeThrough]: ['~'], + [MarkdownShortcuts.Code]: ['`'], + [MarkdownShortcuts.BlockQuote]: [' '], + [MarkdownShortcuts.CodeBlock]: ['`'], + [MarkdownShortcuts.Divider]: ['-', '*'], + [MarkdownShortcuts.Equation]: ['$'], + [MarkdownShortcuts.BulletedList]: [' '], + [MarkdownShortcuts.NumberedList]: [' '], + [MarkdownShortcuts.TodoList]: [' '], + [MarkdownShortcuts.ToggleList]: [' '], +}; + +export function isTriggerChar(char: string) { + return Object.values(defaultTriggerChar).some((trigger) => trigger.includes(char)); +} + +export function whatShortcutTrigger(char: string): MarkdownShortcuts[] | null { + const isTrigger = isTriggerChar(char); + + if (!isTrigger) { + return null; + } + + const shortcuts = Object.keys(defaultTriggerChar).map((key) => Number(key) as MarkdownShortcuts); + + return shortcuts.filter((shortcut) => defaultTriggerChar[shortcut].includes(char)); +} + +export function getRegex(shortcut: MarkdownShortcuts) { + return defaultMarkdownRegex[shortcut]; +} + +export function whatShortcutsMatch(text: string) { + const shortcuts = Object.keys(defaultMarkdownRegex).map((key) => Number(key) as MarkdownShortcuts); + + return shortcuts.filter((shortcut) => { + const regexes = defaultMarkdownRegex[shortcut]; + + return regexes.some((regex) => regex.pattern.test(text)); + }); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts index a22c5b7544..45d61f847c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/shortcuts.hooks.ts @@ -1,93 +1,346 @@ import { ReactEditor } from 'slate-react'; import { useCallback, KeyboardEvent } from 'react'; -import { EditorNodeType, TodoListNode, ToggleListNode } from '$app/application/document/document.types'; -import isHotkey from 'is-hotkey'; +import { EditorMarkFormat, EditorNodeType, ToggleListNode } from '$app/application/document/document.types'; import { getBlock } from '$app/components/editor/plugins/utils'; import { SOFT_BREAK_TYPES } from '$app/components/editor/plugins/constants'; import { CustomEditor } from '$app/components/editor/command'; +import { createHotkey, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { openUrl } from '$app/utils/open_url'; +import { Range } from 'slate'; +import { readText } from '@tauri-apps/api/clipboard'; +import { useDecorateDispatch } from '$app/components/editor/stores'; -/** - * Hotkeys shortcuts - * @description [getHotKeys] is defined in [hotkey.ts] - * - indent: Tab - * - outdent: Shift+Tab - * - split block: Enter - * - insert \n: Shift+Enter - * - toggle todo or toggle: Mod+Enter (toggle todo list or toggle list) - */ +function getScrollContainer(editor: ReactEditor) { + const editorDom = ReactEditor.toDOMNode(editor, editor); + + return editorDom.closest('.appflowy-scroll-container') as HTMLDivElement; +} export function useShortcuts(editor: ReactEditor) { + const { add: addDecorate } = useDecorateDispatch(); + + const formatLink = useCallback(() => { + const { selection } = editor; + + if (!selection || Range.isCollapsed(selection)) return; + + const isIncludeRoot = CustomEditor.selectionIncludeRoot(editor); + + if (isIncludeRoot) return; + + const isActivatedInline = CustomEditor.isInlineActive(editor); + + if (isActivatedInline) return; + + addDecorate({ + range: selection, + class_name: 'bg-content-blue-100 rounded', + type: 'link', + }); + }, [addDecorate, editor]); + const onKeyDown = useCallback( (e: KeyboardEvent) => { + const event = e.nativeEvent; + const hasEditableTarget = ReactEditor.hasEditableTarget(editor, event.target); + + if (!hasEditableTarget) return; + const node = getBlock(editor); - if (isHotkey('Escape', e)) { - e.preventDefault(); + const { selection } = editor; + const isExpanded = selection && Range.isExpanded(selection); - editor.deselect(); + switch (true) { + /** + * Select all: Mod+A + * Default behavior: Select all text in the editor + * Special case for select all in code block: Only select all text in code block + */ + case createHotkey(HOT_KEY_NAME.SELECT_ALL)(event): + if (node && node.type === EditorNodeType.CodeBlock) { + e.preventDefault(); + const path = ReactEditor.findPath(editor, node); - return; - } + editor.select(path); + } - if (isHotkey('Tab', e)) { - e.preventDefault(); - if (SOFT_BREAK_TYPES.includes(node?.type as EditorNodeType)) { - editor.insertText('\t'); - return; - } - - return CustomEditor.tabForward(editor); - } - - if (isHotkey('shift+Tab', e)) { - e.preventDefault(); - return CustomEditor.tabBackward(editor); - } - - if (isHotkey('Enter', e)) { - if (SOFT_BREAK_TYPES.includes(node?.type as EditorNodeType)) { + break; + /** + * Escape: Esc + * Default behavior: Deselect editor + */ + case createHotkey(HOT_KEY_NAME.ESCAPE)(event): + editor.deselect(); + break; + /** + * Indent block: Tab + * Default behavior: Indent block + */ + case createHotkey(HOT_KEY_NAME.INDENT_BLOCK)(event): e.preventDefault(); - editor.insertText('\n'); - return; - } - } + if (SOFT_BREAK_TYPES.includes(node?.type as EditorNodeType)) { + editor.insertText('\t'); + break; + } - if (isHotkey('shift+Enter', e) && node) { - e.preventDefault(); - if (SOFT_BREAK_TYPES.includes(node.type as EditorNodeType)) { - editor.splitNodes({ - always: true, + CustomEditor.tabForward(editor); + break; + /** + * Outdent block: Shift+Tab + * Default behavior: Outdent block + */ + case createHotkey(HOT_KEY_NAME.OUTDENT_BLOCK)(event): + e.preventDefault(); + CustomEditor.tabBackward(editor); + break; + /** + * Split block: Enter + * Default behavior: Split block + * Special case for soft break types: Insert \n + */ + case createHotkey(HOT_KEY_NAME.SPLIT_BLOCK)(event): + if (SOFT_BREAK_TYPES.includes(node?.type as EditorNodeType)) { + e.preventDefault(); + editor.insertText('\n'); + } + + break; + /** + * Insert soft break: Shift+Enter + * Default behavior: Insert \n + * Special case for soft break types: Split block + */ + case createHotkey(HOT_KEY_NAME.INSERT_SOFT_BREAK)(event): + e.preventDefault(); + if (node && SOFT_BREAK_TYPES.includes(node.type as EditorNodeType)) { + editor.splitNodes({ + always: true, + }); + } else { + editor.insertText('\n'); + } + + break; + /** + * Toggle todo: Shift+Enter + * Default behavior: Toggle todo + * Special case for toggle list block: Toggle collapse + */ + case createHotkey(HOT_KEY_NAME.TOGGLE_TODO)(event): + case createHotkey(HOT_KEY_NAME.TOGGLE_COLLAPSE)(event): + e.preventDefault(); + if (node && node.type === EditorNodeType.ToggleListBlock) { + CustomEditor.toggleToggleList(editor, node as ToggleListNode); + } else { + CustomEditor.toggleTodo(editor); + } + + break; + /** + * Backspace: Backspace / Shift+Backspace + * Default behavior: Delete backward + */ + case createHotkey(HOT_KEY_NAME.BACKSPACE)(event): + e.stopPropagation(); + break; + /** + * Open link: Alt + enter + * Default behavior: Open one link in selection + */ + case createHotkey(HOT_KEY_NAME.OPEN_LINK)(event): { + if (!isExpanded) break; + e.preventDefault(); + const links = CustomEditor.getLinks(editor); + + if (links.length === 0) break; + openUrl(links[0]); + break; + } + + /** + * Open links: Alt + Shift + enter + * Default behavior: Open all links in selection + */ + case createHotkey(HOT_KEY_NAME.OPEN_LINKS)(event): { + if (!isExpanded) break; + e.preventDefault(); + const links = CustomEditor.getLinks(editor); + + if (links.length === 0) break; + links.forEach((link) => openUrl(link)); + break; + } + + /** + * Extend line backward: Opt + Shift + right + * Default behavior: Extend line backward + */ + case createHotkey(HOT_KEY_NAME.EXTEND_LINE_BACKWARD)(event): + e.preventDefault(); + CustomEditor.extendLineBackward(editor); + break; + /** + * Extend line forward: Opt + Shift + left + */ + case createHotkey(HOT_KEY_NAME.EXTEND_LINE_FORWARD)(event): + e.preventDefault(); + CustomEditor.extendLineForward(editor); + break; + + /** + * Paste: Mod + Shift + V + * Default behavior: Paste plain text + */ + case createHotkey(HOT_KEY_NAME.PASTE_PLAIN_TEXT)(event): + e.preventDefault(); + void (async () => { + const text = await readText(); + + if (!text) return; + CustomEditor.insertPlainText(editor, text); + })(); + + break; + /** + * Highlight: Mod + Shift + H + * Default behavior: Highlight selected text + */ + case createHotkey(HOT_KEY_NAME.HIGH_LIGHT)(event): + e.preventDefault(); + CustomEditor.highlight(editor); + break; + /** + * Extend document backward: Mod + Shift + Up + * Don't prevent default behavior + * Default behavior: Extend document backward + */ + case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD)(event): + editor.collapse({ edge: 'start' }); + break; + /** + * Extend document forward: Mod + Shift + Down + * Don't prevent default behavior + * Default behavior: Extend document forward + */ + case createHotkey(HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD)(event): + editor.collapse({ edge: 'end' }); + break; + + /** + * Scroll to top: Home + * Default behavior: Scroll to top + */ + case createHotkey(HOT_KEY_NAME.SCROLL_TO_TOP)(event): { + const scrollContainer = getScrollContainer(editor); + + scrollContainer.scrollTo({ + top: 0, }); - } else { - editor.insertText('\n'); + break; } - return; - } + /** + * Scroll to bottom: End + * Default behavior: Scroll to bottom + */ + case createHotkey(HOT_KEY_NAME.SCROLL_TO_BOTTOM)(event): { + const scrollContainer = getScrollContainer(editor); - if (isHotkey('mod+Enter', e) && node) { - if (node.type === EditorNodeType.TodoListBlock) { + scrollContainer.scrollTo({ + top: scrollContainer.scrollHeight, + }); + break; + } + + /** + * Align left: Control + Shift + L + * Default behavior: Align left + */ + case createHotkey(HOT_KEY_NAME.ALIGN_LEFT)(event): e.preventDefault(); - CustomEditor.toggleTodo(editor, node as TodoListNode); - return; - } - - if (node.type === EditorNodeType.ToggleListBlock) { + CustomEditor.toggleAlign(editor, 'left'); + break; + /** + * Align center: Control + Shift + E + */ + case createHotkey(HOT_KEY_NAME.ALIGN_CENTER)(event): e.preventDefault(); - CustomEditor.toggleToggleList(editor, node as ToggleListNode); - return; - } - } + CustomEditor.toggleAlign(editor, 'center'); + break; + /** + * Align right: Control + Shift + R + */ + case createHotkey(HOT_KEY_NAME.ALIGN_RIGHT)(event): + e.preventDefault(); + CustomEditor.toggleAlign(editor, 'right'); + break; + /** + * Bold: Mod + B + */ + case createHotkey(HOT_KEY_NAME.BOLD)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Bold, + value: true, + }); + break; + /** + * Italic: Mod + I + */ + case createHotkey(HOT_KEY_NAME.ITALIC)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Italic, + value: true, + }); + break; + /** + * Underline: Mod + U + */ + case createHotkey(HOT_KEY_NAME.UNDERLINE)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Underline, + value: true, + }); + break; + /** + * Strikethrough: Mod + Shift + S / Mod + Shift + X + */ + case createHotkey(HOT_KEY_NAME.STRIKETHROUGH)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.StrikeThrough, + value: true, + }); + break; + /** + * Code: Mod + E + */ + case createHotkey(HOT_KEY_NAME.CODE)(event): + e.preventDefault(); + CustomEditor.toggleMark(editor, { + key: EditorMarkFormat.Code, + value: true, + }); + break; + /** + * Format link: Mod + K + */ + case createHotkey(HOT_KEY_NAME.FORMAT_LINK)(event): + formatLink(); + break; - if (isHotkey('shift+backspace', e)) { - e.preventDefault(); - e.stopPropagation(); + case createHotkey(HOT_KEY_NAME.FIND_REPLACE)(event): + console.log('find replace'); + break; - editor.deleteBackward('character'); - return; + default: + break; } }, - [editor] + [formatLink, editor] ); return { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdown.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdown.ts new file mode 100644 index 0000000000..fd7801204c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdown.ts @@ -0,0 +1,239 @@ +import { Range, Element, Editor, NodeEntry, Path } from 'slate'; +import { ReactEditor } from 'slate-react'; +import { + defaultTriggerChar, + getRegex, + MarkdownShortcuts, + whatShortcutsMatch, + whatShortcutTrigger, +} from '$app/components/editor/plugins/shortcuts/markdown'; +import { CustomEditor } from '$app/components/editor/command'; +import { EditorMarkFormat, EditorNodeType } from '$app/application/document/document.types'; +import isEqual from 'lodash-es/isEqual'; + +export const withMarkdown = (editor: ReactEditor) => { + const { insertText } = editor; + + editor.insertText = (char) => { + const { selection } = editor; + + insertText(char); + if (!selection || !Range.isCollapsed(selection)) { + return; + } + + const triggerShortcuts = whatShortcutTrigger(char); + + if (!triggerShortcuts) { + return; + } + + const match = CustomEditor.getBlock(editor); + const [node, path] = match as NodeEntry; + + let prevIsNumberedList = false; + + try { + const prevPath = Path.previous(path); + const prev = editor.node(prevPath) as NodeEntry; + + prevIsNumberedList = prev && prev[0].type === EditorNodeType.NumberedListBlock; + } catch (e) { + // do nothing + } + + const start = Editor.start(editor, path); + const beforeRange = { anchor: start, focus: selection.anchor }; + const beforeText = Editor.string(editor, beforeRange); + + const removeBeforeText = (beforeRange: Range) => { + editor.deleteBackward('character'); + editor.delete({ + at: beforeRange, + }); + }; + + const matchBlockShortcuts = whatShortcutsMatch(beforeText); + + for (const shortcut of matchBlockShortcuts) { + const block = whichBlock(shortcut, beforeText); + + // if the block shortcut is matched, remove the before text and turn to the block + // then return + if (block && defaultTriggerChar[shortcut].includes(char)) { + // Don't turn to the block condition + // 1. Heading should be able to co-exist with number list + if (block.type === EditorNodeType.NumberedListBlock && node.type === EditorNodeType.HeadingBlock) { + return; + } + + // 2. If the block is the same type, and data is the same + if (block.type === node.type && isEqual(block.data || {}, node.data || {})) { + return; + } + + // 3. If the block is number list, and the previous block is also number list + if (block.type === EditorNodeType.NumberedListBlock && prevIsNumberedList) { + return; + } + + removeBeforeText(beforeRange); + CustomEditor.turnToBlock(editor, block); + + return; + } + } + + // get the range that matches the mark shortcuts + const markRange = { + anchor: Editor.start(editor, selection.anchor.path), + focus: selection.focus, + }; + const rangeText = Editor.string(editor, markRange) + char; + + if (!rangeText) return; + + // inputting a character that is start of a mark + const isStartTyping = rangeText.indexOf(char) === rangeText.lastIndexOf(char); + + if (isStartTyping) return; + + // if the range text includes a double character mark, and the last one is not finished + const doubleCharNotFinish = + ['*', '_', '~'].includes(char) && + rangeText.indexOf(`${char}${char}`) > -1 && + rangeText.indexOf(`${char}${char}`) === rangeText.lastIndexOf(`${char}${char}`); + + if (doubleCharNotFinish) return; + + const matchMarkShortcuts = whatShortcutsMatch(rangeText); + + for (const shortcut of matchMarkShortcuts) { + const item = getRegex(shortcut).find((p) => p.pattern.test(rangeText)); + const execArr = item?.pattern?.exec(rangeText); + + const removeText = execArr ? execArr[0] : ''; + + const text = execArr ? execArr[2]?.replaceAll(char, '') : ''; + + if (text) { + const index = rangeText.indexOf(removeText); + const removeRange = { + anchor: { + path: markRange.anchor.path, + offset: markRange.anchor.offset + index, + }, + focus: { + path: markRange.anchor.path, + offset: markRange.anchor.offset + index + removeText.length, + }, + }; + + removeBeforeText(removeRange); + insertMark(editor, shortcut, text); + return; + } + } + }; + + return editor; +}; + +function whichBlock(shortcut: MarkdownShortcuts, beforeText: string) { + switch (shortcut) { + case MarkdownShortcuts.Heading: + return { + type: EditorNodeType.HeadingBlock, + data: { + level: beforeText.length, + }, + }; + case MarkdownShortcuts.CodeBlock: + return { + type: EditorNodeType.CodeBlock, + data: { + language: 'json', + }, + }; + case MarkdownShortcuts.BulletedList: + return { + type: EditorNodeType.BulletedListBlock, + data: {}, + }; + case MarkdownShortcuts.NumberedList: + return { + type: EditorNodeType.NumberedListBlock, + data: { + number: Number(beforeText.split('.')[0]) ?? 1, + }, + }; + case MarkdownShortcuts.TodoList: + return { + type: EditorNodeType.TodoListBlock, + data: { + checked: beforeText.includes('[x]'), + }, + }; + case MarkdownShortcuts.BlockQuote: + return { + type: EditorNodeType.QuoteBlock, + data: {}, + }; + case MarkdownShortcuts.Divider: + return { + type: EditorNodeType.DividerBlock, + data: {}, + }; + + case MarkdownShortcuts.ToggleList: + return { + type: EditorNodeType.ToggleListBlock, + data: { + collapsed: false, + }, + }; + + default: + return null; + } +} + +function insertMark(editor: ReactEditor, shortcut: MarkdownShortcuts, text: string) { + switch (shortcut) { + case MarkdownShortcuts.Bold: + case MarkdownShortcuts.Italic: + case MarkdownShortcuts.StrikeThrough: + case MarkdownShortcuts.Code: { + const textNode = { + text, + }; + const attributes = { + [MarkdownShortcuts.Bold]: { + [EditorMarkFormat.Bold]: true, + }, + [MarkdownShortcuts.Italic]: { + [EditorMarkFormat.Italic]: true, + }, + [MarkdownShortcuts.StrikeThrough]: { + [EditorMarkFormat.StrikeThrough]: true, + }, + [MarkdownShortcuts.Code]: { + [EditorMarkFormat.Code]: true, + }, + }; + + Object.assign(textNode, attributes[shortcut]); + + editor.insertNodes(textNode); + return; + } + + case MarkdownShortcuts.Equation: { + CustomEditor.insertFormula(editor, text); + return; + } + + default: + return null; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdownShortcuts.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdownShortcuts.ts deleted file mode 100644 index f49f4d9dda..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withMarkdownShortcuts.ts +++ /dev/null @@ -1,352 +0,0 @@ -import { ReactEditor } from 'slate-react'; -import { Editor, Range, Element as SlateElement, Transforms } from 'slate'; -import { EditorMarkFormat, EditorNodeType } from '$app/application/document/document.types'; -import { CustomEditor } from '$app/components/editor/command'; - -/** - * Markdown shortcuts - * @description - * - bold: **bold** or __bold__ - * - italic: *italic* or _italic_ - * - strikethrough: ~~strikethrough~~ or ~strikethrough~ - * - code: `code` - * - heading: # or ## or ### - * - bulleted list: * or - or + - * - number list: 1. or 2. or 3. - * - toggle list: > - * - quote: ” or “ or " - * - todo list: -[ ] or -[x] or -[] or [] or [x] or [ ] - * - code block: ``` - * - callout: [!TIP] or [!INFO] or [!WARNING] or [!DANGER] - * - divider: ---or*** - * - equation: $$formula$$ - */ - -const regexMap: Record< - string, - { - pattern: RegExp; - data?: Record; - }[] -> = { - [EditorNodeType.BulletedListBlock]: [ - { - pattern: /^([*\-+])$/, - }, - ], - [EditorNodeType.ToggleListBlock]: [ - { - pattern: /^>$/, - data: { - collapsed: false, - }, - }, - ], - [EditorNodeType.QuoteBlock]: [ - { - pattern: /^”$/, - }, - { - pattern: /^“$/, - }, - { - pattern: /^"$/, - }, - ], - [EditorNodeType.TodoListBlock]: [ - { - pattern: /^(-)?\[ ]$/, - data: { - checked: false, - }, - }, - { - pattern: /^(-)?\[x]$/, - data: { - checked: true, - }, - }, - { - pattern: /^(-)?\[]$/, - data: { - checked: false, - }, - }, - ], - [EditorNodeType.NumberedListBlock]: [ - { - pattern: /^(\d+)\.$/, - }, - ], - [EditorNodeType.HeadingBlock]: [ - { - pattern: /^#$/, - data: { - level: 1, - }, - }, - { - pattern: /^#{2}$/, - data: { - level: 2, - }, - }, - { - pattern: /^#{3}$/, - data: { - level: 3, - }, - }, - ], - [EditorNodeType.CodeBlock]: [ - { - pattern: /^(`{3,})$/, - data: { - language: 'json', - }, - }, - ], - [EditorNodeType.CalloutBlock]: [ - { - pattern: /^\[!TIP]$/, - data: { - icon: '💡', - }, - }, - { - pattern: /^\[!INFO]$/, - data: { - icon: 'ℹ️', - }, - }, - { - pattern: /^\[!WARNING]$/, - data: { - icon: '⚠️', - }, - }, - { - pattern: /^\[!DANGER]$/, - data: { - icon: '🚨', - }, - }, - ], - [EditorNodeType.DividerBlock]: [ - { - pattern: /^(([-*]){3,})$/, - }, - ], - [EditorNodeType.EquationBlock]: [ - { - pattern: /^\$\$(.*)\$\$$/, - data: { - formula: '', - }, - }, - ], -}; - -const blockCommands = [' ', '-', '`', '$', '*']; - -const CharToMarkTypeMap: Record = { - '**': EditorMarkFormat.Bold, - __: EditorMarkFormat.Bold, - '*': EditorMarkFormat.Italic, - _: EditorMarkFormat.Italic, - '~': EditorMarkFormat.StrikeThrough, - '~~': EditorMarkFormat.StrikeThrough, - '`': EditorMarkFormat.Code, -}; - -const inlineBlockCommands = ['*', '_', '~', '`']; -const doubleCharCommands = ['*', '_', '~']; - -const matchBlockShortcutType = (beforeText: string, endChar: string) => { - // end with divider char: - - if (endChar === '-' || endChar === '*') { - const dividerRegex = regexMap[EditorNodeType.DividerBlock][0]; - - return dividerRegex.pattern.test(beforeText + endChar) - ? { - type: EditorNodeType.DividerBlock, - data: {}, - } - : null; - } - - // end with code block char: ` - if (endChar === '`') { - const codeBlockRegex = regexMap[EditorNodeType.CodeBlock][0]; - - return codeBlockRegex.pattern.test(beforeText + endChar) - ? { - type: EditorNodeType.CodeBlock, - data: codeBlockRegex.data, - } - : null; - } - - if (endChar === '$') { - const equationBlockRegex = regexMap[EditorNodeType.EquationBlock][0]; - - const match = equationBlockRegex.pattern.exec(beforeText + endChar); - - const formula = match?.[1]; - - return equationBlockRegex.pattern.test(beforeText + endChar) - ? { - type: EditorNodeType.EquationBlock, - data: { - formula, - }, - } - : null; - } - - for (const [type, regexes] of Object.entries(regexMap)) { - for (const regex of regexes) { - if (regex.pattern.test(beforeText)) { - return { - type, - data: regex.data, - }; - } - } - } - - return null; -}; - -export const withMarkdownShortcuts = (editor: ReactEditor) => { - const { insertText } = editor; - - editor.insertText = (text) => { - if (CustomEditor.isCodeBlock(editor) || CustomEditor.selectionIncludeRoot(editor)) { - insertText(text); - return; - } - - const { selection } = editor; - - if (!selection || !Range.isCollapsed(selection)) { - insertText(text); - return; - } - - // block shortcuts - if (blockCommands.some((char) => text.endsWith(char))) { - const endChar = text.slice(-1); - const [match] = Editor.nodes(editor, { - match: (n) => !Editor.isEditor(n) && SlateElement.isElement(n) && n.type === EditorNodeType.Text, - }); - - if (!match) { - insertText(text); - return; - } - - const [, path] = match; - - const { anchor } = selection; - const start = Editor.start(editor, path); - const range = { anchor, focus: start }; - const beforeText = Editor.string(editor, range) + text.slice(0, -1); - - if (beforeText === undefined) { - insertText(text); - return; - } - - const matchItem = matchBlockShortcutType(beforeText, endChar); - - if (matchItem) { - const { type, data } = matchItem; - - Transforms.select(editor, range); - - if (!Range.isCollapsed(range)) { - Transforms.delete(editor); - } - - const newProperties: Partial = { - type, - data, - }; - - CustomEditor.turnToBlock(editor, newProperties); - - return; - } - } - - // inline shortcuts - // end with inline mark char: * or _ or ~ or ` - // eg: **bold** or *italic* or ~strikethrough~ or `code` or _italic_ or __bold__ or ~~strikethrough~~ - const keyword = inlineBlockCommands.find((char) => text.endsWith(char)); - - if (keyword !== undefined) { - const { focus } = selection; - const start = { - path: focus.path, - offset: 0, - }; - const range = { anchor: start, focus }; - - const rangeText = Editor.string(editor, range); - - if (!rangeText.includes(keyword)) { - insertText(text); - return; - } - - const fullText = rangeText + keyword; - - let matchChar = keyword; - - if (doubleCharCommands.includes(keyword)) { - const doubleKeyword = `${keyword}${keyword}`; - - if (rangeText.includes(doubleKeyword)) { - const match = fullText.match(new RegExp(`\\${keyword}{2}(.*)\\${keyword}{2}`)); - - if (!match) { - insertText(text); - return; - } - - matchChar = doubleKeyword; - } - } - - const markType = CharToMarkTypeMap[matchChar]; - - const startIndex = rangeText.lastIndexOf(matchChar); - const beforeText = rangeText.slice(startIndex + matchChar.length, matchChar.length > 1 ? -1 : undefined); - - if (!beforeText) { - insertText(text); - return; - } - - const anchor = { path: start.path, offset: start.offset + startIndex }; - - const at = { - anchor, - focus, - }; - - editor.select(at); - editor.addMark(markType, true); - editor.insertText(beforeText); - editor.collapse({ - edge: 'end', - }); - return; - } - - insertText(text); - }; - - return editor; -}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withShortcuts.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withShortcuts.ts deleted file mode 100644 index 42b37f2a0f..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/withShortcuts.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { ReactEditor } from 'slate-react'; -import { withMarkdownShortcuts } from '$app/components/editor/plugins/shortcuts/withMarkdownShortcuts'; - -export function withShortcuts(editor: ReactEditor) { - return withMarkdownShortcuts(editor); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts index 1421a3c93b..62e3ad945a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/utils.ts @@ -10,6 +10,12 @@ export function getHeadingCssProperty(level: number) { return 'text-2xl pt-[8px] pb-[6px] font-bold'; case 3: return 'text-xl pt-[4px] font-bold'; + case 4: + return 'text-lg pt-[4px] font-bold'; + case 5: + return 'text-base pt-[4px] font-bold'; + case 6: + return 'text-sm pt-[4px] font-bold'; default: return ''; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts index d73ebcb9c4..0bcd0965a9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockDelete.ts @@ -71,8 +71,13 @@ export function withBlockDelete(editor: ReactEditor) { }); } - // if the current node is not a paragraph, convert it to a paragraph - if (node.type !== EditorNodeType.Paragraph && node.type !== EditorNodeType.Page) { + // if the current node is not a paragraph, convert it to a paragraph(except code block and callout block) + if ( + ![EditorNodeType.Paragraph, EditorNodeType.CalloutBlock, EditorNodeType.CodeBlock].includes( + node.type as EditorNodeType + ) && + node.type !== EditorNodeType.Page + ) { CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph }); return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts index cbb1816db2..b6f8da0e56 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockInsertBreak.ts @@ -1,8 +1,9 @@ import { ReactEditor } from 'slate-react'; import { EditorNodeType } from '$app/application/document/document.types'; import { CustomEditor } from '$app/components/editor/command'; -import { Path } from 'slate'; +import { Path, Transforms } from 'slate'; import { YjsEditor } from '@slate-yjs/core'; +import { generateId } from '$app/components/editor/provider/utils/convert'; export function withBlockInsertBreak(editor: ReactEditor) { const { insertBreak } = editor; @@ -16,9 +17,9 @@ export function withBlockInsertBreak(editor: ReactEditor) { const isEmbed = editor.isEmbed(node); - if (isEmbed) { - const nextPath = Path.next(path); + const nextPath = Path.next(path); + if (isEmbed) { CustomEditor.insertEmptyLine(editor as ReactEditor & YjsEditor, nextPath); editor.select(nextPath); return; @@ -26,11 +27,63 @@ export function withBlockInsertBreak(editor: ReactEditor) { const type = node.type as EditorNodeType; + const isBeginning = CustomEditor.focusAtStartOfBlock(editor); + const isEmpty = CustomEditor.isEmptyText(editor, node); - // if the node is empty, convert it to a paragraph - if (isEmpty && type !== EditorNodeType.Paragraph && type !== EditorNodeType.Page) { - CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph }); + if (isEmpty) { + const depth = path.length; + let hasNextNode = false; + + try { + hasNextNode = Boolean(editor.node(nextPath)); + } catch (e) { + // do nothing + } + + // if the node is empty and the depth is greater than 1, tab backward + if (depth > 1 && !hasNextNode) { + CustomEditor.tabBackward(editor); + return; + } + + // if the node is empty, convert it to a paragraph + if (type !== EditorNodeType.Paragraph && type !== EditorNodeType.Page) { + CustomEditor.turnToBlock(editor, { type: EditorNodeType.Paragraph }); + return; + } + } else if (isBeginning) { + // insert line below the current block + const newNodeType = [ + EditorNodeType.TodoListBlock, + EditorNodeType.BulletedListBlock, + EditorNodeType.NumberedListBlock, + ].includes(type) + ? type + : EditorNodeType.Paragraph; + + Transforms.insertNodes( + editor, + { + type: newNodeType, + data: node.data ?? {}, + blockId: generateId(), + children: [ + { + type: EditorNodeType.Text, + textId: generateId(), + children: [ + { + text: '', + }, + ], + }, + ], + }, + { + at: path, + } + ); return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts index ee7489b8bf..1e9fc7f105 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withBlockPlugins.ts @@ -3,7 +3,7 @@ import { ReactEditor } from 'slate-react'; import { withBlockDelete } from '$app/components/editor/plugins/withBlockDelete'; import { withBlockInsertBreak } from '$app/components/editor/plugins/withBlockInsertBreak'; import { withSplitNodes } from '$app/components/editor/plugins/withSplitNodes'; -import { withPasted } from '$app/components/editor/plugins/withPasted'; +import { withPasted, withCopy } from '$app/components/editor/plugins/copyPasted'; import { withBlockMove } from '$app/components/editor/plugins/withBlockMove'; import { CustomEditor } from '$app/components/editor/command'; @@ -26,5 +26,5 @@ export function withBlockPlugins(editor: ReactEditor) { return !CustomEditor.isEmbedNode(element) && isEmpty(element); }; - return withBlockMove(withSplitNodes(withBlockInsertBreak(withBlockDelete(withPasted(editor))))); + return withPasted(withBlockMove(withSplitNodes(withBlockInsertBreak(withBlockDelete(withCopy(editor)))))); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withPasted.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withPasted.ts deleted file mode 100644 index 105f995a27..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withPasted.ts +++ /dev/null @@ -1,287 +0,0 @@ -import { ReactEditor } from 'slate-react'; -import { convertBlockToJson } from '$app/application/document/document.service'; -import { Editor, Element, NodeEntry, Path, Node, Text, Location, Range } from 'slate'; -import { EditorNodeType } from '$app/application/document/document.types'; -import { InputType } from '@/services/backend'; -import { CustomEditor } from '$app/components/editor/command'; -import { generateId } from '$app/components/editor/provider/utils/convert'; -import { LIST_TYPES } from '$app/components/editor/command/tab'; -import { Log } from '$app/utils/log'; - -export function withPasted(editor: ReactEditor) { - const { insertData, insertFragment, setFragmentData } = editor; - - editor.setFragmentData = (...args) => { - if (!editor.selection) { - setFragmentData(...args); - return; - } - - // selection is collapsed and the node is an embed, we need to set the data manually - if (Range.isCollapsed(editor.selection)) { - const match = Editor.above(editor, { - match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, - }); - const node = match ? (match[0] as Element) : undefined; - - if (node && editor.isEmbed(node)) { - const fragment = editor.getFragment(); - - if (fragment.length > 0) { - const data = args[0]; - const string = JSON.stringify(fragment); - const encoded = window.btoa(encodeURIComponent(string)); - - const dom = ReactEditor.toDOMNode(editor, node); - - data.setData(`application/x-slate-fragment`, encoded); - data.setData(`text/html`, dom.innerHTML); - } - } - } - - setFragmentData(...args); - }; - - editor.insertData = (data) => { - const fragment = data.getData('application/x-slate-fragment'); - - if (fragment) { - insertData(data); - return; - } - - const html = data.getData('text/html'); - const text = data.getData('text/plain'); - - if (!html && !text) { - insertData(data); - return; - } - - void (async () => { - try { - const nodes = await convertBlockToJson(html, InputType.Html); - - const htmlTransNoText = nodes.every((node) => { - return CustomEditor.getNodeTextContent(node).length === 0; - }); - - if (!htmlTransNoText) { - return editor.insertFragment(nodes); - } - } catch (e) { - Log.warn('pasted html error', e); - // ignore - } - - if (text) { - const nodes = await convertBlockToJson(text, InputType.PlainText); - - editor.insertFragment(nodes); - return; - } - })(); - }; - - editor.insertFragment = (fragment, options = {}) => { - Editor.withoutNormalizing(editor, () => { - const { at = getDefaultInsertLocation(editor) } = options; - - if (!fragment.length) { - return; - } - - if (Range.isRange(at) && !Range.isCollapsed(at)) { - editor.delete({ - unit: 'character', - }); - } - - const selection = editor.selection; - - if (!selection) return; - - const [node] = editor.node(selection); - const isText = Text.isText(node); - const parent = Editor.above(editor, { - at: selection, - match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, - }); - - if (isText && parent) { - const [parentNode, parentPath] = parent as NodeEntry; - const pastedNodeIsPage = parentNode.type === EditorNodeType.Page; - const pastedNodeIsNotList = !LIST_TYPES.includes(parentNode.type as EditorNodeType); - const clonedFragment = transFragment(editor, fragment); - - const [firstNode, ...otherNodes] = clonedFragment; - const lastNode = getLastNode(otherNodes[otherNodes.length - 1]); - const firstIsEmbed = editor.isEmbed(firstNode); - const insertNodes: Element[] = [...otherNodes]; - const needMoveChildren = parentNode.children.length > 1 && !pastedNodeIsPage && !pastedNodeIsNotList; - let moveStartIndex = 0; - - if (firstIsEmbed) { - insertNodes.unshift(firstNode); - } else { - // merge the first fragment node with the current text node - const [textNode, ...children] = firstNode.children as Element[]; - - const textElements = textNode.children; - - const end = Editor.end(editor, [...parentPath, 0]); - - // merge text node - editor.insertNodes(textElements, { - at: end, - select: true, - }); - - if (children.length > 0) { - if (pastedNodeIsPage || pastedNodeIsNotList) { - // lift the children of the first fragment node to current node - insertNodes.unshift(...children); - } else { - const lastChild = getLastNode(children[children.length - 1]); - - const lastIsEmbed = lastChild && editor.isEmbed(lastChild); - - // insert the children of the first fragment node to current node - editor.insertNodes(children, { - at: [...parentPath, 1], - select: !lastIsEmbed, - }); - - moveStartIndex += children.length; - } - } - } - - if (insertNodes.length === 0) return; - - // insert a new paragraph if the last node is an embed - if ((!lastNode && firstIsEmbed) || (lastNode && editor.isEmbed(lastNode))) { - insertNodes.push(generateNewParagraph()); - } - - const pastedPath = Path.next(parentPath); - - // insert the sibling of the current node - editor.insertNodes(insertNodes, { - at: pastedPath, - select: true, - }); - - if (!needMoveChildren) return; - - if (!editor.selection) return; - - // current node is the last node of the pasted fragment - const currentPath = editor.selection.anchor.path; - const current = editor.above({ - at: currentPath, - match: (n) => !Editor.isEditor(n) && Element.isElement(n) && n.blockId !== undefined, - }); - - if (!current) return; - - const [currentNode, currentNodePath] = current as NodeEntry; - - // split the operation into the next tick to avoid the wrong path - if (LIST_TYPES.includes(currentNode.type as EditorNodeType)) { - const length = currentNode.children.length; - - setTimeout(() => { - // move the children of the current node to the last node of the pasted fragment - for (let i = parentNode.children.length - 1; i > 0; i--) { - editor.moveNodes({ - at: [...parentPath, i + moveStartIndex], - to: [...currentNodePath, length], - }); - } - }, 0); - } else { - // if the current node is not a list, we need to move these children to the next path - setTimeout(() => { - const nextPath = Path.next(currentNodePath); - - for (let i = parentNode.children.length - 1; i > 0; i--) { - editor.moveNodes({ - at: [...parentPath, i + moveStartIndex], - to: nextPath, - }); - } - }, 0); - } - } else { - insertFragment(fragment); - return; - } - }); - }; - - return editor; -} - -export const getDefaultInsertLocation = (editor: Editor): Location => { - if (editor.selection) { - return editor.selection; - } else if (editor.children.length > 0) { - return Editor.end(editor, []); - } else { - return [0]; - } -}; - -export const generateNewParagraph = (): Element => ({ - type: EditorNodeType.Paragraph, - blockId: generateId(), - children: [ - { - type: EditorNodeType.Text, - textId: generateId(), - children: [{ text: '' }], - }, - ], -}); - -function getLastNode(node: Node): Element | undefined { - if (!Element.isElement(node) || node.blockId === undefined) return; - - if (Element.isElement(node) && node.blockId !== undefined && node.children.length > 0) { - const child = getLastNode(node.children[node.children.length - 1]); - - if (!child) { - return node; - } else { - return child; - } - } - - return node; -} - -function transFragment(editor: ReactEditor, fragment: Node[]) { - // flatten the fragment to avoid the empty node(doesn't have text node) in the fragment - const flatMap = (node: Node): Node[] => { - const isInputElement = - !Editor.isEditor(node) && Element.isElement(node) && node.blockId !== undefined && !editor.isEmbed(node); - - if ( - isInputElement && - node.children?.length > 0 && - Element.isElement(node.children[0]) && - node.children[0].type !== EditorNodeType.Text - ) { - return node.children.flatMap((child) => flatMap(child)); - } - - return [node]; - }; - - const fragmentFlatMap = fragment?.flatMap(flatMap); - - // clone the node to avoid the duplicated block id - return fragmentFlatMap.map((item) => CustomEditor.cloneBlock(editor, item as Element)); -} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts index 55c9b8b8f2..eee7dd92d0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/withSplitNodes.ts @@ -30,8 +30,6 @@ export function withSplitNodes(editor: ReactEditor) { const { splitNodes } = editor; editor.splitNodes = (...args) => { - const selection = editor.selection; - const isInsertBreak = args.length === 1 && JSON.stringify(args[0]) === JSON.stringify({ always: true }); if (!isInsertBreak) { @@ -39,6 +37,8 @@ export function withSplitNodes(editor: ReactEditor) { return; } + const selection = editor.selection; + const isCollapsed = selection && Range.isCollapsed(selection); if (!isCollapsed) { @@ -106,10 +106,14 @@ export function withSplitNodes(editor: ReactEditor) { Transforms.insertNodes(editor, newNode, { at: newNodePath, - select: true, }); + editor.select(newNodePath); + CustomEditor.removeMarks(editor); + editor.collapse({ + edge: 'start', + }); return; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts index 14c3b408df..026ee57222 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/action.test.ts @@ -13,6 +13,7 @@ describe('Transform events to actions', () => { let provider: Provider; beforeEach(() => { provider = new Provider(generateId()); + provider.initialDocument(true); provider.connect(); applyActions.mockClear(); }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts index f7823e2c9d..0937d265ed 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/__tests__/observe.test.ts @@ -11,6 +11,7 @@ describe('Provider connected', () => { beforeEach(() => { provider = new Provider(generateId()); + provider.initialDocument(true); provider.connect(); applyActions.mockClear(); }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts index ead982006c..727b33ec69 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/provider.ts @@ -13,10 +13,9 @@ export class Provider extends EventEmitter { dataClient: DataClient; // get origin data after document updated backupDoc: Y.Doc = new Y.Doc(); - constructor(public id: string, includeRoot?: boolean) { + constructor(public id: string) { super(); this.dataClient = new DataClient(id); - void this.initialDocument(includeRoot); this.document.on('update', this.documentUpdate); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts index c2d78c8e2a..447a8f95f9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/provider/utils/action.ts @@ -161,6 +161,8 @@ function blockOps2BlockActions( ids: [deletedId], }) ); + } else { + Log.error('blockOps2BlockActions', 'deletedId is not exist'); } } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx index 30b9822fa4..6da2ee96d0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/error/ErrorModal.tsx @@ -1,24 +1,22 @@ -import { InformationSvg } from '../_shared/svg/InformationSvg'; -import { CloseSvg } from '../_shared/svg/CloseSvg'; +import { ReactComponent as InformationSvg } from '$app/assets/information.svg'; +import { ReactComponent as CloseSvg } from '$app/assets/close.svg'; export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => { return (
-
- +
+

Oops.. something went wrong

{message}

diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.hooks.ts new file mode 100644 index 0000000000..807c1e6811 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.hooks.ts @@ -0,0 +1,54 @@ +import { useCallback } from 'react'; +import { createHotkey, HOT_KEY_NAME } from '$app/utils/hotkeys'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { currentUserActions, ThemeMode } from '$app_reducers/current-user/slice'; +import { UserService } from '$app/application/user/user.service'; +import { sidebarActions } from '$app_reducers/sidebar/slice'; + +export function useShortcuts() { + const dispatch = useAppDispatch(); + const userSettingState = useAppSelector((state) => state.currentUser.userSetting); + const { isDark } = userSettingState; + + const switchThemeMode = useCallback(() => { + const newSetting = { + themeMode: isDark ? ThemeMode.Light : ThemeMode.Dark, + isDark: !isDark, + }; + + dispatch(currentUserActions.setUserSetting(newSetting)); + void UserService.setAppearanceSetting({ + theme_mode: newSetting.themeMode, + }); + }, [dispatch, isDark]); + + const toggleSidebar = useCallback(() => { + dispatch(sidebarActions.toggleCollapse()); + }, [dispatch]); + + return useCallback( + (e: KeyboardEvent) => { + switch (true) { + /** + * Toggle theme: Mod+L + * Switch between light and dark theme + */ + case createHotkey(HOT_KEY_NAME.TOGGLE_THEME)(e): + switchThemeMode(); + break; + /** + * Toggle sidebar: Mod+. (period) + * Prevent the default behavior of the browser (Exit full screen) + * Collapse or expand the sidebar + */ + case createHotkey(HOT_KEY_NAME.TOGGLE_SIDEBAR)(e): + e.preventDefault(); + toggleSidebar(); + break; + default: + break; + } + }, + [toggleSidebar, switchThemeMode] + ); +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx index 14bc179189..509aa388cf 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx @@ -1,28 +1,42 @@ -import React, { ReactNode, useEffect } from 'react'; +import React, { ReactNode, useEffect, useMemo } from 'react'; import SideBar from '$app/components/layout/side_bar/SideBar'; import TopBar from '$app/components/layout/top_bar/TopBar'; import { useAppSelector } from '$app/stores/store'; import './layout.scss'; +import { AFScroller } from '../_shared/scroller'; +import { useNavigate } from 'react-router-dom'; +import { pageTypeMap } from '$app_reducers/pages/slice'; +import { useShortcuts } from '$app/components/layout/Layout.hooks'; function Layout({ children }: { children: ReactNode }) { const { isCollapsed, width } = useAppSelector((state) => state.sidebar); + const currentUser = useAppSelector((state) => state.currentUser); + const navigate = useNavigate(); + const { id: latestOpenViewId, layout } = useMemo( + () => + currentUser?.workspaceSetting?.latestView || { + id: undefined, + layout: undefined, + }, + [currentUser?.workspaceSetting?.latestView] + ); + + const onKeyDown = useShortcuts(); useEffect(() => { - const onKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Backspace' && e.target instanceof HTMLBodyElement) { - e.preventDefault(); - } - - if (e.key === 'Escape') { - e.preventDefault(); - } - }; - window.addEventListener('keydown', onKeyDown); return () => { window.removeEventListener('keydown', onKeyDown); }; - }, []); + }, [onKeyDown]); + + useEffect(() => { + if (latestOpenViewId) { + const pageType = pageTypeMap[layout]; + + navigate(`/page/${pageType}/${latestOpenViewId}`); + } + }, [latestOpenViewId, navigate, layout]); return ( <>
@@ -34,14 +48,15 @@ function Layout({ children }: { children: ReactNode }) { }} > -
{children} -
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx index e93bd1c07b..ec9e990cdb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/BreadCrumb.tsx @@ -3,23 +3,22 @@ import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcr import Breadcrumbs from '@mui/material/Breadcrumbs'; import Link from '@mui/material/Link'; import Typography from '@mui/material/Typography'; -import { Page, pageTypeMap } from '$app_reducers/pages/slice'; -import { useNavigate } from 'react-router-dom'; +import { Page } from '$app_reducers/pages/slice'; import { useTranslation } from 'react-i18next'; import { getPageIcon } from '$app/hooks/page.hooks'; +import { useAppDispatch } from '$app/stores/store'; +import { openPage } from '$app_reducers/pages/async_actions'; function Breadcrumb() { const { t } = useTranslation(); const { isTrash, pagePath, currentPage } = useLoadExpandedPages(); - const navigate = useNavigate(); + const dispatch = useAppDispatch(); const navigateToPage = useCallback( (page: Page) => { - const pageType = pageTypeMap[page.layout]; - - navigate(`/page/${pageType}/${page.id}`); + void dispatch(openPage(page.id)); }, - [navigate] + [dispatch] ); if (!currentPage) { @@ -35,9 +34,9 @@ function Breadcrumb() { {pagePath?.map((page: Page, index) => { if (index === pagePath.length - 1) { return ( -
-
{getPageIcon(page)}
- {page.name || t('menuAppHeader.defaultNewPageName')} +
+
{getPageIcon(page)}
+ {page.name.trim() || t('menuAppHeader.defaultNewPageName')}
); } @@ -54,7 +53,7 @@ function Breadcrumb() { >
{getPageIcon(page)}
- {page.name || t('document.title.placeholder')} + {page.name.trim() || t('menuAppHeader.defaultNewPageName')} ); })} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx index 0dfe7e51f3..87662a99bb 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/collapse_menu_button/CollapseMenuButton.tsx @@ -1,12 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { IconButton, Tooltip } from '@mui/material'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { sidebarActions } from '$app_reducers/sidebar/slice'; import { ReactComponent as ShowMenuIcon } from '$app/assets/show-menu.svg'; import { useTranslation } from 'react-i18next'; -import { getModifier } from '$app/utils/hotkeys'; -import isHotkey from 'is-hotkey'; +import { createHotKeyLabel, HOT_KEY_NAME } from '$app/utils/hotkeys'; function CollapseMenuButton() { const isCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); @@ -21,25 +20,11 @@ function CollapseMenuButton() { return (
{isCollapsed ? t('sideBar.openSidebar') : t('sideBar.closeSidebar')}
-
{`${getModifier()} + \\`}
+
{createHotKeyLabel(HOT_KEY_NAME.TOGGLE_SIDEBAR)}
); }, [isCollapsed, t]); - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (isHotkey('mod+\\', e)) { - e.preventDefault(); - handleClick(); - } - }; - - document.addEventListener('keydown', handleKeyDown); - return () => { - document.removeEventListener('keydown', handleKeyDown); - }; - }, [handleClick]); - return ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss index 5183e4010f..43f4f55892 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss @@ -32,10 +32,16 @@ .appflowy-scroll-container { &::-webkit-scrollbar { - width: 0px; + width: 0; } } +.appflowy-scrollbar-thumb-horizontal, .appflowy-scrollbar-thumb-vertical { + background-color: var(--scrollbar-thumb); + border-radius: 4px; + opacity: 60%; +} + .workspaces { ::-webkit-scrollbar { width: 0px; @@ -55,4 +61,21 @@ &:hover { background-color: rgba(156, 156, 156, 0.20); } -} \ No newline at end of file +} + +.theme-mode-item { + @apply relative flex h-[72px] w-[88px] cursor-pointer items-end justify-end rounded border hover:shadow; + background: linear-gradient(150.74deg, rgba(231, 231, 231, 0) 17.95%, #C5C5C5 95.51%); +} + +[data-dark-mode="true"] { + .theme-mode-item { + background: linear-gradient(150.74deg, rgba(128, 125, 125, 0) 17.95%, #4d4d4d 95.51%); + } +} + +.document-header { + .view-banner { + @apply items-center; + } +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts index f2c2164f8c..d43499e801 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.hooks.ts @@ -1,9 +1,9 @@ import { useCallback, useEffect } from 'react'; -import { pagesActions, pageTypeMap, parserViewPBToPage } from '$app_reducers/pages/slice'; +import { pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { FolderNotification, ViewLayoutPB } from '@/services/backend'; -import { useNavigate, useParams } from 'react-router-dom'; -import { updatePageName } from '$app_reducers/pages/async_actions'; +import { useParams } from 'react-router-dom'; +import { openPage, updatePageName } from '$app_reducers/pages/async_actions'; import { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service'; import { subscribeNotifications } from '$app/application/notification'; @@ -82,14 +82,10 @@ export function usePageActions(pageId: string) { const dispatch = useAppDispatch(); const params = useParams(); const currentPageId = params.id; - const navigate = useNavigate(); const onPageClick = useCallback(() => { - if (!page) return; - const pageType = pageTypeMap[page.layout]; - - navigate(`/page/${pageType}/${pageId}`); - }, [navigate, page, pageId]); + void dispatch(openPage(pageId)); + }, [dispatch, pageId]); const onAddPage = useCallback( async (layout: ViewLayoutPB) => { @@ -112,21 +108,19 @@ export function usePageActions(pageId: string) { ); dispatch(pagesActions.expandPage(pageId)); - const pageType = pageTypeMap[layout]; - - navigate(`/page/${pageType}/${newViewId}`); + await dispatch(openPage(newViewId)); }, - [dispatch, navigate, pageId] + [dispatch, pageId] ); const onDeletePage = useCallback(async () => { if (currentPageId === pageId) { - navigate(`/`); + dispatch(pagesActions.setTrashSnackbar(true)); } await deletePage(pageId); dispatch(pagesActions.deletePages([pageId])); - }, [dispatch, currentPageId, navigate, pageId]); + }, [dispatch, pageId, currentPageId]); const onDuplicatePage = useCallback(async () => { await duplicatePage(page); @@ -149,7 +143,5 @@ export function usePageActions(pageId: string) { } export function useSelectedPage(pageId: string) { - const id = useParams().id; - - return id === pageId; + return useParams().id === pageId; } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx index 448fdc441a..948aedcae2 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPageTitle.tsx @@ -76,7 +76,7 @@ function NestedPageTitle({ {pageIcon}
- {page?.name || t('menuAppHeader.defaultNewPageName')} + {page?.name.trim() || t('menuAppHeader.defaultNewPageName')}
e.stopPropagation()} className={'min:w-14 flex items-center justify-end px-2'}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx index 5e284a94be..639d5283e0 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/Resizer.tsx @@ -43,7 +43,7 @@ function Resizer() {
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx index 02e8bfb60b..5cdbfb125b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/SideBar.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef } from 'react'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; -import { AppflowyLogoDark } from '$app/components/_shared/svg/AppflowyLogoDark'; -import { AppflowyLogoLight } from '$app/components/_shared/svg/AppflowyLogoLight'; +import { ReactComponent as AppflowyLogoDark } from '$app/assets/dark-logo.svg'; +import { ReactComponent as AppflowyLogoLight } from '$app/assets/light-logo.svg'; import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; import Resizer from '$app/components/layout/side_bar/Resizer'; import UserInfo from '$app/components/layout/side_bar/UserInfo'; @@ -50,7 +50,11 @@ function SideBar() { >
- {isDark ? : } + {isDark ? ( + + ) : ( + + )}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx index b02620e88c..62763c670e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/side_bar/UserInfo.tsx @@ -1,11 +1,11 @@ import React, { useState } from 'react'; import { useAppSelector } from '$app/stores/store'; -import { Avatar, IconButton } from '@mui/material'; -import PersonOutline from '@mui/icons-material/PersonOutline'; -import UserSetting from '../user_setting/UserSetting'; +import { IconButton } from '@mui/material'; import { ReactComponent as SettingIcon } from '$app/assets/settings.svg'; import Tooltip from '@mui/material/Tooltip'; import { useTranslation } from 'react-i18next'; +import { SettingsDialog } from '$app/components/settings/SettingsDialog'; +import { ProfileAvatar } from '$app/components/_shared/avatar'; function UserInfo() { const currentUser = useAppSelector((state) => state.currentUser); @@ -16,19 +16,9 @@ function UserInfo() { return ( <>
-
- - {currentUser.displayName ? currentUser.displayName[0] : } - - {currentUser.displayName} +
+ + {currentUser.displayName}
@@ -43,7 +33,7 @@ function UserInfo() {
- setShowUserSetting(false)} /> + {showUserSetting && setShowUserSetting(false)} />} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/DeletePageSnackbar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/DeletePageSnackbar.tsx new file mode 100644 index 0000000000..f5638362b9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/DeletePageSnackbar.tsx @@ -0,0 +1,104 @@ +import React, { useEffect } from 'react'; +import { Alert, Snackbar } from '@mui/material'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { useParams } from 'react-router-dom'; +import { pagesActions } from '$app_reducers/pages/slice'; +import Slide, { SlideProps } from '@mui/material/Slide'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { useTrashActions } from '$app/components/trash/Trash.hooks'; +import { openPage } from '$app_reducers/pages/async_actions'; + +function SlideTransition(props: SlideProps) { + return ; +} + +function DeletePageSnackbar() { + const firstViewId = useAppSelector((state) => { + const workspaceId = state.workspace.currentWorkspaceId; + const children = workspaceId ? state.pages.relationMap[workspaceId] : undefined; + + if (!children) return null; + + return children[0]; + }); + + const showTrashSnackbar = useAppSelector((state) => state.pages.showTrashSnackbar); + const dispatch = useAppDispatch(); + const { onPutback, onDelete } = useTrashActions(); + const { id } = useParams(); + + const { t } = useTranslation(); + + useEffect(() => { + dispatch(pagesActions.setTrashSnackbar(false)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id]); + + const handleBack = () => { + if (firstViewId) { + void dispatch(openPage(firstViewId)); + } + }; + + const handleClose = (toBack = true) => { + dispatch(pagesActions.setTrashSnackbar(false)); + if (toBack) { + handleBack(); + } + }; + + const handleRestore = () => { + if (!id) return; + void onPutback(id); + handleClose(false); + }; + + const handleDelete = () => { + if (!id) return; + void onDelete([id]); + + if (!firstViewId) { + handleClose(false); + return; + } + + handleBack(); + }; + + return ( + + handleClose()} + severity='info' + variant='standard' + sx={{ + width: '100%', + '.MuiAlert-action': { + padding: 0, + }, + }} + > +
+ {t('deletePagePrompt.text')} + + +
+
+
+ ); +} + +export default DeletePageSnackbar; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx index 1ffae1eb85..d37d1bf060 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/MoreButton.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Drawer, IconButton } from '@mui/material'; -import { Details2Svg } from '$app/components/_shared/svg/Details2Svg'; +import { ReactComponent as Details2Svg } from '$app/assets/details.svg'; import Tooltip from '@mui/material/Tooltip'; import MoreOptions from '$app/components/layout/top_bar/MoreOptions'; import { useMoreOptionsConfig } from '$app/components/layout/top_bar/MoreOptions.hooks'; @@ -18,8 +18,8 @@ function MoreButton() { return ( <> - toggleDrawer(true)} className={'h-8 w-8 text-icon-primary'}> - + toggleDrawer(true)} className={'text-icon-primary'}> + toggleDrawer(false)}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx index fd7fe34ec7..173bf86cab 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/top_bar/TopBar.tsx @@ -2,12 +2,13 @@ import React from 'react'; import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; import { useAppSelector } from '$app/stores/store'; import Breadcrumb from '$app/components/layout/bread_crumb/BreadCrumb'; +import DeletePageSnackbar from '$app/components/layout/top_bar/DeletePageSnackbar'; function TopBar() { const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); return ( -
+
{sidebarIsCollapsed && (
@@ -18,6 +19,7 @@ function TopBar() {
+
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx deleted file mode 100644 index e28a467526..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/AppearanceSetting.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useCallback, useMemo } from 'react'; -import Select from '@mui/material/Select'; -import { Theme, ThemeMode, UserSetting } from '$app/stores/reducers/current-user/slice'; -import MenuItem from '@mui/material/MenuItem'; -import { useTranslation } from 'react-i18next'; - -function AppearanceSetting({ - themeMode = ThemeMode.System, - onChange, -}: { - theme?: Theme; - themeMode?: ThemeMode; - onChange: (setting: UserSetting) => void; -}) { - const { t } = useTranslation(); - - const themeModeOptions = useMemo( - () => [ - { - value: ThemeMode.Light, - content: t('settings.appearance.themeMode.light'), - }, - { - value: ThemeMode.Dark, - content: t('settings.appearance.themeMode.dark'), - }, - { - value: ThemeMode.System, - content: t('settings.appearance.themeMode.system'), - }, - ], - [t] - ); - - const renderSelect = useCallback( - ( - items: { - options: { value: ThemeMode | Theme; content: string }[]; - label: string; - value: ThemeMode | Theme; - onChange: (newValue: ThemeMode | Theme) => void; - }[] - ) => { - return items.map((item) => { - const { value, options, label, onChange } = item; - - return ( -
-
{label}
-
- -
-
- ); - }); - }, - [] - ); - - return ( -
- {renderSelect([ - { - options: themeModeOptions, - label: t('settings.appearance.themeMode.label'), - value: themeMode, - onChange: (newValue) => { - onChange({ - themeMode: newValue as ThemeMode, - isDark: - newValue === ThemeMode.Dark || - (newValue === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches), - }); - }, - }, - ])} -
- ); -} - -export default AppearanceSetting; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx deleted file mode 100644 index 81e7d067a4..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/LanguageSetting.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { useTranslation } from 'react-i18next'; -import Select from '@mui/material/Select'; -import { UserSetting } from '$app/stores/reducers/current-user/slice'; -import MenuItem from '@mui/material/MenuItem'; - -const languages = [ - { - key: 'ar-SA', - title: 'العربية', - }, - { key: 'ca-ES', title: 'Català' }, - { key: 'de-DE', title: 'Deutsch' }, - { key: 'en', title: 'English' }, - { key: 'es-VE', title: 'Español (Venezuela)' }, - { key: 'eu-ES', title: 'Español' }, - { key: 'fr-FR', title: 'Français' }, - { key: 'hu-HU', title: 'Magyar' }, - { key: 'id-ID', title: 'Bahasa Indonesia' }, - { key: 'it-IT', title: 'Italiano' }, - { key: 'ja-JP', title: '日本語' }, - { key: 'ko-KR', title: '한국어' }, - { key: 'pl-PL', title: 'Polski' }, - { key: 'pt-BR', title: 'Português' }, - { key: 'pt-PT', title: 'Português' }, - { key: 'ru-RU', title: 'Русский' }, - { key: 'sv', title: 'Svenska' }, - { key: 'th-TH', title: 'ไทย' }, - { key: 'tr-TR', title: 'Türkçe' }, - { key: 'zh-CN', title: '简体中文' }, - { key: 'zh-TW', title: '繁體中文' }, -]; - -function LanguageSetting({ - language = 'en', - onChange, -}: { - language?: string; - onChange: (setting: UserSetting) => void; -}) { - const { t, i18n } = useTranslation(); - - return ( -
-
-
{t('settings.menu.language')}
-
- -
-
-
- ); -} - -export default LanguageSetting; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx deleted file mode 100644 index 9da3cb8f74..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/Menu.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { useMemo } from 'react'; -import LanguageIcon from '@mui/icons-material/Language'; -import PaletteOutlined from '@mui/icons-material/PaletteOutlined'; -import { useTranslation } from 'react-i18next'; - -export enum MenuItem { - Appearance = 'Appearance', - Language = 'Language', -} - -function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem) => void; selected: MenuItem }) { - const { t } = useTranslation(); - - const options = useMemo(() => { - return [ - { - label: t('settings.menu.appearance'), - value: MenuItem.Appearance, - icon: , - }, - { - label: t('settings.menu.language'), - value: MenuItem.Language, - icon: , - }, - ]; - }, [t]); - - return ( -
- {options.map((option) => { - return ( -
{ - onSelect(option.value); - }} - className={`my-1 flex w-full cursor-pointer items-center justify-start rounded-md p-2 text-xs text-text-title ${ - selected === option.value ? 'bg-fill-list-hover' : 'hover:text-content-blue-300' - }`} - > -
{option.icon}
-
{option.label}
-
- ); - })} -
- ); -} - -export default UserSettingMenu; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/SettingPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/SettingPanel.tsx deleted file mode 100644 index c88fe1f2e3..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/SettingPanel.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React, { useMemo } from 'react'; -import { MenuItem } from './Menu'; -import AppearanceSetting from './AppearanceSetting'; -import LanguageSetting from './LanguageSetting'; - -import { UserSetting } from '$app/stores/reducers/current-user/slice'; - -function UserSettingPanel({ - selected, - userSettingState = {}, - onChange, -}: { - selected: MenuItem; - userSettingState?: UserSetting; - onChange: (setting: Partial) => void; -}) { - const { theme, themeMode, language } = userSettingState; - - const options = useMemo(() => { - return [ - { - value: MenuItem.Appearance, - content: , - }, - { - value: MenuItem.Language, - content: , - }, - ]; - }, [language, onChange, theme, themeMode]); - - const option = options.find((option) => option.value === selected); - - return
{option?.content}
; -} - -export default UserSettingPanel; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx deleted file mode 100644 index 7cfbac4a76..0000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/user_setting/UserSetting.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import Dialog from '@mui/material/Dialog'; -import DialogContent from '@mui/material/DialogContent'; -import DialogTitle from '@mui/material/DialogTitle'; -import Slide, { SlideProps } from '@mui/material/Slide'; -import UserSettingMenu, { MenuItem } from './Menu'; -import UserSettingPanel from './SettingPanel'; -import { UserSetting } from '$app/stores/reducers/current-user/slice'; -import { useAppDispatch, useAppSelector } from '$app/stores/store'; -import { currentUserActions } from '$app_reducers/current-user/slice'; -import { useTranslation } from 'react-i18next'; -import { UserService } from '$app/application/user/user.service'; - -const SlideTransition = React.forwardRef((props: SlideProps, ref) => { - return ; -}); - -function UserSettings({ open, onClose }: { open: boolean; onClose: () => void }) { - const userSettingState = useAppSelector((state) => state.currentUser.userSetting); - const dispatch = useAppDispatch(); - const { t } = useTranslation(); - const [selected, setSelected] = useState(MenuItem.Appearance); - const handleChange = useCallback( - (setting: Partial) => { - const newSetting = { ...userSettingState, ...setting }; - - dispatch(currentUserActions.setUserSetting(newSetting)); - const language = newSetting.language || 'en'; - - void UserService.setAppearanceSetting({ - theme: newSetting.theme, - theme_mode: newSetting.themeMode, - locale: { - language_code: language.split('-')[0], - country_code: language.split('-')[1], - }, - }); - }, - [dispatch, userSettingState] - ); - - return ( - e.stopPropagation()} - open={open} - TransitionComponent={SlideTransition} - keepMounted={false} - onClose={onClose} - > - {t('settings.title')} - - { - setSelected(selected); - }} - selected={selected} - /> - - - - ); -} - -export default UserSettings; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx index e89af36869..984ed6f67f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/TrashButton.tsx @@ -1,5 +1,5 @@ import React, { useCallback } from 'react'; -import { TrashSvg } from '$app/components/_shared/svg/TrashSvg'; +import { ReactComponent as TrashSvg } from '$app/assets/delete.svg'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { useDrag } from 'src/appflowy_app/components/_shared/drag_block'; @@ -34,9 +34,7 @@ function TrashButton() { selected ? 'bg-fill-list-active' : '' } ${isDraggingOver ? 'bg-fill-list-hover' : ''}`} > -
- -
+ {t('trash.text')}
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts index 7d77b12d69..86bca45ada 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.hooks.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice'; import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice'; @@ -6,33 +6,33 @@ import { subscribeNotifications } from '$app/application/notification'; import { FolderNotification, ViewLayoutPB } from '@/services/backend'; import * as workspaceService from '$app/application/folder/workspace.service'; import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service'; -import { useNavigate } from 'react-router-dom'; +import { openPage } from '$app_reducers/pages/async_actions'; export function useLoadWorkspaces() { const dispatch = useAppDispatch(); - const { workspaces, currentWorkspace } = useAppSelector((state) => state.workspace); + const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace); + + const currentWorkspace = useMemo(() => { + return workspaces.find((workspace) => workspace.id === currentWorkspaceId); + }, [workspaces, currentWorkspaceId]); const initializeWorkspaces = useCallback(async () => { const workspaces = await workspaceService.getWorkspaces(); - const currentWorkspace = await workspaceService.getCurrentWorkspace(); + + const currentWorkspaceId = await workspaceService.getCurrentWorkspace(); dispatch( workspaceActions.initWorkspaces({ workspaces, - currentWorkspace, + currentWorkspaceId, }) ); }, [dispatch]); - useEffect(() => { - void (async () => { - await initializeWorkspaces(); - })(); - }, [initializeWorkspaces]); - return { workspaces, currentWorkspace, + initializeWorkspaces, }; } @@ -80,6 +80,15 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { useEffect(() => { const unsubscribePromise = subscribeNotifications( { + [FolderNotification.DidUpdateWorkspace]: async (changeset) => { + dispatch( + workspaceActions.updateWorkspace({ + id: String(changeset.id), + name: changeset.name, + icon: changeset.icon_url, + }) + ); + }, [FolderNotification.DidUpdateWorkspaceViews]: async (changeset) => { const res = changeset.items; @@ -90,7 +99,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { ); return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); - }, [id, onChildPagesChanged]); + }, [dispatch, id, onChildPagesChanged]); return { openWorkspace, @@ -99,8 +108,7 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { } export function useWorkspaceActions(workspaceId: string) { - const navigate = useNavigate(); - + const dispatch = useAppDispatch(); const newPage = useCallback(async () => { const { id } = await createCurrentWorkspaceChildView({ name: '', @@ -108,8 +116,19 @@ export function useWorkspaceActions(workspaceId: string) { parent_view_id: workspaceId, }); - navigate(`/page/document/${id}`); - }, [navigate, workspaceId]); + dispatch( + pagesActions.addPage({ + page: { + id: id, + parentId: workspaceId, + layout: ViewLayoutPB.Document, + name: '', + }, + isLast: true, + }) + ); + void dispatch(openPage(id)); + }, [dispatch, workspaceId]); return { newPage, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx index 0035ba702f..24fc7be91e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/Workspace.tsx @@ -2,11 +2,11 @@ import React, { useState } from 'react'; import { WorkspaceItem } from '$app_reducers/workspace/slice'; import NestedViews from '$app/components/layout/workspace_manager/NestedPages'; import { useLoadWorkspace, useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks'; -import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; import { ReactComponent as AddIcon } from '$app/assets/add.svg'; import { IconButton } from '@mui/material'; import Tooltip from '@mui/material/Tooltip'; +import { WorkplaceAvatar } from '$app/components/_shared/avatar'; function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) { useLoadWorkspace(workspace); @@ -42,9 +42,22 @@ function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: bo className={'mt-2 flex h-[22px] w-full cursor-pointer select-none items-center justify-between px-4'} > - - {t('sideBar.personal')} - +
+ {!workspace.name ? ( + t('sideBar.personal') + ) : ( + <> + + {workspace.name} + + )} +
{showAdd && ( diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx index c6404d435c..083dd61ec3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceManager.tsx @@ -1,21 +1,32 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import NewPageButton from '$app/components/layout/workspace_manager/NewPageButton'; import { useLoadWorkspaces } from '$app/components/layout/workspace_manager/Workspace.hooks'; import Workspace from './Workspace'; import TrashButton from '$app/components/layout/workspace_manager/TrashButton'; +import { useAppSelector } from '@/appflowy_app/stores/store'; +import { LoginState } from '$app_reducers/current-user/slice'; +import { AFScroller } from '$app/components/_shared/scroller'; function WorkspaceManager() { - const { workspaces, currentWorkspace } = useLoadWorkspaces(); + const { workspaces, currentWorkspace, initializeWorkspaces } = useLoadWorkspaces(); + + const loginState = useAppSelector((state) => state.currentUser.loginState); + + useEffect(() => { + if (loginState === LoginState.Success || loginState === undefined) { + void initializeWorkspaces(); + } + }, [initializeWorkspaces, loginState]); return (
-
+
{workspaces.map((workspace) => ( ))}
-
+
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/Login.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Login.tsx new file mode 100644 index 0000000000..d5ecc4bc0c --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Login.tsx @@ -0,0 +1,22 @@ +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import Button from '@mui/material/Button'; +import { LoginButtonGroup } from '$app/components/auth/LoginButtonGroup'; + +export const Login = ({ onBack }: { onBack?: () => void }) => { + const { t } = useTranslation(); + + return ( +
+ + {t('button.login')} + +
+ + +
+
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/Settings.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Settings.tsx new file mode 100644 index 0000000000..1d9f3c0cd9 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/Settings.tsx @@ -0,0 +1,92 @@ +import { useMemo, useState } from 'react'; +import { Box, Tab, Tabs } from '@mui/material'; +import { useTranslation } from 'react-i18next'; +import { MyAccount } from '$app/components/settings/my_account'; +import { ReactComponent as AccountIcon } from '$app/assets/settings/account.svg'; +import { ReactComponent as WorkplaceIcon } from '$app/assets/settings/workplace.svg'; +import { Workplace } from '$app/components/settings/workplace'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value, index, ...other } = props; + + return ( + + ); +} + +export const Settings = ({ onForward }: { onForward: (route: SettingsRoutes) => void }) => { + const { t } = useTranslation(); + const [activeTab, setActiveTab] = useState(0); + + const tabOptions = useMemo(() => { + return [ + { + label: t('newSettings.myAccount.title'), + Icon: AccountIcon, + Component: MyAccount, + }, + { + label: t('newSettings.workplace.name'), + Icon: WorkplaceIcon, + Component: Workplace, + }, + ]; + }, [t]); + + const handleChangeTab = (event: React.SyntheticEvent, newValue: number) => { + setActiveTab(newValue); + }; + + return ( + + + {tabOptions.map((tab, index) => ( + + + {tab.label} +
+ } + onClick={() => setActiveTab(index)} + sx={{ '&.Mui-selected': { borderColor: 'transparent', backgroundColor: 'var(--fill-list-active)' } }} + /> + ))} + + {tabOptions.map((tab, index) => ( + + + + ))} + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/SettingsDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/SettingsDialog.tsx new file mode 100644 index 0000000000..b53f8a6002 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/SettingsDialog.tsx @@ -0,0 +1,108 @@ +/** + * @figmaUrl https://www.figma.com/file/MF5CWlOzBRuGHp45zAXyUH/Appflowy%3A-Desktop-Settings?type=design&node-id=100%3A2119&mode=design&t=4Wb0Zg5NOFO36kOf-1 + */ + +import Dialog, { DialogProps } from '@mui/material/Dialog'; +import { Settings } from '$app/components/settings/Settings'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import DialogTitle from '@mui/material/DialogTitle'; +import { IconButton } from '@mui/material'; +import { ReactComponent as CloseIcon } from '$app/assets/close.svg'; +import { useTranslation } from 'react-i18next'; +import { ReactComponent as UpIcon } from '$app/assets/up.svg'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; +import DialogContent from '@mui/material/DialogContent'; +import { Login } from '$app/components/settings/Login'; +import SwipeableViews from 'react-swipeable-views'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { currentUserActions, LoginState } from '$app_reducers/current-user/slice'; + +export const SettingsDialog = (props: DialogProps) => { + const dispatch = useAppDispatch(); + const [routes, setRoutes] = useState([]); + const loginState = useAppSelector((state) => state.currentUser.loginState); + const lastLoginStateRef = useRef(loginState); + const { t } = useTranslation(); + const handleForward = useCallback((route: SettingsRoutes) => { + setRoutes((prev) => { + return [...prev, route]; + }); + }, []); + + const handleBack = useCallback(() => { + setRoutes((prevState) => { + return prevState.slice(0, -1); + }); + dispatch(currentUserActions.resetLoginState()); + }, [dispatch]); + + const handleClose = useCallback(() => { + dispatch(currentUserActions.resetLoginState()); + props?.onClose?.({}, 'backdropClick'); + }, [dispatch, props]); + + const currentRoute = routes[routes.length - 1]; + + useEffect(() => { + if (lastLoginStateRef.current === LoginState.Loading && loginState === LoginState.Success) { + handleClose(); + return; + } + + lastLoginStateRef.current = loginState; + }, [loginState, handleClose]); + + return ( + { + if (e.key === 'Escape') { + e.preventDefault(); + } + }} + scroll={'paper'} + > + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/index.ts new file mode 100644 index 0000000000..0f0a2c23f4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/index.ts @@ -0,0 +1 @@ +export * from './my_account'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/AccountLogin.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/AccountLogin.tsx new file mode 100644 index 0000000000..05b375c920 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/AccountLogin.tsx @@ -0,0 +1,39 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import { Divider } from '@mui/material'; +import { DeleteAccount } from '$app/components/settings/my_account/DeleteAccount'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; +import { useAuth } from '$app/components/auth/auth.hooks'; + +export const AccountLogin = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => { + const { t } = useTranslation(); + const { currentUser, logout } = useAuth(); + + const isLocal = currentUser.isLocal; + + return ( + <> +
+ + {t('newSettings.myAccount.accountLogin')} + + + + +
+ + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccount.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccount.tsx new file mode 100644 index 0000000000..82a909180e --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccount.tsx @@ -0,0 +1,43 @@ +import { useTranslation } from 'react-i18next'; +import { useState } from 'react'; +import Typography from '@mui/material/Typography'; +import Button from '@mui/material/Button'; +import { DeleteAccountDialog } from '$app/components/settings/my_account/DeleteAccountDialog'; + +export const DeleteAccount = () => { + const { t } = useTranslation(); + + const [openDialog, setOpenDialog] = useState(false); + + return ( +
+
+ + {t('newSettings.myAccount.deleteAccount.title')} + + + {t('newSettings.myAccount.deleteAccount.subtitle')} + +
+
+ +
+ { + setOpenDialog(false); + }} + /> +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccountDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccountDialog.tsx new file mode 100644 index 0000000000..2f8cc37258 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/DeleteAccountDialog.tsx @@ -0,0 +1,50 @@ +import Dialog, { DialogProps } from '@mui/material/Dialog'; +import { useTranslation } from 'react-i18next'; +import DialogTitle from '@mui/material/DialogTitle'; +import { DialogActions, DialogContentText, IconButton } from '@mui/material'; +import Button from '@mui/material/Button'; +import DialogContent from '@mui/material/DialogContent'; +import { ReactComponent as CloseIcon } from '$app/assets/close.svg'; + +export const DeleteAccountDialog = (props: DialogProps) => { + const { t } = useTranslation(); + + const handleClose = () => { + props?.onClose?.({}, 'backdropClick'); + }; + + const handleOk = () => { + //123 + }; + + return ( + + {t('newSettings.myAccount.deleteAccount.dialogTitle')} + + {t('newSettings.myAccount.deleteAccount.dialogContent1')} + {t('newSettings.myAccount.deleteAccount.dialogContent2')} + + +
+ +
+
+ +
+
+ + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/MyAccount.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/MyAccount.tsx new file mode 100644 index 0000000000..b3a315994b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/MyAccount.tsx @@ -0,0 +1,24 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { Profile } from './Profile'; +import { AccountLogin } from './AccountLogin'; +import { Divider } from '@mui/material'; +import { SettingsRoutes } from '$app/components/settings/workplace/const'; + +export const MyAccount = ({ onForward }: { onForward?: (route: SettingsRoutes) => void }) => { + const { t } = useTranslation(); + + return ( +
+ + {t('newSettings.myAccount.title')} + + + {t('newSettings.myAccount.subtitle')} + + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/Profile.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/Profile.tsx new file mode 100644 index 0000000000..2ac672b0e5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/Profile.tsx @@ -0,0 +1,180 @@ +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import { IconButton, InputAdornment, OutlinedInput } from '@mui/material'; +import { useAppSelector } from '$app/stores/store'; +import React, { useState } from 'react'; +import { ReactComponent as CheckIcon } from '$app/assets/select-check.svg'; +import { ReactComponent as CloseIcon } from '$app/assets/close.svg'; +import { ReactComponent as EditIcon } from '$app/assets/edit.svg'; + +import Tooltip from '@mui/material/Tooltip'; +import { UserService } from '$app/application/user/user.service'; +import { notify } from '$app/components/_shared/notify'; +import { ProfileAvatar } from '$app/components/_shared/avatar'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker'; +import Button from '@mui/material/Button'; + +export const Profile = () => { + const { displayName, id } = useAppSelector((state) => state.currentUser); + const { t } = useTranslation(); + const [isEditing, setIsEditing] = useState(false); + const [newName, setNewName] = useState(displayName ?? 'Me'); + const [error, setError] = useState(false); + const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null); + const openEmojiPicker = Boolean(emojiPickerAnchor); + const handleSave = async () => { + setError(false); + if (!newName) { + setError(true); + return; + } + + if (newName === displayName) { + setIsEditing(false); + return; + } + + try { + await UserService.updateUserProfile({ + id, + name: newName, + }); + setIsEditing(false); + } catch { + setError(true); + notify.error(t('newSettings.myAccount.updateNameError')); + } + }; + + const handleEmojiSelect = async (emoji: string) => { + try { + await UserService.updateUserProfile({ + id, + icon_url: emoji, + }); + } catch { + notify.error(t('newSettings.myAccount.updateIconError')); + } + }; + + const handleCancel = () => { + setNewName(displayName ?? 'Me'); + setIsEditing(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void handleSave(); + } + + if (e.key === 'Escape') { + e.stopPropagation(); + e.preventDefault(); + handleCancel(); + } + }; + + return ( +
+ + {t('newSettings.myAccount.profileLabel')} + +
+ + +
+ {isEditing ? ( + setNewName(e.target.value)} + spellCheck={false} + autoFocus={true} + autoCorrect={'off'} + autoCapitalize={'off'} + fullWidth + endAdornment={ + +
+ + + + + + + + + + +
+
+ } + sx={{ + '&.MuiOutlinedInput-root': { + borderRadius: '8px', + }, + }} + placeholder={t('newSettings.myAccount.profileNamePlaceholder')} + value={newName} + /> + ) : ( + + {newName} + + setIsEditing(true)} size={'small'} className={'ml-1'}> + + + + + )} +
+
+ {openEmojiPicker && ( + { + setEmojiPickerAnchor(null); + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + { + setEmojiPickerAnchor(null); + }} + onEmojiSelect={handleEmojiSelect} + /> + + )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/index.ts new file mode 100644 index 0000000000..d923fcefce --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/my_account/index.ts @@ -0,0 +1 @@ +export * from './MyAccount'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Appearance.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Appearance.tsx new file mode 100644 index 0000000000..1dc8581dae --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Appearance.tsx @@ -0,0 +1,20 @@ +import { useTranslation } from 'react-i18next'; +import { ThemeModeSwitch } from '$app/components/settings/workplace/appearance/ThemeModeSwitch'; +import Typography from '@mui/material/Typography'; +import { Divider } from '@mui/material'; +import { LanguageSetting } from '$app/components/settings/workplace/appearance/LanguageSetting'; + +export const Appearance = () => { + const { t } = useTranslation(); + + return ( + <> + + {t('newSettings.workplace.appearance.name')} + + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Workplace.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Workplace.tsx new file mode 100644 index 0000000000..8af69eec51 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/Workplace.tsx @@ -0,0 +1,23 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { WorkplaceDisplay } from '$app/components/settings/workplace/WorkplaceDisplay'; +import { Divider } from '@mui/material'; +import { Appearance } from '$app/components/settings/workplace/Appearance'; + +export const Workplace = () => { + const { t } = useTranslation(); + + return ( +
+ + {t('newSettings.workplace.title')} + + + {t('newSettings.workplace.subtitle')} + + + + +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/WorkplaceDisplay.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/WorkplaceDisplay.tsx new file mode 100644 index 0000000000..3a71c5f070 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/WorkplaceDisplay.tsx @@ -0,0 +1,155 @@ +import { useTranslation } from 'react-i18next'; +import Typography from '@mui/material/Typography'; +import { Divider, OutlinedInput } from '@mui/material'; +import React, { useMemo, useState } from 'react'; +import Button from '@mui/material/Button'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { changeWorkspaceIcon, renameWorkspace } from '$app/application/folder/workspace.service'; +import { notify } from '$app/components/_shared/notify'; +import { WorkplaceAvatar } from '$app/components/_shared/avatar'; +import Popover from '@mui/material/Popover'; +import { PopoverCommonProps } from '$app/components/editor/components/tools/popover'; +import EmojiPicker from '$app/components/_shared/emoji_picker/EmojiPicker'; +import { workspaceActions } from '$app_reducers/workspace/slice'; +import debounce from 'lodash-es/debounce'; + +export const WorkplaceDisplay = () => { + const { t } = useTranslation(); + const isLocal = useAppSelector((state) => state.currentUser.isLocal); + const { workspaces, currentWorkspaceId } = useAppSelector((state) => state.workspace); + const workspace = useMemo( + () => workspaces.find((workspace) => workspace.id === currentWorkspaceId), + [workspaces, currentWorkspaceId] + ); + const [name, setName] = useState(workspace?.name ?? ''); + const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null); + const openEmojiPicker = Boolean(emojiPickerAnchor); + const dispatch = useAppDispatch(); + + const debounceUpdateWorkspace = useMemo(() => { + return debounce(async ({ id, name, icon }: { id: string; name?: string; icon?: string }) => { + if (!id || !name) return; + + if (icon) { + try { + await changeWorkspaceIcon(id, icon); + } catch { + notify.error(t('newSettings.workplace.updateIconError')); + } + } + + if (name) { + try { + await renameWorkspace(id, name); + } catch { + notify.error(t('newSettings.workplace.renameError')); + } + } + }, 500); + }, [t]); + + const handleSave = async () => { + if (!workspace || !name) return; + dispatch(workspaceActions.updateWorkspace({ id: workspace.id, name })); + + await debounceUpdateWorkspace({ id: workspace.id, name }); + }; + + const handleEmojiSelect = async (icon: string) => { + if (!workspace) return; + dispatch(workspaceActions.updateWorkspace({ id: workspace.id, icon })); + + await debounceUpdateWorkspace({ id: workspace.id, icon }); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.stopPropagation(); + e.preventDefault(); + void handleSave(); + } + }; + + return ( +
+ + {t('newSettings.workplace.workplaceName')} + +
+
+ setName(e.target.value)} + sx={{ + '&.MuiOutlinedInput-root': { + borderRadius: '8px', + }, + }} + placeholder={t('newSettings.workplace.workplaceNamePlaceholder')} + value={name} + /> +
+ +
+ + + {t('newSettings.workplace.workplaceIcon')} + + + {t('newSettings.workplace.workplaceIconSubtitle')} + + + {openEmojiPicker && ( + { + setEmojiPickerAnchor(null); + }} + anchorOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + > + { + setEmojiPickerAnchor(null); + }} + onEmojiSelect={handleEmojiSelect} + /> + + )} +
+ ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/LanguageSetting.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/LanguageSetting.tsx new file mode 100644 index 0000000000..41a42bd011 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/LanguageSetting.tsx @@ -0,0 +1,115 @@ +import Typography from '@mui/material/Typography'; +import { useTranslation } from 'react-i18next'; +import MenuItem from '@mui/material/MenuItem'; +import Select from '@mui/material/Select'; +import React, { useCallback } from 'react'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { currentUserActions } from '$app_reducers/current-user/slice'; +import { UserService } from '$app/application/user/user.service'; + +const languages = [ + { + key: 'ar-SA', + title: 'العربية', + }, + { key: 'ca-ES', title: 'Català' }, + { key: 'de-DE', title: 'Deutsch' }, + { key: 'en', title: 'English' }, + { key: 'es-VE', title: 'Español (Venezuela)' }, + { key: 'eu-ES', title: 'Español' }, + { key: 'fr-FR', title: 'Français' }, + { key: 'hu-HU', title: 'Magyar' }, + { key: 'id-ID', title: 'Bahasa Indonesia' }, + { key: 'it-IT', title: 'Italiano' }, + { key: 'ja-JP', title: '日本語' }, + { key: 'ko-KR', title: '한국어' }, + { key: 'pl-PL', title: 'Polski' }, + { key: 'pt-BR', title: 'Português' }, + { key: 'pt-PT', title: 'Português' }, + { key: 'ru-RU', title: 'Русский' }, + { key: 'sv', title: 'Svenska' }, + { key: 'th-TH', title: 'ไทย' }, + { key: 'tr-TR', title: 'Türkçe' }, + { key: 'zh-CN', title: '简体中文' }, + { key: 'zh-TW', title: '繁體中文' }, +]; + +export const LanguageSetting = () => { + const { t, i18n } = useTranslation(); + const userSettingState = useAppSelector((state) => state.currentUser.userSetting); + const dispatch = useAppDispatch(); + const selectedLanguage = userSettingState.language; + + const [hoverKey, setHoverKey] = React.useState(null); + + const handleChange = useCallback( + (language: string) => { + const newSetting = { ...userSettingState, language }; + + dispatch(currentUserActions.setUserSetting(newSetting)); + const newLanguage = newSetting.language || 'en'; + + void UserService.setAppearanceSetting({ + theme: newSetting.theme, + theme_mode: newSetting.themeMode, + locale: { + language_code: newLanguage.split('-')[0], + country_code: newLanguage.split('-')[1], + }, + }); + }, + [dispatch, userSettingState] + ); + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + } + }, []); + + return ( + <> + + {t('newSettings.workplace.appearance.language')} + + + + + ); +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/ThemeModeSwitch.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/ThemeModeSwitch.tsx new file mode 100644 index 0000000000..34fdb8e598 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/appearance/ThemeModeSwitch.tsx @@ -0,0 +1,93 @@ +import { useTranslation } from 'react-i18next'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; +import { useCallback, useMemo } from 'react'; +import { ThemeModePB } from '@/services/backend'; +import darkSrc from '$app/assets/settings/dark.png'; +import lightSrc from '$app/assets/settings/light.png'; +import { currentUserActions, ThemeMode } from '$app_reducers/current-user/slice'; +import { UserService } from '$app/application/user/user.service'; +import { ReactComponent as CheckCircle } from '$app/assets/settings/check_circle.svg'; + +export const ThemeModeSwitch = () => { + const { t } = useTranslation(); + const userSettingState = useAppSelector((state) => state.currentUser.userSetting); + const dispatch = useAppDispatch(); + + const selectedMode = userSettingState.themeMode; + const themeModes = useMemo(() => { + return [ + { + name: t('newSettings.workplace.appearance.themeMode.auto'), + value: ThemeModePB.System, + img: ( +
+ + +
+ ), + }, + { + name: t('newSettings.workplace.appearance.themeMode.light'), + value: ThemeModePB.Light, + img: , + }, + { + name: t('newSettings.workplace.appearance.themeMode.dark'), + value: ThemeModePB.Dark, + img: , + }, + ]; + }, [t]); + + const handleChange = useCallback( + (newValue: ThemeModePB) => { + const newSetting = { + ...userSettingState, + ...{ + themeMode: newValue, + isDark: + newValue === ThemeMode.Dark || + (newValue === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches), + }, + }; + + dispatch(currentUserActions.setUserSetting(newSetting)); + + void UserService.setAppearanceSetting({ + theme_mode: newSetting.themeMode, + }); + }, + [dispatch, userSettingState] + ); + + const renderThemeModeItem = useCallback( + (option: { name: string; value: ThemeModePB; img: JSX.Element }) => { + return ( +
handleChange(option.value)} + className={'flex cursor-pointer flex-col items-center gap-2'} + > +
+ {option.img} + +
+
{option.name}
+
+ ); + }, + [handleChange, selectedMode] + ); + + return
{themeModes.map((mode) => renderThemeModeItem(mode))}
; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/const.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/const.ts new file mode 100644 index 0000000000..075e2744a5 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/const.ts @@ -0,0 +1,3 @@ +export enum SettingsRoutes { + LOGIN = 'login', +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/index.ts b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/index.ts new file mode 100644 index 0000000000..a64592ac8b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/settings/workplace/index.ts @@ -0,0 +1 @@ +export * from './Workplace'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts index e98a846da0..b6748614b8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.hooks.ts @@ -39,6 +39,9 @@ export function useLoadTrash() { export function useTrashActions() { const [restoreAllDialogOpen, setRestoreAllDialogOpen] = useState(false); const [deleteAllDialogOpen, setDeleteAllDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + + const [deleteId, setDeleteId] = useState(''); const onClickRestoreAll = () => { setRestoreAllDialogOpen(true); @@ -51,9 +54,18 @@ export function useTrashActions() { const closeDialog = () => { setRestoreAllDialogOpen(false); setDeleteAllDialogOpen(false); + setDeleteDialogOpen(false); + }; + + const onClickDelete = (id: string) => { + setDeleteId(id); + setDeleteDialogOpen(true); }; return { + onClickDelete, + deleteDialogOpen, + deleteId, onPutback: putback, onDelete: deleteTrashItem, onDeleteAll: deleteAll, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx index 40f51d1fbf..f10848dc9b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx @@ -20,6 +20,9 @@ function Trash() { onRestoreAll, onDeleteAll, closeDialog, + deleteDialogOpen, + deleteId, + onClickDelete, } = useTrashActions(); const [hoverId, setHoverId] = useState(''); @@ -50,7 +53,7 @@ function Trash() { item={item} key={item.id} onPutback={onPutback} - onDelete={onDelete} + onDelete={onClickDelete} hoverId={hoverId} setHoverId={setHoverId} /> @@ -62,6 +65,7 @@ function Trash() { subtitle={t('trash.confirmRestoreAll.caption')} onOk={onRestoreAll} onClose={closeDialog} + okText={t('trash.restoreAll')} /> + onDelete([deleteId])} + onClose={closeDialog} + />
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx index 9d4bb15628..d266005612 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/TrashItem.tsx @@ -17,7 +17,7 @@ function TrashItem({ item: Trash; hoverId: string; onPutback: (id: string) => void; - onDelete: (ids: string[]) => void; + onDelete: (id: string) => void; }) { const { t } = useTranslation(); @@ -35,7 +35,9 @@ function TrashItem({ }} >
-
{item.name || t('document.title.placeholder')}
+
+ {item.name.trim() || t('menuAppHeader.defaultNewPageName')} +
{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}
{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}
- onDelete([item.id])}> + onDelete(item.id)}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts index 13ad581175..464b7428a3 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/current-user/slice.ts @@ -1,6 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { WorkspaceSettingPB } from '@/services/backend/models/flowy-folder/workspace'; import { ThemeModePB as ThemeMode } from '@/services/backend'; +import { Page, parserViewPBToPage } from '$app_reducers/pages/slice'; export { ThemeMode }; @@ -17,35 +18,55 @@ export enum Theme { Lavender = 'lavender', } +export enum LoginState { + Loading = 'loading', + Success = 'success', + Error = 'error', +} + +export interface UserWorkspaceSetting { + workspaceId: string; + latestView?: Page; + hasLatestView: boolean; +} + +export function parseWorkspaceSettingPBToSetting(workspaceSetting: WorkspaceSettingPB): UserWorkspaceSetting { + return { + workspaceId: workspaceSetting.workspace_id, + latestView: workspaceSetting.latest_view ? parserViewPBToPage(workspaceSetting.latest_view) : undefined, + hasLatestView: !!workspaceSetting.latest_view, + }; +} + export interface ICurrentUser { id?: number; + deviceId?: string; displayName?: string; email?: string; token?: string; + iconUrl?: string; isAuthenticated: boolean; - workspaceSetting?: WorkspaceSettingPB; + workspaceSetting?: UserWorkspaceSetting; userSetting: UserSetting; + isLocal: boolean; + loginState?: LoginState; } const initialState: ICurrentUser | null = { isAuthenticated: false, userSetting: {}, + isLocal: true, }; export const currentUserSlice = createSlice({ name: 'currentUser', initialState: initialState, reducers: { - checkUser: (state, action: PayloadAction>) => { - return { - ...state, - ...action.payload, - }; - }, updateUser: (state, action: PayloadAction>) => { return { ...state, ...action.payload, + loginState: LoginState.Success, }; }, logout: () => { @@ -57,6 +78,21 @@ export const currentUserSlice = createSlice({ ...action.payload, }; }, + + setLoginState: (state, action: PayloadAction) => { + state.loginState = action.payload; + }, + + resetLoginState: (state) => { + state.loginState = undefined; + }, + + setLatestView: (state, action: PayloadAction) => { + if (state.workspaceSetting) { + state.workspaceSetting.latestView = action.payload; + state.workspaceSetting.hasLatestView = true; + } + }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts index f401808e44..90014c1e7f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/async_actions.ts @@ -1,8 +1,9 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { RootState } from '$app/stores/store'; import { pagesActions } from '$app_reducers/pages/slice'; -import { movePage, updatePage } from '$app/application/folder/page.service'; +import { movePage, setLatestOpenedPage, updatePage } from '$app/application/folder/page.service'; import debounce from 'lodash-es/debounce'; +import { currentUserActions } from '$app_reducers/current-user/slice'; export const movePageThunk = createAsyncThunk( 'pages/movePage', @@ -91,3 +92,15 @@ export const updatePageName = createAsyncThunk( } } ); + +export const openPage = createAsyncThunk('pages/openPage', async (id: string, thunkAPI) => { + const { dispatch, getState } = thunkAPI; + const { pageMap } = (getState() as RootState).pages; + + const page = pageMap[id]; + + if (!page) return; + + dispatch(currentUserActions.setLatestView(page)); + await setLatestOpenedPage(id); +}); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts index 57fe941acc..dbf313ecc1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/pages/slice.ts @@ -56,6 +56,7 @@ export interface PageState { pageMap: Record; relationMap: Record; expandedIdMap: Record; + showTrashSnackbar: boolean; } export const initialState: PageState = { @@ -65,6 +66,7 @@ export const initialState: PageState = { acc[id] = true; return acc; }, {} as Record), + showTrashSnackbar: false, }; export const pagesSlice = createSlice({ @@ -201,6 +203,10 @@ export const pagesSlice = createSlice({ storeExpandedPageIds(ids); }, + + setTrashSnackbar(state, action: PayloadAction) { + state.showTrashSnackbar = action.payload; + }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts index 090382de70..d071de846e 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/workspace/slice.ts @@ -3,16 +3,17 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface WorkspaceItem { id: string; name: string; + icon?: string; } interface WorkspaceState { workspaces: WorkspaceItem[]; - currentWorkspace: WorkspaceItem | null; + currentWorkspaceId: string | null; } const initialState: WorkspaceState = { workspaces: [], - currentWorkspace: null, + currentWorkspaceId: null, }; export const workspaceSlice = createSlice({ @@ -23,11 +24,22 @@ export const workspaceSlice = createSlice({ state, action: PayloadAction<{ workspaces: WorkspaceItem[]; - currentWorkspace: WorkspaceItem | null; + currentWorkspaceId: string | null; }> ) => { return action.payload; }, + + updateWorkspace: (state, action: PayloadAction>) => { + const index = state.workspaces.findIndex((workspace) => workspace.id === action.payload.id); + + if (index !== -1) { + state.workspaces[index] = { + ...state.workspaces[index], + ...action.payload, + }; + } + }, }, }); diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/avatar.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/avatar.ts new file mode 100644 index 0000000000..a9a752c579 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/avatar.ts @@ -0,0 +1,26 @@ +export function stringToColor(string: string) { + let hash = 0; + let i; + + /* eslint-disable no-bitwise */ + for (i = 0; i < string.length; i += 1) { + hash = string.charCodeAt(i) + ((hash << 5) - hash); + } + + let color = '#'; + + for (i = 0; i < 3; i += 1) { + const value = (hash >> (i * 8)) & 0xff; + + color += `00${value.toString(16)}`.slice(-2); + } + /* eslint-enable no-bitwise */ + + return color; +} + +export function stringToShortName(string: string) { + const [firstName, lastName = ''] = string.split(' '); + + return `${firstName.charAt(0)}${lastName.charAt(0)}`; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts index 4861e4de2d..025c8c45ed 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/color.ts @@ -22,10 +22,29 @@ export const colorMap = { [ColorEnum.Blue]: 'var(--tint-blue)', }; +// Convert ARGB to RGBA +// Flutter uses ARGB, but CSS uses RGBA +function argbToRgba(color: string): string { + const hex = color.replace(/^#|0x/, ''); + + const hasAlpha = hex.length === 8; + + if (!hasAlpha) { + return color.replace('0x', '#'); + } + + const r = parseInt(hex.slice(2, 4), 16); + const g = parseInt(hex.slice(4, 6), 16); + const b = parseInt(hex.slice(6, 8), 16); + const a = hasAlpha ? parseInt(hex.slice(0, 2), 16) / 255 : 1; + + return `rgba(${r}, ${g}, ${b}, ${a})`; +} + export function renderColor(color: string) { if (colorMap[color as ColorEnum]) { return colorMap[color as ColorEnum]; } - return color.replace('0x', '#'); + return argbToRgba(color); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts index fab7f0612f..20aa05db27 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/hotkeys.ts @@ -14,6 +14,10 @@ export const getModifier = () => { }; export enum HOT_KEY_NAME { + LEFT = 'left', + RIGHT = 'right', + SELECT_ALL = 'select-all', + ESCAPE = 'escape', ALIGN_LEFT = 'align-left', ALIGN_CENTER = 'align-center', ALIGN_RIGHT = 'align-right', @@ -22,40 +26,109 @@ export enum HOT_KEY_NAME { UNDERLINE = 'underline', STRIKETHROUGH = 'strikethrough', CODE = 'code', + TOGGLE_TODO = 'toggle-todo', + TOGGLE_COLLAPSE = 'toggle-collapse', + INDENT_BLOCK = 'indent-block', + OUTDENT_BLOCK = 'outdent-block', + INSERT_SOFT_BREAK = 'insert-soft-break', + SPLIT_BLOCK = 'split-block', + BACKSPACE = 'backspace', + OPEN_LINK = 'open-link', + OPEN_LINKS = 'open-links', + EXTEND_LINE_BACKWARD = 'extend-line-backward', + EXTEND_LINE_FORWARD = 'extend-line-forward', + PASTE = 'paste', + PASTE_PLAIN_TEXT = 'paste-plain-text', + HIGH_LIGHT = 'high-light', + EXTEND_DOCUMENT_BACKWARD = 'extend-document-backward', + EXTEND_DOCUMENT_FORWARD = 'extend-document-forward', + SCROLL_TO_TOP = 'scroll-to-top', + SCROLL_TO_BOTTOM = 'scroll-to-bottom', + FORMAT_LINK = 'format-link', + FIND_REPLACE = 'find-replace', + /** + * Navigation + */ + TOGGLE_THEME = 'toggle-theme', + TOGGLE_SIDEBAR = 'toggle-sidebar', } const defaultHotKeys = { - [HOT_KEY_NAME.ALIGN_LEFT]: 'control+shift+l', - [HOT_KEY_NAME.ALIGN_CENTER]: 'control+shift+e', - [HOT_KEY_NAME.ALIGN_RIGHT]: 'control+shift+r', - [HOT_KEY_NAME.BOLD]: 'mod+b', - [HOT_KEY_NAME.ITALIC]: 'mod+i', - [HOT_KEY_NAME.UNDERLINE]: 'mod+u', - [HOT_KEY_NAME.STRIKETHROUGH]: 'mod+shift+s', - [HOT_KEY_NAME.CODE]: 'mod+shift+c', + [HOT_KEY_NAME.ALIGN_LEFT]: ['control+shift+l'], + [HOT_KEY_NAME.ALIGN_CENTER]: ['control+shift+e'], + [HOT_KEY_NAME.ALIGN_RIGHT]: ['control+shift+r'], + [HOT_KEY_NAME.BOLD]: ['mod+b'], + [HOT_KEY_NAME.ITALIC]: ['mod+i'], + [HOT_KEY_NAME.UNDERLINE]: ['mod+u'], + [HOT_KEY_NAME.STRIKETHROUGH]: ['mod+shift+s', 'mod+shift+x'], + [HOT_KEY_NAME.CODE]: ['mod+e'], + [HOT_KEY_NAME.TOGGLE_TODO]: ['mod+enter'], + [HOT_KEY_NAME.TOGGLE_COLLAPSE]: ['mod+enter'], + [HOT_KEY_NAME.SELECT_ALL]: ['mod+a'], + [HOT_KEY_NAME.ESCAPE]: ['esc'], + [HOT_KEY_NAME.INDENT_BLOCK]: ['tab'], + [HOT_KEY_NAME.OUTDENT_BLOCK]: ['shift+tab'], + [HOT_KEY_NAME.SPLIT_BLOCK]: ['enter'], + [HOT_KEY_NAME.INSERT_SOFT_BREAK]: ['shift+enter'], + [HOT_KEY_NAME.BACKSPACE]: ['backspace', 'shift+backspace'], + [HOT_KEY_NAME.OPEN_LINK]: ['opt+enter'], + [HOT_KEY_NAME.OPEN_LINKS]: ['opt+shift+enter'], + [HOT_KEY_NAME.EXTEND_LINE_BACKWARD]: ['opt+shift+left'], + [HOT_KEY_NAME.EXTEND_LINE_FORWARD]: ['opt+shift+right'], + [HOT_KEY_NAME.PASTE]: ['mod+v'], + [HOT_KEY_NAME.PASTE_PLAIN_TEXT]: ['mod+shift+v'], + [HOT_KEY_NAME.HIGH_LIGHT]: ['mod+shift+h'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_BACKWARD]: ['mod+shift+up'], + [HOT_KEY_NAME.EXTEND_DOCUMENT_FORWARD]: ['mod+shift+down'], + [HOT_KEY_NAME.SCROLL_TO_TOP]: ['home'], + [HOT_KEY_NAME.SCROLL_TO_BOTTOM]: ['end'], + [HOT_KEY_NAME.TOGGLE_THEME]: ['mod+shift+l'], + [HOT_KEY_NAME.TOGGLE_SIDEBAR]: ['mod+.'], + [HOT_KEY_NAME.FORMAT_LINK]: ['mod+k'], + [HOT_KEY_NAME.LEFT]: ['left'], + [HOT_KEY_NAME.RIGHT]: ['right'], + [HOT_KEY_NAME.FIND_REPLACE]: ['mod+f'], }; const replaceModifier = (hotkey: string) => { return hotkey.replace('mod', getModifier()).replace('control', 'ctrl'); }; -export const createHotkey = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { +/** + * Create a hotkey checker. + * @example trigger strike through when user press "Cmd + Shift + S" or "Cmd + Shift + X" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotkey = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { const keys = customHotKeys || defaultHotKeys; - const hotkey = keys[hotkeyName]; + const hotkeys = keys[hotkeyName]; return (event: KeyboardEvent) => { - return isHotkey(hotkey, event); + return hotkeys.some((hotkey) => { + return isHotkey(hotkey, event); + }); }; }; -export const createHotKeyLabel = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { +/** + * Create a hotkey label. + * eg. "Ctrl + B / ⌘ + B" + * @param hotkeyName + * @param customHotKeys + */ +export const createHotKeyLabel = (hotkeyName: HOT_KEY_NAME, customHotKeys?: Record) => { const keys = customHotKeys || defaultHotKeys; - const hotkey = replaceModifier(keys[hotkeyName]); + const hotkeys = keys[hotkeyName].map((key) => replaceModifier(key)); - return hotkey - .split('+') - .map((key) => { - return key === ' ' ? 'Space' : key.charAt(0).toUpperCase() + key.slice(1); - }) - .join(' + '); + return hotkeys + .map((hotkey) => + hotkey + .split('+') + .map((key) => { + return key === ' ' ? 'Space' : key.charAt(0).toUpperCase() + key.slice(1); + }) + .join(' + ') + ) + .join(' / '); }; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/list.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/list.ts new file mode 100644 index 0000000000..6e5d22ccda --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/list.ts @@ -0,0 +1,45 @@ +const romanMap: [number, string][] = [ + [1000, 'M'], + [900, 'CM'], + [500, 'D'], + [400, 'CD'], + [100, 'C'], + [90, 'XC'], + [50, 'L'], + [40, 'XL'], + [10, 'X'], + [9, 'IX'], + [5, 'V'], + [4, 'IV'], + [1, 'I'], +]; + +export function romanize(num: number): string { + let result = ''; + let nextNum = num; + + for (const [value, symbol] of romanMap) { + const count = Math.floor(nextNum / value); + + nextNum -= value * count; + result += symbol.repeat(count); + if (nextNum === 0) break; + } + + return result; +} + +export function letterize(num: number): string { + let nextNum = num; + let letters = ''; + + while (nextNum > 0) { + nextNum--; + const letter = String.fromCharCode((nextNum % 26) + 'a'.charCodeAt(0)); + + letters = letter + letters; + nextNum = Math.floor(nextNum / 26); + } + + return letters; +} diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts index badd5c60a3..94e2cf94d5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -56,7 +56,10 @@ export const getDesignTokens = (isDark: boolean): ThemeOptions => { }, outlinedInherit: { color: 'var(--text-title)', - borderColor: 'var(--line-divider)', + borderColor: 'var(--line-border)', + '&:hover': { + boxShadow: 'var(--shadow)', + }, }, }, }, diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts index 3fd9933a45..d854be5211 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/open_url.ts @@ -1,9 +1,14 @@ import { open as openWindow } from '@tauri-apps/api/shell'; -export const pattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})(\S*)*\/?(\?[=&\w.%-]*)?(#[\w.\-!~*'()]*)?$/; +const urlPattern = /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})(\S*)*\/?(\?[=&\w.%-]*)?(#[\w.\-!~*'()]*)?$/; +const ipPattern = /^(https?:\/\/)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d{1,5})?$/; + +export function isUrl(str: string) { + return urlPattern.test(str) || ipPattern.test(str); +} export function openUrl(str: string) { - if (pattern.test(str)) { + if (isUrl(str)) { const linkPrefix = ['http://', 'https://', 'file://', 'ftp://', 'ftps://', 'mailto:']; if (linkPrefix.some((prefix) => str.startsWith(prefix))) { diff --git a/frontend/appflowy_tauri/src/styles/variables/dark.variables.css b/frontend/appflowy_tauri/src/styles/variables/dark.variables.css index 9d8bb9decd..ca7544687b 100644 --- a/frontend/appflowy_tauri/src/styles/variables/dark.variables.css +++ b/frontend/appflowy_tauri/src/styles/variables/dark.variables.css @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 29 Jan 2024 03:52:24 GMT +* Generated on Tue, 19 Mar 2024 03:48:58 GMT * Generated from $pnpm css:variables */ @@ -36,7 +36,7 @@ --base-light-color-light-green: #ddffd6; --base-light-color-light-aqua: #defff1; --base-light-color-light-blue: #e1fbff; - --base-light-color-light-red: #ffe7ee; + --base-light-color-light-red: #ffdddd; --base-black-neutral-100: #252F41; --base-black-neutral-200: #313c51; --base-black-neutral-300: #3c4557; @@ -88,7 +88,7 @@ --fill-hover: #005174; --fill-toolbar: #0F111C; --fill-selector: #232b38; - --fill-list-active: #252F41; + --fill-list-active: #3c4557; --fill-list-hover: #005174; --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; @@ -116,4 +116,6 @@ --tint-aqua: #1B3849; --tint-orange: #5E3C3C; --shadow: 0px 0px 25px 0px rgba(0,0,0,0.3); + --scrollbar-track: #252F41; + --scrollbar-thumb: #3c4557; } \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/styles/variables/light.variables.css b/frontend/appflowy_tauri/src/styles/variables/light.variables.css index cdaaf791a5..26acc76f0a 100644 --- a/frontend/appflowy_tauri/src/styles/variables/light.variables.css +++ b/frontend/appflowy_tauri/src/styles/variables/light.variables.css @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 29 Jan 2024 03:52:24 GMT +* Generated on Tue, 19 Mar 2024 03:48:58 GMT * Generated from $pnpm css:variables */ @@ -36,7 +36,7 @@ --base-light-color-light-green: #ddffd6; --base-light-color-light-aqua: #defff1; --base-light-color-light-blue: #e1fbff; - --base-light-color-light-red: #ffe7ee; + --base-light-color-light-red: #ffdddd; --base-black-neutral-100: #252F41; --base-black-neutral-200: #313c51; --base-black-neutral-300: #3c4557; @@ -92,7 +92,7 @@ --fill-active: #e0f8ff; --fill-list-hover: #e0f8ff; --fill-list-active: #edeef2; - --content-blue-400: rgb(0, 188, 240); + --content-blue-400: #00bcf0; --content-blue-300: #52d1f4; --content-blue-600: #009fd1; --content-blue-100: #e0f8ff; @@ -119,4 +119,6 @@ --tint-orange: #ffefe3; --tint-yellow: #fff2cd; --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); + --scrollbar-thumb: #bdbdbd; + --scrollbar-track: #edeef2; } \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs index a3b36ef1c0..e9d8024320 100644 --- a/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs +++ b/frontend/appflowy_tauri/style-dictionary/tailwind/box-shadow.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 29 Jan 2024 03:52:24 GMT +* Generated on Tue, 19 Mar 2024 03:48:58 GMT * Generated from $pnpm css:variables */ diff --git a/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs index 1cd0a67ada..bfa25fa56f 100644 --- a/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs +++ b/frontend/appflowy_tauri/style-dictionary/tailwind/colors.cjs @@ -1,6 +1,6 @@ /** * Do not edit directly -* Generated on Mon, 29 Jan 2024 03:52:24 GMT +* Generated on Tue, 19 Mar 2024 03:48:58 GMT * Generated from $pnpm css:variables */ @@ -67,5 +67,9 @@ module.exports = { "lime": "var(--tint-lime)", "aqua": "var(--tint-aqua)", "orange": "var(--tint-orange)" + }, + "scrollbar": { + "track": "var(--scrollbar-track)", + "thumb": "var(--scrollbar-thumb)" } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/dark.json b/frontend/appflowy_tauri/style-dictionary/tokens/dark.json index ea97844f34..c67af7c9ec 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/dark.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/dark.json @@ -80,7 +80,7 @@ }, "list": { "active": { - "value": "{Base.black.neutral.100}", + "value": "{Base.black.neutral.300}", "type": "color" }, "hover": { @@ -207,5 +207,15 @@ "type": "innerShadow" }, "type": "boxShadow" + }, + "scrollbar": { + "track": { + "value": "{Base.black.neutral.100}", + "type": "color" + }, + "thumb": { + "value": "{Base.black.neutral.300}", + "type": "color" + } } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/style-dictionary/tokens/light.json b/frontend/appflowy_tauri/style-dictionary/tokens/light.json index 98dcb21505..173f3d35aa 100644 --- a/frontend/appflowy_tauri/style-dictionary/tokens/light.json +++ b/frontend/appflowy_tauri/style-dictionary/tokens/light.json @@ -219,5 +219,15 @@ "type": "dropShadow" }, "type": "boxShadow" + }, + "scrollbar": { + "thumb": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "track": { + "value": "{Base.Light.neutral.100}", + "type": "color" + } } } \ No newline at end of file diff --git a/frontend/appflowy_tauri/vite.config.ts b/frontend/appflowy_tauri/vite.config.ts index a0153b9276..b571cc40de 100644 --- a/frontend/appflowy_tauri/vite.config.ts +++ b/frontend/appflowy_tauri/vite.config.ts @@ -33,7 +33,7 @@ export default defineConfig({ }, }), ], - publicDir: '../appflowy_flutter/assets', + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` // prevent vite from obscuring rust errors clearScreen: false, diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 456875ee7d..96028067b9 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -191,12 +191,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -221,7 +215,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "bincode", @@ -232,8 +226,10 @@ dependencies = [ "serde_repr", "thiserror", "tokio", + "tsify", "url", "uuid", + "wasm-bindgen", ] [[package]] @@ -545,7 +541,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "again", "anyhow", @@ -555,8 +551,11 @@ dependencies = [ "brotli", "bytes", "chrono", + "client-websocket", "collab", "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", "database-entity", "futures-core", "futures-util", @@ -565,11 +564,8 @@ dependencies = [ "gotrue-entity", "governor", "mime", - "mime_guess", "parking_lot 0.12.1", "prost", - "realtime-entity", - "realtime-protocol", "reqwest", "scraper 0.17.1", "semver", @@ -585,10 +581,27 @@ dependencies = [ "url", "uuid", "wasm-bindgen-futures", - "websocket", "yrs", ] +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "cmd_lib" version = "1.9.3" @@ -618,7 +631,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-trait", @@ -634,6 +647,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "unicode-segmentation", "web-sys", "yrs", ] @@ -641,7 +655,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "collab", @@ -660,7 +674,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "bytes", @@ -675,7 +689,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "chrono", @@ -701,6 +715,7 @@ dependencies = [ "collab", "collab-entity", "collab-plugins", + "futures", "lib-infra", "parking_lot 0.12.1", "serde", @@ -712,7 +727,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-stream", @@ -747,10 +762,49 @@ dependencies = [ "yrs", ] +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "collab", + "serde", + "thiserror", + "tracing", + "yrs", +] + [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "collab", @@ -902,7 +956,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -947,7 +1001,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -1234,6 +1288,7 @@ dependencies = [ "collab-entity", "collab-integrate", "collab-plugins", + "dashmap", "flowy-codegen", "flowy-derive", "flowy-document-pub", @@ -1245,7 +1300,6 @@ dependencies = [ "indexmap", "lib-dispatch", "lib-infra", - "lru", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1400,6 +1454,7 @@ dependencies = [ "mime_guess", "parking_lot 0.12.1", "postgrest", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -1698,10 +1753,23 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "futures-util", @@ -1718,7 +1786,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -1771,10 +1839,6 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash", - "allocator-api2", -] [[package]] name = "heck" @@ -2052,7 +2116,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "reqwest", @@ -2272,15 +2336,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "lru" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2c024b41519440580066ba82aab04092b333e09066a5eb86c7c4890df31f22" -dependencies = [ - "hashbrown", -] - [[package]] name = "mac" version = "0.1.1" @@ -2781,7 +2836,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -2801,6 +2856,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" dependencies = [ + "phf_macros 0.11.2", "phf_shared 0.11.2", ] @@ -2868,6 +2924,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -3293,43 +3362,6 @@ dependencies = [ "bitflags 1.3.2", ] -[[package]] -name = "realtime-entity" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "anyhow", - "bincode", - "bytes", - "collab", - "collab-entity", - "database-entity", - "prost", - "prost-build", - "protoc-bin-vendored", - "realtime-protocol", - "serde", - "serde_json", - "serde_repr", - "thiserror", - "tokio-tungstenite", - "websocket", - "yrs", -] - -[[package]] -name = "realtime-protocol" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "anyhow", - "bincode", - "collab", - "serde", - "thiserror", - "yrs", -] - [[package]] name = "redox_syscall" version = "0.1.57" @@ -3690,6 +3722,17 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "serde_json" version = "1.0.111" @@ -3773,7 +3816,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -4311,6 +4354,31 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.48", +] + [[package]] name = "tungstenite" version = "0.20.1" @@ -4712,24 +4780,6 @@ version = "0.25.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" -[[package]] -name = "websocket" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "futures-channel", - "futures-util", - "http", - "httparse", - "js-sys", - "percent-encoding", - "thiserror", - "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wee_alloc" version = "0.4.5" @@ -5023,4 +5073,4 @@ dependencies = [ [[patch.unused]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index abd5e3752b..c575d44445 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -55,7 +55,7 @@ codegen-units = 1 # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" } # Please use the following script to update collab. # Working directory: frontend # @@ -65,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0be # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs b/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs index 4d64464d42..a4590ad34e 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs @@ -2,7 +2,7 @@ use crate::authenticate_user::AuthenticateUser; use crate::define::{user_profile_key, user_workspace_key, AF_USER_SESSION_KEY}; use af_persistence::store::{AppFlowyWASMStore, IndexddbStore}; use anyhow::Context; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab_entity::CollabType; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use collab_integrate::{CollabKVDB, MutexCollab}; @@ -10,7 +10,7 @@ use collab_user::core::{MutexUserAwareness, UserAwareness}; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; use flowy_user_pub::entities::{ - awareness_oid_from_user_uuid, AuthResponse, Authenticator, UserAuthResponse, UserProfile, + user_awareness_object_id, AuthResponse, Authenticator, UserAuthResponse, UserProfile, UserWorkspace, }; use flowy_user_pub::session::Session; @@ -86,10 +86,6 @@ impl UserManager { .save_auth_data(&response, &new_user_profile, &new_session) .await?; - if let Err(err) = self.initialize_user_awareness(&new_session).await { - error!("Failed to initialize user awareness: {:?}", err); - } - for callback in self.user_callbacks.iter() { if let Err(e) = callback .did_sign_up( @@ -133,20 +129,6 @@ impl UserManager { collab_builder.initialize(session.user_workspace.id.clone()); } - async fn initialize_user_awareness(&self, new_session: &Session) -> FlowyResult<()> { - let data = self - .cloud_services - .get_user_service()? - .get_user_awareness_doc_state(new_session.user_id) - .await?; - trace!("Get user awareness collab: {}", data.len()); - let collab = self - .collab_for_user_awareness(new_session, Arc::downgrade(&self.collab_db), data) - .await?; - MutexUserAwareness::new(UserAwareness::create(collab, None)); - Ok(()) - } - #[instrument(level = "info", skip_all, err)] async fn save_auth_data( &self, @@ -198,21 +180,21 @@ impl UserManager { async fn collab_for_user_awareness( &self, - session: &Session, + uid: i64, + object_id: &str, collab_db: Weak, - raw_data: CollabDocState, + raw_data: Vec, ) -> Result, FlowyError> { let collab_builder = self.collab_builder.upgrade().ok_or(FlowyError::new( ErrorCode::Internal, "Unexpected error: collab builder is not available", ))?; - let user_awareness_id = awareness_oid_from_user_uuid(&session.user_uuid); let collab = collab_builder .build( - session.user_id, - &user_awareness_id.to_string(), + uid, + object_id, CollabType::UserAwareness, - raw_data, + DocStateSource::FromDocState(raw_data), collab_db, CollabBuilderConfig::default().sync_enable(true), ) diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs index 335c16381f..6f3c71025a 100644 --- a/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/integrate/server.rs @@ -55,8 +55,8 @@ impl CollabCloudPluginProvider for ServerProviderWASM { CollabPluginProviderType::AppFlowyCloud } - fn get_plugins(&self, _context: CollabPluginProviderContext) -> Fut>> { - to_fut(async move { vec![] }) + fn get_plugins(&self, _context: CollabPluginProviderContext) -> Vec> { + vec![] } fn is_sync_enabled(&self) -> bool { diff --git a/frontend/appflowy_web_app/.eslintignore b/frontend/appflowy_web_app/.eslintignore new file mode 100644 index 0000000000..7827beec6c --- /dev/null +++ b/frontend/appflowy_web_app/.eslintignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +src-tauri/ +.eslintrc.cjs +tsconfig.json +**/backend/** \ No newline at end of file diff --git a/frontend/appflowy_web_app/.eslintignore.web b/frontend/appflowy_web_app/.eslintignore.web new file mode 100644 index 0000000000..d13ba84467 --- /dev/null +++ b/frontend/appflowy_web_app/.eslintignore.web @@ -0,0 +1,6 @@ +node_modules/ +dist/ +src-tauri/ +.eslintrc.cjs +tsconfig.json +src/application/services/tauri-services/ \ No newline at end of file diff --git a/frontend/appflowy_web_app/.eslintrc.cjs b/frontend/appflowy_web_app/.eslintrc.cjs new file mode 100644 index 0000000000..ff6f405885 --- /dev/null +++ b/frontend/appflowy_web_app/.eslintrc.cjs @@ -0,0 +1,73 @@ +module.exports = { + // https://eslint.org/docs/latest/use/configure/configuration-files + env: { + browser: true, + es6: true, + node: true, + }, + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], + parser: '@typescript-eslint/parser', + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + tsconfigRootDir: __dirname, + extraFileExtensions: ['.json'], + }, + plugins: ['@typescript-eslint', 'react-hooks'], + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + '@typescript-eslint/adjacent-overload-signatures': 'error', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-namespace': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'error', + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/prefer-for-of': 'error', + '@typescript-eslint/triple-slash-reference': 'error', + '@typescript-eslint/unified-signatures': 'error', + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'off', + 'constructor-super': 'error', + eqeqeq: ['error', 'always'], + 'no-cond-assign': 'error', + 'no-duplicate-case': 'error', + 'no-duplicate-imports': 'error', + 'no-empty': [ + 'error', + { + allowEmptyCatch: true, + }, + ], + 'no-invalid-this': 'error', + 'no-new-wrappers': 'error', + 'no-param-reassign': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unsafe-finally': 'error', + 'no-unused-labels': 'error', + 'no-var': 'error', + 'no-void': 'off', + 'prefer-const': 'error', + 'prefer-spread': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + }, + ], + 'padding-line-between-statements': [ + 'error', + { blankLine: 'always', prev: ['const', 'let', 'var'], next: '*' }, + { blankLine: 'any', prev: ['const', 'let', 'var'], next: ['const', 'let', 'var'] }, + { blankLine: 'always', prev: 'import', next: '*' }, + { blankLine: 'any', prev: 'import', next: 'import' }, + { blankLine: 'always', prev: 'block-like', next: '*' }, + { blankLine: 'always', prev: 'block', next: '*' }, + + ], + }, + ignorePatterns: ['src/**/*.test.ts', '**/__tests__/**/*.json', 'package.json'], +}; diff --git a/frontend/appflowy_web_app/.gitignore b/frontend/appflowy_web_app/.gitignore new file mode 100644 index 0000000000..474a3a975e --- /dev/null +++ b/frontend/appflowy_web_app/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist/** +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +src/@types/translations/*.json + + +src/application/services/tauri-services/backend/models/ +src/application/services/tauri-services/backend/events/ + +.env \ No newline at end of file diff --git a/frontend/appflowy_web_app/.prettierrc.cjs b/frontend/appflowy_web_app/.prettierrc.cjs new file mode 100644 index 0000000000..f283db53a2 --- /dev/null +++ b/frontend/appflowy_web_app/.prettierrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + arrowParens: 'always', + bracketSpacing: true, + endOfLine: 'lf', + htmlWhitespaceSensitivity: 'css', + insertPragma: false, + jsxBracketSameLine: false, + jsxSingleQuote: true, + printWidth: 121, + plugins: [require('prettier-plugin-tailwindcss')], + proseWrap: 'preserve', + quoteProps: 'as-needed', + requirePragma: false, + semi: true, + singleQuote: true, + tabWidth: 2, + trailingComma: 'es5', + useTabs: false, + vueIndentScriptAndStyle: false, +}; diff --git a/frontend/appflowy_web_app/README.md b/frontend/appflowy_web_app/README.md new file mode 100644 index 0000000000..fa55e54fbc --- /dev/null +++ b/frontend/appflowy_web_app/README.md @@ -0,0 +1,201 @@ +
+ +

AppFlowy Web Project

+ +
Welcome to the AppFlowy Web Project, a robust and versatile platform designed to bring the innovative features of +AppFlowy to the web. This project uniquely supports running as a desktop application via Tauri, and offers web +interfaces powered by WebAssembly (WASM). Dive into an exceptional development experience with high performance and +extensive capabilities.
+ +
+ +## 🐑 Features + +- **Cross-Platform Compatibility**: Seamlessly run on desktop environments with Tauri, and on any web browser through + WASM. +- **High Performance**: Leverage the speed and efficiency of WebAssembly for your web interfaces. +- **Tauri Integration**: Build lightweight, secure, and efficient desktop applications. +- **Flexible Development**: Utilize a wide range of AppFlowy's functionalities in your web or desktop projects. + +## 🚀 Getting Started + +### 🛠️ Prerequisites + +Before you begin, ensure you have the following installed: + +- Node.js (v14 or later) +- Rust (latest stable version) +- Tauri prerequisites for your operating system +- PNPM (8.5.0) + +### 🏗️ Installation + +#### Clone the Repository + + ```bash + git clone https://github.com/AppFlowy-IO/AppFlowy + ``` + +#### 🌐 Install the frontend dependencies: + + ```bash + cd frontend/appflowy_web_app + pnpm install + ``` + +#### 🖥️ Desktop Application (Tauri) (Optional) + +> **Note**: if you want to run the web app in the browser, skip this step + +- Follow the instructions [here](https://tauri.app/v1/guides/getting-started/prerequisites/) to install Tauri + +##### Windows and Linux Prerequisites + +###### Windows only + +- Install the Duckscript CLI and vcpkg + + ```bash + cargo install --force duckscript_cli + vcpkg integrate install + ``` + +###### Linux only + +- Install the required dependencies + + ```bash + sudo apt-get update + sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf + ``` + +- **Get error**: failed to run custom build command for librocksdb-sys v6.11.4 + + ```bash + sudo apt install clang + ``` + +##### Install Tauri Dependencies + +- Install cargo-make + + ```bash + cargo install --force cargo-make + ``` + + +- Install AppFlowy dev tools + + ```bash + # install development tools + # make sure you are in the root directory of the project + cd frontend + cargo make appflowy-tauri-deps-tools + ``` + +- Build the service/dependency + + ```bash + # make sure you are in the root directory of the project + cd frontend/appflowy_web_app + mkdir dist + cd src-tauri + cargo build + ``` + +### 🚀 Running the Application + +#### 🌐 Web Application + +- Run the web application + + ```bash + pnpm run dev + ``` +- Open your browser and navigate to `http://localhost:3000`, You can now interact with the AppFlowy web application + +#### 🖥️ Desktop Application (Tauri) + +**Ensure close web application before running the desktop application** + +- Run the desktop application + + ```bash + pnpm run tauri:dev + ``` +- The AppFlowy desktop application will open, and you can interact with it + +### 🛠️ Development + +#### How to add or modify i18n keys + +- Modify the i18n files in `frontend/resources/translations/en.json` to add or modify i18n keys +- Run the following command to update the i18n keys in the application + + ```bash + pnpm run sync:i18n + ``` + +#### How to modify the theme + +Don't modify the theme file in `frontend/appflowy_web_app/src/styles/variables` directly) + +- Modify the theme file in `frontend/appflowy_web_app/style-dictionary/tokens/base.json( or dark.json or light.json)` to + add or modify theme keys +- Run the following command to update the theme in the application + + ```bash + pnpm run css:variables + ``` + +#### How to add or modify the environment variables + +- Modify the environment file in `frontend/appflowy_web_app/.env` to add or modify environment variables + +#### How to create symlink for the @appflowyinc/client-api-wasm in local development + +- Run the following command to create a symlink for the @appflowyinc/client-api-wasm + + ```bash + # ensure you are in the frontend/appflowy_web_app directory + + pnpm run link:client-api $source_path $target_path + + # Example + # pnpm run link:client-api ../../../AppFlowy-Cloud/libs/client-api-wasm/pkg ./node_modules/@appflowyinc/client-api-wasm + ``` + +### 📝 About the Project + +#### 📁 Directory Structure + +- `frontend/appflowy_web_app`: Contains the web application source code +- `frontend/appflowy_web_app/src`: Contains the app entry point and the source code +- `frontend/appflowy_web_app/src/components`: Contains the react components +- `frontend/appflowy_web_app/src/styles`: Contains the styles for the application +- `frontend/appflowy_web_app/src/utils`: Contains the utility functions +- `frontend/appflowy_web_app/src/i18n`: Contains the i18n files +- `frontend/appflowy_web_app/src/assets`: Contains the assets for the application +- `frontend/appflowy_web_app/src/store`: Contains the redux store +- `frontend/appflowy_web_app/src/@types`: Contains the typescript types +- `frontend/appflowy_web_app/src/applications/services`: Contains the services for the application. In vite.config.ts, + we have defined the alias for the services directory for different environments(Tauri/Web) + ```typescript + resolve: { + alias: [ + // ... + { + find: '$client-services', + replacement: process.env.TAURI_MODE + ? `${__dirname}/src/application/services/tauri-services` + : `${__dirname}/src/application/services/js-services`, + }, + ] + } + ``` + +### 🧪 Testing + +> To be Continued... + + diff --git a/frontend/appflowy_web_app/index.html b/frontend/appflowy_web_app/index.html new file mode 100644 index 0000000000..5d36aaa0a1 --- /dev/null +++ b/frontend/appflowy_web_app/index.html @@ -0,0 +1,16 @@ + + + + + + + + AppFlowy + + +
+ + + diff --git a/frontend/appflowy_web_app/package.json b/frontend/appflowy_web_app/package.json new file mode 100644 index 0000000000..7fb9b5bddf --- /dev/null +++ b/frontend/appflowy_web_app/package.json @@ -0,0 +1,131 @@ +{ + "name": "appflowy_web_app", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "pnpm run sync:i18n && vite", + "dev:tauri": "TAURI_MODE=true pnpm run sync:i18n && vite", + "build": "vite build", + "build:tauri": "TAURI_MODE=true vite build", + "lint:tauri": "TAURI_MODE=true pnpm run sync:i18n && tsc --noEmit && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore", + "lint": "pnpm run sync:i18n && tsc --noEmit --project tsconfig.web.json && eslint --ext .js,.ts,.tsx . --ignore-path .eslintignore.web", + "preview": "vite preview --port 8080", + "tauri:dev": "tauri dev", + "css:variables": "node style-dictionary/config.cjs", + "sync:i18n": "node scripts/i18n.cjs", + "link:client-api": "rm -rf node_modules/.vite && node scripts/create-symlink.cjs" + }, + "dependencies": { + "@appflowyinc/client-api-wasm": "^0.0.2", + "@emoji-mart/data": "^1.1.2", + "@emoji-mart/react": "^1.1.1", + "@emotion/react": "^11.10.6", + "@emotion/styled": "^11.10.6", + "@mui/icons-material": "^5.11.11", + "@mui/material": "^5.11.12", + "@mui/system": "^5.14.4", + "@mui/x-date-pickers-pro": "^6.18.2", + "@reduxjs/toolkit": "2.0.0", + "@slate-yjs/core": "^1.0.2", + "@tauri-apps/api": "^1.5.3", + "@types/react-swipeable-views": "^0.13.4", + "axios": "^1.6.8", + "dayjs": "^1.11.9", + "emoji-mart": "^5.5.2", + "emoji-regex": "^10.2.1", + "events": "^3.3.0", + "google-protobuf": "^3.15.12", + "i18next": "^22.4.10", + "i18next-browser-languagedetector": "^7.0.1", + "i18next-resources-to-backend": "^1.1.4", + "is-hotkey": "^0.2.0", + "jest": "^29.5.0", + "js-base64": "^3.7.5", + "katex": "^0.16.7", + "lodash-es": "^4.17.21", + "nanoid": "^4.0.0", + "prismjs": "^1.29.0", + "protoc-gen-ts": "0.8.7", + "quill": "^1.3.7", + "quill-delta": "^5.1.0", + "react": "^18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-big-calendar": "^1.8.5", + "react-color": "^2.19.3", + "react-custom-scrollbars": "^4.2.1", + "react-datepicker": "^4.23.0", + "react-dom": "^18.2.0", + "react-error-boundary": "^4.0.13", + "react-hot-toast": "^2.4.1", + "react-i18next": "^14.1.0", + "react-katex": "^3.0.1", + "react-redux": "^8.0.5", + "react-router-dom": "^6.22.3", + "react-swipeable-views": "^0.14.0", + "react-transition-group": "^4.4.5", + "react-virtualized-auto-sizer": "^1.0.20", + "react-vtree": "^2.0.4", + "react-window": "^1.8.10", + "react18-input-otp": "^1.1.2", + "redux": "^4.2.1", + "rxjs": "^7.8.0", + "sass": "^1.70.0", + "slate": "^0.101.4", + "slate-history": "^0.100.0", + "slate-react": "^0.101.3", + "ts-results": "^3.3.0", + "unsplash-js": "^7.0.19", + "utf8": "^3.0.0", + "valtio": "^1.12.1", + "vite-plugin-wasm": "^3.3.0", + "yjs": "^13.5.51" + }, + "devDependencies": { + "@svgr/plugin-svgo": "^8.0.1", + "@tauri-apps/cli": "^1.5.11", + "@types/google-protobuf": "^3.15.12", + "@types/is-hotkey": "^0.1.7", + "@types/jest": "^29.5.3", + "@types/katex": "^0.16.0", + "@types/lodash-es": "^4.17.11", + "@types/node": "^20.11.30", + "@types/prismjs": "^1.26.0", + "@types/quill": "^2.0.10", + "@types/react": "^18.2.66", + "@types/react-beautiful-dnd": "^13.1.3", + "@types/react-color": "^3.0.6", + "@types/react-custom-scrollbars": "^4.0.13", + "@types/react-datepicker": "^4.19.3", + "@types/react-dom": "^18.2.22", + "@types/react-katex": "^3.0.0", + "@types/react-transition-group": "^4.4.6", + "@types/react-window": "^1.8.8", + "@types/utf8": "^3.0.1", + "@types/uuid": "^9.0.1", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.13", + "babel-jest": "^29.6.2", + "chalk": "^4.1.2", + "eslint": "^8.57.0", + "eslint-plugin-react": "^7.32.2", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "jest-environment-jsdom": "^29.6.2", + "postcss": "^8.4.21", + "prettier": "2.8.4", + "prettier-plugin-tailwindcss": "^0.2.2", + "style-dictionary": "^3.9.2", + "tailwindcss": "^3.2.7", + "ts-jest": "^29.1.1", + "ts-node-dev": "^2.0.0", + "tsconfig-paths-jest": "^0.0.1", + "typescript": "4.9.5", + "uuid": "^9.0.0", + "vite": "^5.2.0", + "vite-plugin-svgr": "^3.2.0", + "vite-plugin-terminal": "^1.2.0" + } +} diff --git a/frontend/appflowy_web_app/pnpm-lock.yaml b/frontend/appflowy_web_app/pnpm-lock.yaml new file mode 100644 index 0000000000..f6386be35a --- /dev/null +++ b/frontend/appflowy_web_app/pnpm-lock.yaml @@ -0,0 +1,7648 @@ +lockfileVersion: '6.0' + +dependencies: + '@appflowyinc/client-api-wasm': + specifier: ^0.0.2 + version: 0.0.2 + '@emoji-mart/data': + specifier: ^1.1.2 + version: 1.1.2 + '@emoji-mart/react': + specifier: ^1.1.1 + version: 1.1.1(emoji-mart@5.5.2)(react@18.2.0) + '@emotion/react': + specifier: ^11.10.6 + version: 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': + specifier: ^11.10.6 + version: 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/icons-material': + specifier: ^5.11.11 + version: 5.11.11(@mui/material@5.11.12)(@types/react@18.2.66)(react@18.2.0) + '@mui/material': + specifier: ^5.11.12 + version: 5.11.12(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': + specifier: ^5.14.4 + version: 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/x-date-pickers-pro': + specifier: ^6.18.2 + version: 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@5.11.12)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@reduxjs/toolkit': + specifier: 2.0.0 + version: 2.0.0(react-redux@8.0.5)(react@18.2.0) + '@slate-yjs/core': + specifier: ^1.0.2 + version: 1.0.2(slate@0.101.4)(yjs@13.5.51) + '@tauri-apps/api': + specifier: ^1.5.3 + version: 1.5.3 + '@types/react-swipeable-views': + specifier: ^0.13.4 + version: 0.13.4 + axios: + specifier: ^1.6.8 + version: 1.6.8 + dayjs: + specifier: ^1.11.9 + version: 1.11.9 + emoji-mart: + specifier: ^5.5.2 + version: 5.5.2 + emoji-regex: + specifier: ^10.2.1 + version: 10.2.1 + events: + specifier: ^3.3.0 + version: 3.3.0 + google-protobuf: + specifier: ^3.15.12 + version: 3.16.0 + i18next: + specifier: ^22.4.10 + version: 22.4.10 + i18next-browser-languagedetector: + specifier: ^7.0.1 + version: 7.0.1 + i18next-resources-to-backend: + specifier: ^1.1.4 + version: 1.1.4 + is-hotkey: + specifier: ^0.2.0 + version: 0.2.0 + jest: + specifier: ^29.5.0 + version: 29.5.0(@types/node@20.11.30) + js-base64: + specifier: ^3.7.5 + version: 3.7.5 + katex: + specifier: ^0.16.7 + version: 0.16.7 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + nanoid: + specifier: ^4.0.0 + version: 4.0.0 + prismjs: + specifier: ^1.29.0 + version: 1.29.0 + protoc-gen-ts: + specifier: 0.8.7 + version: 0.8.7 + quill: + specifier: ^1.3.7 + version: 1.3.7 + quill-delta: + specifier: ^5.1.0 + version: 5.1.0 + react: + specifier: ^18.2.0 + version: 18.2.0 + react-beautiful-dnd: + specifier: ^13.1.1 + version: 13.1.1(react-dom@18.2.0)(react@18.2.0) + react-big-calendar: + specifier: ^1.8.5 + version: 1.8.5(react-dom@18.2.0)(react@18.2.0) + react-color: + specifier: ^2.19.3 + version: 2.19.3(react@18.2.0) + react-custom-scrollbars: + specifier: ^4.2.1 + version: 4.2.1(react-dom@18.2.0)(react@18.2.0) + react-datepicker: + specifier: ^4.23.0 + version: 4.23.0(react-dom@18.2.0)(react@18.2.0) + react-dom: + specifier: ^18.2.0 + version: 18.2.0(react@18.2.0) + react-error-boundary: + specifier: ^4.0.13 + version: 4.0.13(react@18.2.0) + react-hot-toast: + specifier: ^2.4.1 + version: 2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0) + react-i18next: + specifier: ^14.1.0 + version: 14.1.0(i18next@22.4.10)(react-dom@18.2.0)(react@18.2.0) + react-katex: + specifier: ^3.0.1 + version: 3.0.1(prop-types@15.8.1)(react@18.2.0) + react-redux: + specifier: ^8.0.5 + version: 8.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + react-router-dom: + specifier: ^6.22.3 + version: 6.22.3(react-dom@18.2.0)(react@18.2.0) + react-swipeable-views: + specifier: ^0.14.0 + version: 0.14.0(react@18.2.0) + react-transition-group: + specifier: ^4.4.5 + version: 4.4.5(react-dom@18.2.0)(react@18.2.0) + react-virtualized-auto-sizer: + specifier: ^1.0.20 + version: 1.0.20(react-dom@18.2.0)(react@18.2.0) + react-vtree: + specifier: ^2.0.4 + version: 2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0) + react-window: + specifier: ^1.8.10 + version: 1.8.10(react-dom@18.2.0)(react@18.2.0) + react18-input-otp: + specifier: ^1.1.2 + version: 1.1.2(react-dom@18.2.0)(react@18.2.0) + redux: + specifier: ^4.2.1 + version: 4.2.1 + rxjs: + specifier: ^7.8.0 + version: 7.8.0 + sass: + specifier: ^1.70.0 + version: 1.70.0 + slate: + specifier: ^0.101.4 + version: 0.101.4 + slate-history: + specifier: ^0.100.0 + version: 0.100.0(slate@0.101.4) + slate-react: + specifier: ^0.101.3 + version: 0.101.3(react-dom@18.2.0)(react@18.2.0)(slate@0.101.4) + ts-results: + specifier: ^3.3.0 + version: 3.3.0 + unsplash-js: + specifier: ^7.0.19 + version: 7.0.19 + utf8: + specifier: ^3.0.0 + version: 3.0.0 + valtio: + specifier: ^1.12.1 + version: 1.12.1(@types/react@18.2.66)(react@18.2.0) + vite-plugin-wasm: + specifier: ^3.3.0 + version: 3.3.0(vite@5.2.0) + yjs: + specifier: ^13.5.51 + version: 13.5.51 + +devDependencies: + '@svgr/plugin-svgo': + specifier: ^8.0.1 + version: 8.0.1(@svgr/core@8.1.0)(typescript@4.9.5) + '@tauri-apps/cli': + specifier: ^1.5.11 + version: 1.5.11 + '@types/google-protobuf': + specifier: ^3.15.12 + version: 3.15.12 + '@types/is-hotkey': + specifier: ^0.1.7 + version: 0.1.7 + '@types/jest': + specifier: ^29.5.3 + version: 29.5.3 + '@types/katex': + specifier: ^0.16.0 + version: 0.16.0 + '@types/lodash-es': + specifier: ^4.17.11 + version: 4.17.11 + '@types/node': + specifier: ^20.11.30 + version: 20.11.30 + '@types/prismjs': + specifier: ^1.26.0 + version: 1.26.0 + '@types/quill': + specifier: ^2.0.10 + version: 2.0.10 + '@types/react': + specifier: ^18.2.66 + version: 18.2.66 + '@types/react-beautiful-dnd': + specifier: ^13.1.3 + version: 13.1.3 + '@types/react-color': + specifier: ^3.0.6 + version: 3.0.6 + '@types/react-custom-scrollbars': + specifier: ^4.0.13 + version: 4.0.13 + '@types/react-datepicker': + specifier: ^4.19.3 + version: 4.19.3(react-dom@18.2.0)(react@18.2.0) + '@types/react-dom': + specifier: ^18.2.22 + version: 18.2.22 + '@types/react-katex': + specifier: ^3.0.0 + version: 3.0.0 + '@types/react-transition-group': + specifier: ^4.4.6 + version: 4.4.6 + '@types/react-window': + specifier: ^1.8.8 + version: 1.8.8 + '@types/utf8': + specifier: ^3.0.1 + version: 3.0.1 + '@types/uuid': + specifier: ^9.0.1 + version: 9.0.1 + '@typescript-eslint/eslint-plugin': + specifier: ^7.2.0 + version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/parser': + specifier: ^7.2.0 + version: 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.2.1(vite@5.2.0) + autoprefixer: + specifier: ^10.4.13 + version: 10.4.13(postcss@8.4.21) + babel-jest: + specifier: ^29.6.2 + version: 29.6.2(@babel/core@7.24.3) + chalk: + specifier: ^4.1.2 + version: 4.1.2 + eslint: + specifier: ^8.57.0 + version: 8.57.0 + eslint-plugin-react: + specifier: ^7.32.2 + version: 7.32.2(eslint@8.57.0) + eslint-plugin-react-hooks: + specifier: ^4.6.0 + version: 4.6.0(eslint@8.57.0) + eslint-plugin-react-refresh: + specifier: ^0.4.6 + version: 0.4.6(eslint@8.57.0) + jest-environment-jsdom: + specifier: ^29.6.2 + version: 29.6.2 + postcss: + specifier: ^8.4.21 + version: 8.4.21 + prettier: + specifier: 2.8.4 + version: 2.8.4 + prettier-plugin-tailwindcss: + specifier: ^0.2.2 + version: 0.2.2(prettier@2.8.4) + style-dictionary: + specifier: ^3.9.2 + version: 3.9.2 + tailwindcss: + specifier: ^3.2.7 + version: 3.2.7(postcss@8.4.21) + ts-jest: + specifier: ^29.1.1 + version: 29.1.1(@babel/core@7.24.3)(babel-jest@29.6.2)(jest@29.5.0)(typescript@4.9.5) + ts-node-dev: + specifier: ^2.0.0 + version: 2.0.0(@types/node@20.11.30)(typescript@4.9.5) + tsconfig-paths-jest: + specifier: ^0.0.1 + version: 0.0.1 + typescript: + specifier: 4.9.5 + version: 4.9.5 + uuid: + specifier: ^9.0.0 + version: 9.0.0 + vite: + specifier: ^5.2.0 + version: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + vite-plugin-svgr: + specifier: ^3.2.0 + version: 3.2.0(typescript@4.9.5)(vite@5.2.0) + vite-plugin-terminal: + specifier: ^1.2.0 + version: 1.2.0(vite@5.2.0) + +packages: + + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + + /@ampproject/remapping@2.3.0: + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + + /@appflowyinc/client-api-wasm@0.0.2: + resolution: {integrity: sha512-Y8YkH+5ZT1sVJRXpMB5eMFt5SWUeRxgIV3FEXZjl0CjWjqSAwr5wIqEX0uHdOSrLU5fTzWoolRK9oIIiHvG2SA==} + dev: false + + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.2 + picocolors: 1.0.0 + + /@babel/compat-data@7.24.1: + resolution: {integrity: sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==} + engines: {node: '>=6.9.0'} + + /@babel/core@7.24.3: + resolution: {integrity: sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.1 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.3) + '@babel/helpers': 7.24.1 + '@babel/parser': 7.24.1 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + convert-source-map: 2.0.0 + debug: 4.3.4 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + /@babel/generator@7.24.1: + resolution: {integrity: sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.1 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + + /@babel/helper-environment-visitor@7.22.20: + resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} + engines: {node: '>=6.9.0'} + + /@babel/helper-function-name@7.23.0: + resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/types': 7.24.0 + + /@babel/helper-hoist-variables@7.22.5: + resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-module-imports@7.24.3: + resolution: {integrity: sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.3): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.24.3 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + + /@babel/helper-plugin-utils@7.24.0: + resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} + engines: {node: '>=6.9.0'} + + /@babel/helper-simple-access@7.22.5: + resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-split-export-declaration@7.22.6: + resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-identifier@7.22.20: + resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} + engines: {node: '>=6.9.0'} + + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + + /@babel/helpers@7.24.1: + resolution: {integrity: sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + transitivePeerDependencies: + - supports-color + + /@babel/highlight@7.24.2: + resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + + /@babel/parser@7.24.1: + resolution: {integrity: sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 + + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.3): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.3): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-jsx@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.3): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.3): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.3): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-syntax-typescript@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + + /@babel/plugin-transform-react-jsx-self@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + + /@babel/plugin-transform-react-jsx-source@7.24.1(@babel/core@7.24.3): + resolution: {integrity: sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@babel/helper-plugin-utils': 7.24.0 + dev: true + + /@babel/runtime@7.0.0: + resolution: {integrity: sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==} + dependencies: + regenerator-runtime: 0.12.1 + dev: false + + /@babel/runtime@7.24.1: + resolution: {integrity: sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.14.1 + + /@babel/template@7.24.0: + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + + /@babel/traverse@7.24.1: + resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.1 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + debug: 4.3.4 + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + + /@bcoe/v8-coverage@0.2.3: + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + /@cspotcode/source-map-support@0.8.1: + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + dev: true + + /@emoji-mart/data@1.1.2: + resolution: {integrity: sha512-1HP8BxD2azjqWJvxIaWAMyTySeZY0Osr83ukYjltPVkNXeJvTz7yDrPLBtnrD5uqJ3tg4CcLuuBW09wahqL/fg==} + dev: false + + /@emoji-mart/react@1.1.1(emoji-mart@5.5.2)(react@18.2.0): + resolution: {integrity: sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==} + peerDependencies: + emoji-mart: ^5.2 + react: ^16.8 || ^17 || ^18 + dependencies: + emoji-mart: 5.5.2 + react: 18.2.0 + dev: false + + /@emotion/babel-plugin@11.11.0: + resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} + dependencies: + '@babel/helper-module-imports': 7.24.3 + '@babel/runtime': 7.24.1 + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/serialize': 1.1.4 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + dev: false + + /@emotion/cache@11.11.0: + resolution: {integrity: sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==} + dependencies: + '@emotion/memoize': 0.8.1 + '@emotion/sheet': 1.2.2 + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + stylis: 4.2.0 + dev: false + + /@emotion/hash@0.9.1: + resolution: {integrity: sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==} + dev: false + + /@emotion/is-prop-valid@1.2.2: + resolution: {integrity: sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==} + dependencies: + '@emotion/memoize': 0.8.1 + dev: false + + /@emotion/memoize@0.8.1: + resolution: {integrity: sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==} + dev: false + + /@emotion/react@11.10.6(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-6HT8jBmcSkfzO7mc+N1L9uwvOnlcGoix8Zn7srt+9ga0MjREo6lRpuVX0kzo6Jp6oTqDhREOFsygN6Ew4fEQbw==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/babel-plugin': 11.11.0 + '@emotion/cache': 11.11.0 + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@emotion/weak-memoize': 0.3.1 + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + dev: false + + /@emotion/serialize@1.1.4: + resolution: {integrity: sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==} + dependencies: + '@emotion/hash': 0.9.1 + '@emotion/memoize': 0.8.1 + '@emotion/unitless': 0.8.1 + '@emotion/utils': 1.2.1 + csstype: 3.1.3 + dev: false + + /@emotion/sheet@1.2.2: + resolution: {integrity: sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==} + dev: false + + /@emotion/styled@11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-OXtBzOmDSJo5Q0AFemHCfl+bUueT8BIcPSxu0EGTpGk6DmI5dnhSzQANm1e1ze0YZL7TDyAyy6s/b/zmGOS3Og==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/babel-plugin': 11.11.0 + '@emotion/is-prop-valid': 1.2.2 + '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/serialize': 1.1.4 + '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0) + '@emotion/utils': 1.2.1 + '@types/react': 18.2.66 + react: 18.2.0 + dev: false + + /@emotion/unitless@0.8.1: + resolution: {integrity: sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==} + dev: false + + /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0): + resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==} + peerDependencies: + react: '>=16.8.0' + dependencies: + react: 18.2.0 + dev: false + + /@emotion/utils@1.2.1: + resolution: {integrity: sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==} + dev: false + + /@emotion/weak-memoize@0.3.1: + resolution: {integrity: sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==} + dev: false + + /@esbuild/aix-ppc64@0.20.2: + resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + requiresBuild: true + optional: true + + /@esbuild/android-arm64@0.20.2: + resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-arm@0.20.2: + resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/android-x64@0.20.2: + resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + optional: true + + /@esbuild/darwin-arm64@0.20.2: + resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/darwin-x64@0.20.2: + resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@esbuild/freebsd-arm64@0.20.2: + resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/freebsd-x64@0.20.2: + resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + optional: true + + /@esbuild/linux-arm64@0.20.2: + resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-arm@0.20.2: + resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ia32@0.20.2: + resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-loong64@0.20.2: + resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-mips64el@0.20.2: + resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-ppc64@0.20.2: + resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-riscv64@0.20.2: + resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-s390x@0.20.2: + resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/linux-x64@0.20.2: + resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@esbuild/netbsd-x64@0.20.2: + resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + optional: true + + /@esbuild/openbsd-x64@0.20.2: + resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + optional: true + + /@esbuild/sunos-x64@0.20.2: + resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + optional: true + + /@esbuild/win32-arm64@0.20.2: + resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-ia32@0.20.2: + resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@esbuild/win32-x64@0.20.2: + resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@eslint-community/eslint-utils@4.4.0(eslint@8.57.0): + resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + dependencies: + eslint: 8.57.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@eslint-community/regexpp@4.10.0: + resolution: {integrity: sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + dev: true + + /@eslint/eslintrc@2.1.4: + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.1 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@eslint/js@8.57.0: + resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@floating-ui/core@1.6.0: + resolution: {integrity: sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g==} + dependencies: + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/dom@1.6.3: + resolution: {integrity: sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw==} + dependencies: + '@floating-ui/core': 1.6.0 + '@floating-ui/utils': 0.2.1 + dev: false + + /@floating-ui/react-dom@2.0.8(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + dependencies: + '@floating-ui/dom': 1.6.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@floating-ui/utils@0.2.1: + resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} + dev: false + + /@humanwhocodes/config-array@0.11.14: + resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 2.0.2 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer@1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema@2.0.2: + resolution: {integrity: sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==} + dev: true + + /@icons/material@0.2.4(react@18.2.0): + resolution: {integrity: sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==} + peerDependencies: + react: '*' + dependencies: + react: 18.2.0 + dev: false + + /@isaacs/cliui@8.0.2: + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + dependencies: + string-width: 5.1.2 + string-width-cjs: /string-width@4.2.3 + strip-ansi: 7.1.0 + strip-ansi-cjs: /strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: /wrap-ansi@7.0.0 + dev: true + + /@istanbuljs/load-nyc-config@1.1.0: + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.14.1 + resolve-from: 5.0.0 + + /@istanbuljs/schema@0.1.3: + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + + /@jest/core@29.7.0: + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.9.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@20.11.30) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + jest-mock: 29.7.0 + + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.3.0 + '@types/node': 20.11.30 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + '@types/node': 20.11.30 + chalk: 4.1.2 + collect-v8-coverage: 1.0.2 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.7 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.2.0 + transitivePeerDependencies: + - supports-color + + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + callsites: 3.1.0 + graceful-fs: 4.2.11 + + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.2 + + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.24.3 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.25 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.6 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 20.11.30 + '@types/yargs': 17.0.32 + chalk: 4.1.2 + + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + + /@jridgewell/resolve-uri@3.1.2: + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + + /@jridgewell/sourcemap-codec@1.4.15: + resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + + /@jridgewell/trace-mapping@0.3.9: + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /@juggle/resize-observer@3.4.0: + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + dev: false + + /@mui/base@5.0.0-alpha.119(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-XA5zhlYfXi67u613eIF0xRmktkatx6ERy3h+PwrMN5IcWFbgiL1guz8VpdXON+GWb8+G7B8t5oqTFIaCqaSAeA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/is-prop-valid': 1.2.2 + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.66 + clsx: 1.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + dev: false + + /@mui/base@5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-I/lGHztkCzvwlXpjD2+SNmvNQvB4227xBXhISPjEaJUXGImOQ9f3D2Yj/T3KasSI/h0MLWy74X0J6clhPmsRbQ==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@popperjs/core': 2.11.8 + '@types/react': 18.2.66 + clsx: 2.1.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /@mui/core-downloads-tracker@5.15.14: + resolution: {integrity: sha512-on75VMd0XqZfaQW+9pGjSNiqW+ghc5E2ZSLRBXwcXl/C4YzjfyjrLPhrEpKnR9Uym9KXBvxrhoHfPcczYHweyA==} + dev: false + + /@mui/icons-material@5.11.11(@mui/material@5.11.12)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-Eell3ADmQVE8HOpt/LZ3zIma8JSvPh3XgnhwZLT0k5HRqZcd6F/QDHc7xsWtgz09t+UEFvOYJXjtrwKmLdwwpw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@mui/material': ^5.0.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@mui/material': 5.11.12(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@types/react': 18.2.66 + react: 18.2.0 + dev: false + + /@mui/material@5.11.12(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-M6BiIeJjySeEzWeiFJQ9pIjJy6mx5mHPWeMT99wjQdAmA2GxCQhE9A0fh6jQP4jMmYzxhOIhjsGcp0vSdpseXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/base': 5.0.0-alpha.119(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/core-downloads-tracker': 5.15.14 + '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + '@types/react-transition-group': 4.4.6 + clsx: 1.2.1 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + dev: false + + /@mui/private-theming@5.15.14(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-UH0EiZckOWcxiXLX3Jbb0K7rC8mxTr9L9l6QhOZxYc4r8FHUkefltV9VDGLrzCaWh30SQiJvAEd7djX3XXY6Xw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/styled-engine@5.15.14(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0): + resolution: {integrity: sha512-RILkuVD8gY6PvjZjqnWhz8fu68dVkqhM5+jYWfB5yhlSQKg+2rHkmEwm75XIeAqI3qwOndK6zELK5H6Zxn4NHw==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/cache': 11.11.0 + '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/system@5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-oPgfWS97QNfHcDBapdkZIs4G5i85BJt69Hp6wbXF6s7vi3Evcmhdk8AbCRW6n0sX4vTj8oe0mh0RIm1G2A1KDA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/private-theming': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@mui/styled-engine': 5.15.14(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(react@18.2.0) + '@mui/types': 7.2.14(@types/react@18.2.66) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react': 18.2.66 + clsx: 2.1.0 + csstype: 3.1.3 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /@mui/types@7.2.14(@types/react@18.2.66): + resolution: {integrity: sha512-MZsBZ4q4HfzBsywtXgM1Ksj6HDThtiwmOKUXH1pKYISI9gAVXCNHNpo7TlGoGrBaYWZTdNoirIN7JsQcQUjmQQ==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@types/react': 18.2.66 + dev: false + + /@mui/utils@5.15.14(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-0lF/7Hh/ezDv5X7Pry6enMsbYyGKjADzvHyo3Qrc/SSlTsQ1VkbDMbH0m2t3OR5iIVLwMoxwM7yGd+6FCMtTFA==} + engines: {node: '>=12.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 + react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/prop-types': 15.7.12 + '@types/react': 18.2.66 + prop-types: 15.8.1 + react: 18.2.0 + react-is: 18.2.0 + dev: false + + /@mui/x-date-pickers-pro@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@5.11.12)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-8lEVEOtCQssKWel4Ey1pRulGPXUQ73TnkHKzHWsjdv03FjiUs3eYB+Ej0Uk5yWPmsqlShWhOzOlOGDpzsYJsUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.11.12(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@mui/x-date-pickers': 6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@5.11.12)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0) + '@mui/x-license-pro': 6.10.2(@types/react@18.2.66)(react@18.2.0) + clsx: 2.1.0 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-date-pickers@6.18.2(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@mui/material@5.11.12)(@mui/system@5.14.4)(@types/react@18.2.66)(dayjs@1.11.9)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-HJq4uoFQSu5isa/mesWw2BKh8KBRYUQb+KaSlVlWfJNgP3YhPvWZ6yqCNYyxOAiPMxb0n3nBjS9ErO27OHjFMA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.9.0 + '@emotion/styled': ^11.8.1 + '@mui/material': ^5.8.6 + '@mui/system': ^5.8.0 + date-fns: ^2.25.0 + date-fns-jalali: ^2.13.0-0 + dayjs: ^1.10.7 + luxon: ^3.0.2 + moment: ^2.29.4 + moment-hijri: ^2.1.2 + moment-jalaali: ^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + date-fns: + optional: true + date-fns-jalali: + optional: true + dayjs: + optional: true + luxon: + optional: true + moment: + optional: true + moment-hijri: + optional: true + moment-jalaali: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@emotion/react': 11.10.6(@types/react@18.2.66)(react@18.2.0) + '@emotion/styled': 11.10.6(@emotion/react@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/base': 5.0.0-beta.40(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/material': 5.11.12(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0) + '@mui/system': 5.14.4(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.2.66)(react@18.2.0) + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + '@types/react-transition-group': 4.4.10 + clsx: 2.1.0 + dayjs: 1.11.9 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-transition-group: 4.4.5(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - '@types/react' + dev: false + + /@mui/x-license-pro@6.10.2(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-Baw3shilU+eHgU+QYKNPFUKvfS5rSyNJ98pQx02E0gKA22hWp/XAt88K1qUfUMPlkPpvg/uci6gviQSSLZkuKw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + '@mui/utils': 5.15.14(@types/react@18.2.66)(react@18.2.0) + react: 18.2.0 + transitivePeerDependencies: + - '@types/react' + dev: false + + /@nodelib/fs.scandir@2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat@2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk@1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.17.1 + dev: true + + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + + /@polka/url@1.0.0-next.25: + resolution: {integrity: sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==} + dev: true + + /@popperjs/core@2.11.8: + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + /@reduxjs/toolkit@2.0.0(react-redux@8.0.5)(react@18.2.0): + resolution: {integrity: sha512-Kq/a+aO28adYdPoNEu9p800MYPKoUc0tlkYfv035Ief9J7MPq8JvmT7UdpYhvXsoMtOdt567KwZjc9H3Rf8yjg==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + dependencies: + immer: 10.0.4 + react: 18.2.0 + react-redux: 8.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1) + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.0.1 + dev: false + + /@remix-run/router@1.15.3: + resolution: {integrity: sha512-Oy8rmScVrVxWZVOpEF57ovlnhpZ8CCPlnIIumVcV9nFdiSIrus99+Lw78ekXyGvVDlIsFJbSfmSovJUhCWYV3w==} + engines: {node: '>=14.0.0'} + dev: false + + /@restart/hooks@0.4.16(react@18.2.0): + resolution: {integrity: sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==} + peerDependencies: + react: '>=16.8.0' + dependencies: + dequal: 2.0.3 + react: 18.2.0 + dev: false + + /@rollup/plugin-strip@3.0.4: + resolution: {integrity: sha512-LDRV49ZaavxUo2YoKKMQjCxzCxugu1rCPQa0lDYBOWLj6vtzBMr8DcoJjsmg+s450RbKbe3qI9ZLaSO+O1oNbg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@rollup/pluginutils': 5.1.0 + estree-walker: 2.0.2 + magic-string: 0.30.8 + dev: true + + /@rollup/pluginutils@5.1.0: + resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + dependencies: + '@types/estree': 1.0.5 + estree-walker: 2.0.2 + picomatch: 2.3.1 + dev: true + + /@rollup/rollup-android-arm-eabi@4.13.2: + resolution: {integrity: sha512-3XFIDKWMFZrMnao1mJhnOT1h2g0169Os848NhhmGweEcfJ4rCi+3yMCOLG4zA61rbJdkcrM/DjVZm9Hg5p5w7g==} + cpu: [arm] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-android-arm64@4.13.2: + resolution: {integrity: sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==} + cpu: [arm64] + os: [android] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-arm64@4.13.2: + resolution: {integrity: sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-darwin-x64@4.13.2: + resolution: {integrity: sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm-gnueabihf@4.13.2: + resolution: {integrity: sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==} + cpu: [arm] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-gnu@4.13.2: + resolution: {integrity: sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-arm64-musl@4.13.2: + resolution: {integrity: sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-powerpc64le-gnu@4.13.2: + resolution: {integrity: sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==} + cpu: [ppc64le] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-riscv64-gnu@4.13.2: + resolution: {integrity: sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==} + cpu: [riscv64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-s390x-gnu@4.13.2: + resolution: {integrity: sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==} + cpu: [s390x] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-gnu@4.13.2: + resolution: {integrity: sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-linux-x64-musl@4.13.2: + resolution: {integrity: sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-arm64-msvc@4.13.2: + resolution: {integrity: sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-ia32-msvc@4.13.2: + resolution: {integrity: sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@rollup/rollup-win32-x64-msvc@4.13.2: + resolution: {integrity: sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + /@sinonjs/commons@3.0.1: + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + dependencies: + type-detect: 4.0.8 + + /@sinonjs/fake-timers@10.3.0: + resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + dependencies: + '@sinonjs/commons': 3.0.1 + + /@slate-yjs/core@1.0.2(slate@0.101.4)(yjs@13.5.51): + resolution: {integrity: sha512-X0hLFJbQu9c1ItWBaNuEn0pqcXYK76KCp8C4Gvy/VaTQVMo1VgAb2WiiJ0Je/AyuIYEPPSTNVOcyrGHwgA7e6Q==} + peerDependencies: + slate: '>=0.70.0' + yjs: ^13.5.29 + dependencies: + slate: 0.101.4 + y-protocols: 1.0.6(yjs@13.5.51) + yjs: 13.5.51 + dev: false + + /@svgr/babel-plugin-add-jsx-attribute@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-khWbXesWIP9v8HuKCl2NU2HNAyqpSQ/vkIl36Nbn4HIwEYSRWL0H7Gs6idJdha2DkpFDWlsqMELvoCE8lfFY6Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-add-jsx-attribute@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-attribute@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-iiZaIvb3H/c7d3TH2HBeK91uI2rMhZNwnsIrvd7ZwGLkFw6mmunOCoVnjdYua662MqGFxlN9xTq4fv9hgR4VXQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-attribute@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-empty-expression@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-sQQmyo+qegBx8DfFc04PFmIO1FP1MHI1/QEpzcIcclo5OAISsOJPW76ZIs0bDyO/DBSJEa/tDa1W26pVtt0FRw==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-remove-jsx-empty-expression@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-replace-jsx-attribute-value@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-i6MaAqIZXDOJeikJuzocByBf8zO+meLwfQ/qMHIjCcvpnfvWf82PFvredEZElErB5glQFJa2KVKk8N2xV6tRRA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-replace-jsx-attribute-value@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-dynamic-title@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-BoVSh6ge3SLLpKC0pmmN9DFlqgFy4NxNgdZNLPNJWBUU7TQpDWeBuyVuDW88iXydb5Cv0ReC+ffa5h3VrKfk1w==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-dynamic-title@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-em-dimensions@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-tNDcBa+hYn0gO+GkP/AuNKdVtMufVhU9fdzu+vUQsR18RIJ9RWe7h/pSBY338RO08wArntwbDk5WhQBmhf2PaA==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-svg-em-dimensions@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-react-native-svg@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-qw54u8ljCJYL2KtBOjI5z7Nzg8LnSvQOP5hPKj77H4VQL4+HdKbAT5pnkkZLmHKYwzsIHSYKXxHouD8zZamCFQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-react-native-svg@8.1.0(@babel/core@7.24.3): + resolution: {integrity: sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-svg-component@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-CcFECkDj98daOg9jE3Bh3uyD9kzevCAnZ+UtzG6+BQG/jOQ2OA3jHnX6iG4G1MCJkUQFnUvEv33NvQfqrb/F3A==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-plugin-transform-svg-component@8.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==} + engines: {node: '>=12'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + dev: true + + /@svgr/babel-preset@7.0.0(@babel/core@7.24.3): + resolution: {integrity: sha512-EX/NHeFa30j5UjldQGVQikuuQNHUdGmbh9kEpBKofGUtF0GUPJ4T4rhoYiqDAOmBOxojyot36JIFiDUHUK1ilQ==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-plugin-add-jsx-attribute': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-attribute': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-empty-expression': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-replace-jsx-attribute-value': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-dynamic-title': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-em-dimensions': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-react-native-svg': 7.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-svg-component': 7.0.0(@babel/core@7.24.3) + dev: true + + /@svgr/babel-preset@8.1.0(@babel/core@7.24.3): + resolution: {integrity: sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==} + engines: {node: '>=14'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-plugin-add-jsx-attribute': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-attribute': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-remove-jsx-empty-expression': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-replace-jsx-attribute-value': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-dynamic-title': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-svg-em-dimensions': 8.0.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-react-native-svg': 8.1.0(@babel/core@7.24.3) + '@svgr/babel-plugin-transform-svg-component': 8.0.0(@babel/core@7.24.3) + dev: true + + /@svgr/core@7.0.0(typescript@4.9.5): + resolution: {integrity: sha512-ztAoxkaKhRVloa3XydohgQQCb0/8x9T63yXovpmHzKMkHO6pkjdsIAWKOS4bE95P/2quVh1NtjSKlMRNzSBffw==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-preset': 7.0.0(@babel/core@7.24.3) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@4.9.5) + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@svgr/core@8.1.0(typescript@4.9.5): + resolution: {integrity: sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-preset': 8.1.0(@babel/core@7.24.3) + camelcase: 6.3.0 + cosmiconfig: 8.3.6(typescript@4.9.5) + snake-case: 3.0.4 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@svgr/hast-util-to-babel-ast@7.0.0: + resolution: {integrity: sha512-42Ej9sDDEmsJKjrfQ1PHmiDiHagh/u9AHO9QWbeNx4KmD9yS5d1XHmXUNINfUcykAU+4431Cn+k6Vn5mWBYimQ==} + engines: {node: '>=14'} + dependencies: + '@babel/types': 7.24.0 + entities: 4.5.0 + dev: true + + /@svgr/plugin-jsx@7.0.0: + resolution: {integrity: sha512-SWlTpPQmBUtLKxXWgpv8syzqIU8XgFRvyhfkam2So8b3BE0OS0HPe5UfmlJ2KIC+a7dpuuYovPR2WAQuSyMoPw==} + engines: {node: '>=14'} + dependencies: + '@babel/core': 7.24.3 + '@svgr/babel-preset': 7.0.0(@babel/core@7.24.3) + '@svgr/hast-util-to-babel-ast': 7.0.0 + svg-parser: 2.0.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@svgr/plugin-svgo@8.0.1(@svgr/core@8.1.0)(typescript@4.9.5): + resolution: {integrity: sha512-29OJ1QmJgnohQHDAgAuY2h21xWD6TZiXji+hnx+W635RiXTAlHTbjrZDktfqzkN0bOeQEtNe+xgq73/XeWFfSg==} + engines: {node: '>=14'} + peerDependencies: + '@svgr/core': '*' + dependencies: + '@svgr/core': 8.1.0(typescript@4.9.5) + cosmiconfig: 8.3.6(typescript@4.9.5) + deepmerge: 4.3.1 + svgo: 3.2.0 + transitivePeerDependencies: + - typescript + dev: true + + /@tauri-apps/api@1.5.3: + resolution: {integrity: sha512-zxnDjHHKjOsrIzZm6nO5Xapb/BxqUq1tc7cGkFXsFkGTsSWgCPH1D8mm0XS9weJY2OaR73I3k3S+b7eSzJDfqA==} + engines: {node: '>= 14.6.0', npm: '>= 6.6.0', yarn: '>= 1.19.1'} + dev: false + + /@tauri-apps/cli-darwin-arm64@1.5.11: + resolution: {integrity: sha512-2NLSglDb5VfvTbMtmOKWyD+oaL/e8Z/ZZGovHtUFyUSFRabdXc6cZOlcD1BhFvYkHqm+TqGaz5qtPR5UbqDs8A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-darwin-x64@1.5.11: + resolution: {integrity: sha512-/RQllHiJRH2fJOCudtZlaUIjofkHzP3zZgxi71ZUm7Fy80smU5TDfwpwOvB0wSVh0g/ciDjMArCSTo0MRvL+ag==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm-gnueabihf@1.5.11: + resolution: {integrity: sha512-IlBuBPKmMm+a5LLUEK6a21UGr9ZYd6zKuKLq6IGM4tVweQa8Sf2kP2Nqs74dMGIUrLmMs0vuqdURpykQg+z4NQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-gnu@1.5.11: + resolution: {integrity: sha512-w+k1bNHCU/GbmXshtAhyTwqosThUDmCEFLU4Zkin1vl2fuAtQry2RN7thfcJFepblUGL/J7yh3Q/0+BCjtspKQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-arm64-musl@1.5.11: + resolution: {integrity: sha512-PN6/dl+OfYQ/qrAy4HRAfksJ2AyWQYn2IA/2Wwpaa7SDRz2+hzwTQkvajuvy0sQ5L2WCG7ymFYRYMbpC6Hk9Pg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-gnu@1.5.11: + resolution: {integrity: sha512-MTVXLi89Nj7Apcvjezw92m7ZqIDKT5SFKZtVPCg6RoLUBTzko/BQoXYIRWmdoz2pgkHDUHgO2OMJ8oKzzddXbw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-linux-x64-musl@1.5.11: + resolution: {integrity: sha512-kwzAjqFpz7rvTs7WGZLy/a5nS5t15QKr3E9FG95MNF0exTl3d29YoAUAe1Mn0mOSrTJ9Z+vYYAcI/QdcsGBP+w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-arm64-msvc@1.5.11: + resolution: {integrity: sha512-L+5NZ/rHrSUrMxjj6YpFYCXp6wHnq8c8SfDTBOX8dO8x+5283/vftb4vvuGIsLS4UwUFXFnLt3XQr44n84E67Q==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-ia32-msvc@1.5.11: + resolution: {integrity: sha512-oVlD9IVewrY0lZzTdb71kNXkjdgMqFq+ohb67YsJb4Rf7o8A9DTlFds1XLCe3joqLMm4M+gvBKD7YnGIdxQ9vA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli-win32-x64-msvc@1.5.11: + resolution: {integrity: sha512-1CexcqUFCis5ypUIMOKllxUBrna09McbftWENgvVXMfA+SP+yPDPAVb8fIvUcdTIwR/yHJwcIucmTB4anww4vg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@tauri-apps/cli@1.5.11: + resolution: {integrity: sha512-B475D7phZrq5sZ3kDABH4g2mEoUIHtnIO+r4ZGAAfsjMbZCwXxR/jlMGTEL+VO3YzjpF7gQe38IzB4vLBbVppw==} + engines: {node: '>= 10'} + hasBin: true + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 1.5.11 + '@tauri-apps/cli-darwin-x64': 1.5.11 + '@tauri-apps/cli-linux-arm-gnueabihf': 1.5.11 + '@tauri-apps/cli-linux-arm64-gnu': 1.5.11 + '@tauri-apps/cli-linux-arm64-musl': 1.5.11 + '@tauri-apps/cli-linux-x64-gnu': 1.5.11 + '@tauri-apps/cli-linux-x64-musl': 1.5.11 + '@tauri-apps/cli-win32-arm64-msvc': 1.5.11 + '@tauri-apps/cli-win32-ia32-msvc': 1.5.11 + '@tauri-apps/cli-win32-x64-msvc': 1.5.11 + dev: true + + /@tootallnate/once@2.0.0: + resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} + engines: {node: '>= 10'} + dev: true + + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + + /@tsconfig/node10@1.0.11: + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + dev: true + + /@tsconfig/node12@1.0.11: + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + dev: true + + /@tsconfig/node14@1.0.3: + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + dev: true + + /@tsconfig/node16@1.0.4: + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + dev: true + + /@types/babel__core@7.20.5: + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + dependencies: + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + '@types/babel__generator': 7.6.8 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.5 + + /@types/babel__generator@7.6.8: + resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + dependencies: + '@babel/types': 7.24.0 + + /@types/babel__template@7.4.4: + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + dependencies: + '@babel/parser': 7.24.1 + '@babel/types': 7.24.0 + + /@types/babel__traverse@7.20.5: + resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + dependencies: + '@babel/types': 7.24.0 + + /@types/estree@1.0.5: + resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + + /@types/google-protobuf@3.15.12: + resolution: {integrity: sha512-40um9QqwHjRS92qnOaDpL7RmDK15NuZYo9HihiJRbYkMQZlWnuH8AdvbMy8/o6lgLmKbDUKa+OALCltHdbOTpQ==} + dev: true + + /@types/graceful-fs@4.1.9: + resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} + dependencies: + '@types/node': 20.11.30 + + /@types/hoist-non-react-statics@3.3.5: + resolution: {integrity: sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==} + dependencies: + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + dev: false + + /@types/is-hotkey@0.1.10: + resolution: {integrity: sha512-RvC8KMw5BCac1NvRRyaHgMMEtBaZ6wh0pyPTBu7izn4Sj/AX9Y4aXU5c7rX8PnM/knsuUpC1IeoBkANtxBypsQ==} + dev: false + + /@types/is-hotkey@0.1.7: + resolution: {integrity: sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ==} + dev: true + + /@types/istanbul-lib-coverage@2.0.6: + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + /@types/istanbul-lib-report@3.0.3: + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + /@types/istanbul-reports@3.0.4: + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + /@types/jest@29.5.3: + resolution: {integrity: sha512-1Nq7YrO/vJE/FYnqYyw0FS8LdrjExSgIiHyKg7xPpn+yi8Q4huZryKnkJatN1ZRH89Kw2v33/8ZMB7DuZeSLlA==} + dependencies: + expect: 29.7.0 + pretty-format: 29.7.0 + dev: true + + /@types/jsdom@20.0.1: + resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} + dependencies: + '@types/node': 20.11.30 + '@types/tough-cookie': 4.0.5 + parse5: 7.1.2 + dev: true + + /@types/json-schema@7.0.15: + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + dev: true + + /@types/katex@0.16.0: + resolution: {integrity: sha512-hz+S3nV6Mym5xPbT9fnO8dDhBFQguMYpY0Ipxv06JMi1ORgnEM4M1ymWDUhUNer3ElLmT583opRo4RzxKmh9jw==} + dev: true + + /@types/lodash-es@4.17.11: + resolution: {integrity: sha512-eCw8FYAWHt2DDl77s+AMLLzPn310LKohruumpucZI4oOFJkIgnlaJcy23OKMJxx4r9PeTF13Gv6w+jqjWQaYUg==} + dependencies: + '@types/lodash': 4.17.0 + dev: true + + /@types/lodash@4.17.0: + resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==} + + /@types/node@20.11.30: + resolution: {integrity: sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==} + dependencies: + undici-types: 5.26.5 + + /@types/parse-json@4.0.2: + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + dev: false + + /@types/prismjs@1.26.0: + resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==} + dev: true + + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + + /@types/quill@2.0.10: + resolution: {integrity: sha512-L6OHONEj2v4NRbWQOsn7j1N0SyzhRR3M4g1M6j/uuIwIsIW2ShWHhwbqNvH8hSmVktzqu0lITfdnqVOQ4qkrhA==} + dependencies: + parchment: 1.1.4 + quill-delta: 4.2.2 + dev: true + + /@types/react-beautiful-dnd@13.1.3: + resolution: {integrity: sha512-BNdmvONKtsrZq3AGrujECQrIn8cDT+fZsxBLXuX3YWY/nHfZinUFx4W88eS0rkcXzuLbXpKOsu/1WCMPMLEpPg==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-color@3.0.6: + resolution: {integrity: sha512-OzPIO5AyRmLA7PlOyISlgabpYUa3En74LP8mTMa0veCA719SvYQov4WLMsHvCgXP+L+KI9yGhYnqZafVGG0P4w==} + dependencies: + '@types/react': 18.2.66 + '@types/reactcss': 1.2.12 + dev: true + + /@types/react-custom-scrollbars@4.0.13: + resolution: {integrity: sha512-t+15reWgAE1jXlrhaZoxjuH/SQf+EG0rzAzSCzTIkSiP5CDT7KhoExNPwIa6uUxtPkjc3gdW/ry7GetLEwCfGA==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-datepicker@4.19.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==} + dependencies: + '@popperjs/core': 2.11.8 + '@types/react': 18.2.66 + date-fns: 2.30.0 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + transitivePeerDependencies: + - react + - react-dom + dev: true + + /@types/react-dom@18.2.22: + resolution: {integrity: sha512-fHkBXPeNtfvri6gdsMYyW+dW7RXFo6Ad09nLFK0VQWR7yGLai/Cyvyj696gbwYvBnhGtevUG9cET0pmUbMtoPQ==} + dependencies: + '@types/react': 18.2.66 + + /@types/react-katex@3.0.0: + resolution: {integrity: sha512-AiHHXh71a2M7Z6z1wj6iA23SkiRF9r0neHUdu8zjU/cT3MyLxDefYHbcceKhV/gjDEZgF3YaiNHyPNtoGUjPvg==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/react-redux@7.1.33: + resolution: {integrity: sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==} + dependencies: + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.2.66 + hoist-non-react-statics: 3.3.2 + redux: 4.2.1 + dev: false + + /@types/react-swipeable-views@0.13.4: + resolution: {integrity: sha512-hQV9Oq6oa+9HKdnGd43xkckElwf5dThOiegtQxqE7qX761oHhxnZO07fz6IsKSnUy9J3tzlRQBu3sNyvC8+kYw==} + dependencies: + '@types/react': 18.2.66 + dev: false + + /@types/react-transition-group@4.4.10: + resolution: {integrity: sha512-hT/+s0VQs2ojCX823m60m5f0sL5idt9SO6Tj6Dg+rdphGPIeJbJ6CxvBYkgkGKrYeDjvIpKTR38UzmtHJOGW3Q==} + dependencies: + '@types/react': 18.2.66 + dev: false + + /@types/react-transition-group@4.4.6: + resolution: {integrity: sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==} + dependencies: + '@types/react': 18.2.66 + + /@types/react-window@1.8.8: + resolution: {integrity: sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==} + dependencies: + '@types/react': 18.2.66 + + /@types/react@18.2.66: + resolution: {integrity: sha512-OYTmMI4UigXeFMF/j4uv0lBBEbongSgptPrHBxqME44h9+yNov+oL6Z3ocJKo0WyXR84sQUNeyIp9MRfckvZpg==} + dependencies: + '@types/prop-types': 15.7.12 + '@types/scheduler': 0.23.0 + csstype: 3.1.3 + + /@types/reactcss@1.2.12: + resolution: {integrity: sha512-BrXUQ86/wbbFiZv8h/Q1/Q1XOsaHneYmCb/tHe9+M8XBAAUc2EHfdY0DY22ZZjVSaXr5ix7j+zsqO2eGZub8lQ==} + dependencies: + '@types/react': 18.2.66 + dev: true + + /@types/scheduler@0.23.0: + resolution: {integrity: sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==} + + /@types/semver@7.5.8: + resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + dev: true + + /@types/stack-utils@2.0.3: + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + /@types/strip-bom@3.0.0: + resolution: {integrity: sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==} + dev: true + + /@types/strip-json-comments@0.0.30: + resolution: {integrity: sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==} + dev: true + + /@types/tough-cookie@4.0.5: + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + dev: true + + /@types/use-sync-external-store@0.0.3: + resolution: {integrity: sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==} + dev: false + + /@types/utf8@3.0.1: + resolution: {integrity: sha512-1EkWuw7rT3BMz2HpmcEOr/HL61mWNA6Ulr/KdbXR9AI0A55wD4Qfv8hizd8Q1DnknSIzzDvQmvvY/guvX7jjZA==} + dev: true + + /@types/uuid@9.0.1: + resolution: {integrity: sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==} + dev: true + + /@types/warning@3.0.3: + resolution: {integrity: sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==} + dev: false + + /@types/yargs-parser@21.0.3: + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + /@types/yargs@17.0.32: + resolution: {integrity: sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==} + dependencies: + '@types/yargs-parser': 21.0.3 + + /@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@eslint-community/regexpp': 4.10.0 + '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4 + eslint: 8.57.0 + graphemer: 1.4.0 + ignore: 5.3.1 + natural-compare: 1.4.0 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@4.9.5) + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4 + eslint: 8.57.0 + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager@7.2.0: + resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 + dev: true + + /@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 7.2.0(typescript@4.9.5) + '@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@4.9.5) + debug: 4.3.4 + eslint: 8.57.0 + ts-api-utils: 1.3.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types@7.2.0: + resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==} + engines: {node: ^16.0.0 || >=18.0.0} + dev: true + + /@typescript-eslint/typescript-estree@7.2.0(typescript@4.9.5): + resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/visitor-keys': 7.2.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.3 + semver: 7.6.0 + ts-api-utils: 1.3.0(typescript@4.9.5) + typescript: 4.9.5 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@4.9.5): + resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==} + engines: {node: ^16.0.0 || >=18.0.0} + peerDependencies: + eslint: ^8.56.0 + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@types/json-schema': 7.0.15 + '@types/semver': 7.5.8 + '@typescript-eslint/scope-manager': 7.2.0 + '@typescript-eslint/types': 7.2.0 + '@typescript-eslint/typescript-estree': 7.2.0(typescript@4.9.5) + eslint: 8.57.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys@7.2.0: + resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==} + engines: {node: ^16.0.0 || >=18.0.0} + dependencies: + '@typescript-eslint/types': 7.2.0 + eslint-visitor-keys: 3.4.3 + dev: true + + /@ungap/structured-clone@1.2.0: + resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + dev: true + + /@vitejs/plugin-react@4.2.1(vite@5.2.0): + resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.3) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.0 + vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + transitivePeerDependencies: + - supports-color + dev: true + + /abab@2.0.6: + resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} + deprecated: Use your platform's native atob() and btoa() methods instead + dev: true + + /acorn-globals@7.0.1: + resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} + dependencies: + acorn: 8.11.3 + acorn-walk: 8.3.2 + dev: true + + /acorn-jsx@5.3.2(acorn@8.11.3): + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.11.3 + dev: true + + /acorn-node@1.8.2: + resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} + dependencies: + acorn: 7.4.1 + acorn-walk: 7.2.0 + xtend: 4.0.2 + dev: true + + /acorn-walk@7.2.0: + resolution: {integrity: sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn-walk@8.3.2: + resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} + engines: {node: '>=0.4.0'} + dev: true + + /acorn@7.4.1: + resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /acorn@8.11.3: + resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /add-px-to-style@1.0.0: + resolution: {integrity: sha512-YMyxSlXpPjD8uWekCQGuN40lV4bnZagUwqa2m/uFv1z/tNImSk9fnXVMUI5qwME/zzI3MMQRvjZ+69zyfSSyew==} + dev: false + + /agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.21.3 + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + /ansi-regex@6.0.1: + resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} + engines: {node: '>=12'} + dev: true + + /ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + dependencies: + color-convert: 1.9.3 + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + + /ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + /arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + dev: true + + /arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + dev: true + + /argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + dependencies: + sprintf-js: 1.0.3 + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-buffer-byte-length@1.0.1: + resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + is-array-buffer: 3.0.4 + dev: true + + /array-includes@3.1.8: + resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + is-string: 1.0.7 + dev: true + + /array-union@2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /array.prototype.flat@1.3.2: + resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.flatmap@1.3.2: + resolution: {integrity: sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-shim-unscopables: 1.0.2 + dev: true + + /array.prototype.tosorted@1.1.3: + resolution: {integrity: sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-shim-unscopables: 1.0.2 + dev: true + + /arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + dev: true + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + /autoprefixer@10.4.13(postcss@8.4.21): + resolution: {integrity: sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==} + engines: {node: ^10 || ^12 || >=14} + hasBin: true + peerDependencies: + postcss: ^8.1.0 + dependencies: + browserslist: 4.23.0 + caniuse-lite: 1.0.30001603 + fraction.js: 4.3.7 + normalize-range: 0.1.2 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + dev: true + + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: true + + /axios@1.6.8: + resolution: {integrity: sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==} + dependencies: + follow-redirects: 1.15.6 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + + /babel-jest@29.6.2(@babel/core@7.24.3): + resolution: {integrity: sha512-BYCzImLos6J3BH/+HvUCHG1dTf2MzmAB4jaVxHV+29RZLjR29XuYTmsf2sdDwkrb+FczkGo3kOhE7ga6sI0P4A==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.24.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + + /babel-jest@29.7.0(@babel/core@7.24.3): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.24.3 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.24.3) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-istanbul@6.1.1: + resolution: {integrity: sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==} + engines: {node: '>=8'} + dependencies: + '@babel/helper-plugin-utils': 7.24.0 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-instrument: 5.2.1 + test-exclude: 6.0.0 + transitivePeerDependencies: + - supports-color + + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.24.0 + '@babel/types': 7.24.0 + '@types/babel__core': 7.20.5 + '@types/babel__traverse': 7.20.5 + + /babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + dependencies: + '@babel/runtime': 7.24.1 + cosmiconfig: 7.1.0 + resolve: 1.22.8 + dev: false + + /babel-preset-current-node-syntax@1.0.1(@babel/core@7.24.3): + resolution: {integrity: sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.3) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.3) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.3) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.3) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.3) + + /babel-preset-jest@29.6.3(@babel/core@7.24.3): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.3 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.3) + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + + /browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + dependencies: + caniuse-lite: 1.0.30001603 + electron-to-chromium: 1.4.722 + node-releases: 2.0.14 + update-browserslist-db: 1.0.13(browserslist@4.23.0) + + /bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + dependencies: + fast-json-stable-stringify: 2.1.0 + dev: true + + /bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + dependencies: + node-int64: 0.4.0 + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + /call-bind@1.0.7: + resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + set-function-length: 1.2.2 + + /callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + /camel-case@4.1.2: + resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==} + dependencies: + pascal-case: 3.1.2 + tslib: 2.6.2 + dev: true + + /camelcase-css@2.0.1: + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} + engines: {node: '>= 6'} + dev: true + + /camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + /caniuse-lite@1.0.30001603: + resolution: {integrity: sha512-iL2iSS0eDILMb9n5yKQoTBim9jMZ0Yrk8g0N9K7UzYyWnfIKzXBZD5ngpM37ZcL/cv0Mli8XtVMRYMQAfFpi5Q==} + + /capital-case@1.0.4: + resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case-first: 2.0.2 + dev: true + + /chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + /change-case@4.1.2: + resolution: {integrity: sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==} + dependencies: + camel-case: 4.1.2 + capital-case: 1.0.4 + constant-case: 3.0.4 + dot-case: 3.0.4 + header-case: 2.0.4 + no-case: 3.0.4 + param-case: 3.0.4 + pascal-case: 3.1.2 + path-case: 3.0.4 + sentence-case: 3.0.4 + snake-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + /chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + /ci-info@3.9.0: + resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} + engines: {node: '>=8'} + + /cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + + /classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + dev: false + + /cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + + /clsx@2.1.0: + resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} + engines: {node: '>=6'} + dev: false + + /co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + /collect-v8-coverage@1.0.2: + resolution: {integrity: sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==} + + /color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + dependencies: + color-name: 1.1.3 + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + + /color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + /combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + + /commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + /compute-scroll-into-view@3.1.0: + resolution: {integrity: sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==} + dev: false + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + /constant-case@3.0.4: + resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case: 2.0.2 + dev: true + + /convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + dev: false + + /convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + /cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.0 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + dev: false + + /cosmiconfig@8.3.6(typescript@4.9.5): + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + import-fresh: 3.3.0 + js-yaml: 4.1.0 + parse-json: 5.2.0 + path-type: 4.0.0 + typescript: 4.9.5 + dev: true + + /create-jest@29.7.0(@types/node@20.11.30): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@20.11.30) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + dev: true + + /cross-spawn@7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + /css-box-model@1.2.1: + resolution: {integrity: sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==} + dependencies: + tiny-invariant: 1.3.3 + dev: false + + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.2.0 + dev: true + + /css-tree@2.3.1: + resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + dependencies: + mdn-data: 2.0.30 + source-map-js: 1.2.0 + dev: true + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + + /cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + dev: true + + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: true + + /cssom@0.3.8: + resolution: {integrity: sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==} + dev: true + + /cssom@0.5.0: + resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + dev: true + + /cssstyle@2.3.0: + resolution: {integrity: sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==} + engines: {node: '>=8'} + dependencies: + cssom: 0.3.8 + dev: true + + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + /data-urls@3.0.2: + resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} + engines: {node: '>=12'} + dependencies: + abab: 2.0.6 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + dev: true + + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: true + + /date-arithmetic@4.1.0: + resolution: {integrity: sha512-QWxYLR5P/6GStZcdem+V1xoto6DMadYWpMXU82ES3/RfR3Wdwr3D0+be7mgOJ+Ov0G9D5Dmb9T17sNLQYj9XOg==} + dev: false + + /date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + dependencies: + '@babel/runtime': 7.24.1 + + /dayjs@1.11.9: + resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==} + dev: false + + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + + /decimal.js@10.4.3: + resolution: {integrity: sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==} + dev: true + + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + /deep-equal@1.1.2: + resolution: {integrity: sha512-5tdhKF6DbU7iIzrIOa1AOUt39ZRm13cmL1cGEh//aqR8x9+tNfbywRf0n5FD/18OKMdo7DNEtrX2t22ZAkI+eg==} + engines: {node: '>= 0.4'} + dependencies: + is-arguments: 1.1.1 + is-date-object: 1.0.5 + is-regex: 1.1.4 + object-is: 1.1.6 + object-keys: 1.1.1 + regexp.prototype.flags: 1.5.2 + dev: false + + /deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + /define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + dependencies: + es-define-property: 1.0.0 + es-errors: 1.3.0 + gopd: 1.0.1 + + /define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + /defined@1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + dev: true + + /delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + /dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + dev: false + + /derive-valtio@0.1.0(valtio@1.12.1): + resolution: {integrity: sha512-OCg2UsLbXK7GmmpzMXhYkdO64vhJ1ROUUGaTFyHjVwEdMEcTTRj7W1TxLbSBxdY8QLBPCcp66MTyaSy0RpO17A==} + peerDependencies: + valtio: '*' + dependencies: + valtio: 1.12.1(@types/react@18.2.66)(react@18.2.0) + dev: false + + /detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + /detective@5.2.1: + resolution: {integrity: sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==} + engines: {node: '>=0.8.0'} + hasBin: true + dependencies: + acorn-node: 1.8.2 + defined: 1.0.1 + minimist: 1.2.8 + dev: true + + /didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + dev: true + + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dev: true + + /dir-glob@3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /direction@1.0.4: + resolution: {integrity: sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==} + hasBin: true + dev: false + + /dlv@1.1.3: + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} + dev: true + + /doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /dom-css@2.1.0: + resolution: {integrity: sha512-w9kU7FAbaSh3QKijL6n59ofAhkkmMJ31GclJIz/vyQdjogfyxcB6Zf8CZyibOERI5o0Hxz30VmJS7+7r5fEj2Q==} + dependencies: + add-px-to-style: 1.0.0 + prefix-style: 2.0.1 + to-camel-case: 1.0.0 + dev: false + + /dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + dependencies: + '@babel/runtime': 7.24.1 + csstype: 3.1.3 + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: true + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: true + + /domexception@4.0.0: + resolution: {integrity: sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==} + engines: {node: '>=12'} + deprecated: Use your platform's native DOMException instead + dependencies: + webidl-conversions: 7.0.0 + dev: true + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: true + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: true + + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /dynamic-dedupe@0.3.0: + resolution: {integrity: sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==} + dependencies: + xtend: 4.0.2 + dev: true + + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + + /electron-to-chromium@1.4.722: + resolution: {integrity: sha512-5nLE0TWFFpZ80Crhtp4pIp8LXCztjYX41yUcV6b+bKR2PqzjskTMOOlBi1VjBHlvHwS+4gar7kNKOrsbsewEZQ==} + + /emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + /emoji-mart@5.5.2: + resolution: {integrity: sha512-Sqc/nso4cjxhOwWJsp9xkVm8OF5c+mJLZJFoFfzRuKO+yWiN7K8c96xmtughYb0d/fZ8UC6cLIQ/p4BR6Pv3/A==} + dev: false + + /emoji-regex@10.2.1: + resolution: {integrity: sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==} + dev: false + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: true + + /error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + dependencies: + is-arrayish: 0.2.1 + + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: true + + /es-define-property@1.0.0: + resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + + /es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: true + + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: true + + /es-shim-unscopables@1.0.2: + resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} + dependencies: + hasown: 2.0.2 + dev: true + + /es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + dev: true + + /esbuild@0.20.2: + resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/aix-ppc64': 0.20.2 + '@esbuild/android-arm': 0.20.2 + '@esbuild/android-arm64': 0.20.2 + '@esbuild/android-x64': 0.20.2 + '@esbuild/darwin-arm64': 0.20.2 + '@esbuild/darwin-x64': 0.20.2 + '@esbuild/freebsd-arm64': 0.20.2 + '@esbuild/freebsd-x64': 0.20.2 + '@esbuild/linux-arm': 0.20.2 + '@esbuild/linux-arm64': 0.20.2 + '@esbuild/linux-ia32': 0.20.2 + '@esbuild/linux-loong64': 0.20.2 + '@esbuild/linux-mips64el': 0.20.2 + '@esbuild/linux-ppc64': 0.20.2 + '@esbuild/linux-riscv64': 0.20.2 + '@esbuild/linux-s390x': 0.20.2 + '@esbuild/linux-x64': 0.20.2 + '@esbuild/netbsd-x64': 0.20.2 + '@esbuild/openbsd-x64': 0.20.2 + '@esbuild/sunos-x64': 0.20.2 + '@esbuild/win32-arm64': 0.20.2 + '@esbuild/win32-ia32': 0.20.2 + '@esbuild/win32-x64': 0.20.2 + + /escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + /escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + /escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + /escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + dev: true + + /eslint-plugin-react-hooks@4.6.0(eslint@8.57.0): + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-plugin-react-refresh@0.4.6(eslint@8.57.0): + resolution: {integrity: sha512-NjGXdm7zgcKRkKMua34qVO9doI7VOxZ6ancSvBELJSSoX97jyndXcSoa8XBh69JoB31dNz3EEzlMcizZl7LaMA==} + peerDependencies: + eslint: '>=7' + dependencies: + eslint: 8.57.0 + dev: true + + /eslint-plugin-react@7.32.2(eslint@8.57.0): + resolution: {integrity: sha512-t2fBMa+XzonrrNkyVirzKlvn5RXzzPwRHtMvLAtVZrt8oxgnTQaYbU6SXTOO1mwQgp1y5+toMSKInnzGr0Knqg==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 + dependencies: + array-includes: 3.1.8 + array.prototype.flatmap: 1.3.2 + array.prototype.tosorted: 1.1.3 + doctrine: 2.1.0 + eslint: 8.57.0 + estraverse: 5.3.0 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.8 + object.fromentries: 2.0.8 + object.hasown: 1.1.4 + object.values: 1.2.0 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.11 + dev: true + + /eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint@8.57.0: + resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0) + '@eslint-community/regexpp': 4.10.0 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.0 + '@humanwhocodes/config-array': 0.11.14 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.2.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.5.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.1 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.3 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) + eslint-visitor-keys: 3.4.3 + dev: true + + /esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + /esquery@1.5.0: + resolution: {integrity: sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + dev: true + + /esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /eventemitter3@2.0.3: + resolution: {integrity: sha512-jLN68Dx5kyFHaePoXWPsCGW5qdyZQtLYHkxkg02/Mz6g0kYpDx4FyP6XfArhQdlOC4b8Mv+EMxPo/8La7Tzghg==} + dev: false + + /events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + dev: false + + /execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + dependencies: + cross-spawn: 7.0.3 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + /exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + + /extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + + /fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-diff@1.1.2: + resolution: {integrity: sha512-KaJUt+M9t1qaIteSvjc6P3RbMdXsNhK61GRftR6SNxqmhthcd9MGIi4T+o0jD8LUSpSnSKXE20nLtJ3fOHxQig==} + dev: false + + /fast-diff@1.2.0: + resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} + dev: true + + /fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + dev: false + + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + /fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq@1.17.1: + resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + dependencies: + reusify: 1.0.4 + dev: true + + /fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + dependencies: + bser: 2.1.1 + + /file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.2.0 + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + + /find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + dev: false + + /find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.3.1 + keyv: 4.5.4 + rimraf: 3.0.2 + dev: true + + /flatted@3.3.1: + resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + dev: true + + /follow-redirects@1.15.6: + resolution: {integrity: sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /for-each@0.3.3: + resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} + dependencies: + is-callable: 1.2.7 + dev: true + + /foreground-child@3.1.1: + resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} + engines: {node: '>=14'} + dependencies: + cross-spawn: 7.0.3 + signal-exit: 4.1.0 + dev: true + + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + + /fraction.js@4.3.7: + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + dev: true + + /fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.1 + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + + /function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + /function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + dev: true + + /functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + /gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + /get-intrinsic@1.2.4: + resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + + /get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + /get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + /get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + + /glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@10.3.12: + resolution: {integrity: sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.4 + minipass: 7.0.4 + path-scurry: 1.10.2 + dev: true + + /glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + /globalize@0.1.1: + resolution: {integrity: sha512-5e01v8eLGfuQSOvx2MsDMOWS0GFtCx1wPzQSmcHw4hkxFzrQDBO3Xwg/m8Hr/7qXMrHeOIE29qWVzyv06u1TZA==} + dev: false + + /globals@11.12.0: + resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} + engines: {node: '>=4'} + + /globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalthis@1.0.3: + resolution: {integrity: sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + dev: true + + /globby@11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.3.2 + ignore: 5.3.1 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /goober@2.1.14(csstype@3.1.3): + resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} + peerDependencies: + csstype: ^3.0.10 + dependencies: + csstype: 3.1.3 + dev: false + + /google-protobuf@3.16.0: + resolution: {integrity: sha512-gBY66yYL1wbQMU2r1POkXSXkm035Ni0wFv3vx0K9IEUsJLP9G5rAcFVn0xUXfZneRu6MmDjaw93pt/DE56VOyw==} + dev: false + + /gopd@1.0.1: + resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} + dependencies: + get-intrinsic: 1.2.4 + + /graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + /graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + dev: true + + /has-bigints@1.0.2: + resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} + dev: true + + /has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + /has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + dependencies: + es-define-property: 1.0.0 + + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + + /has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + + /header-case@2.0.4: + resolution: {integrity: sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==} + dependencies: + capital-case: 1.0.4 + tslib: 2.6.2 + dev: true + + /hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + dependencies: + react-is: 16.13.1 + dev: false + + /html-encoding-sniffer@3.0.0: + resolution: {integrity: sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==} + engines: {node: '>=12'} + dependencies: + whatwg-encoding: 2.0.0 + dev: true + + /html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + /html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + dependencies: + void-elements: 3.1.0 + dev: false + + /http-proxy-agent@5.0.0: + resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} + engines: {node: '>= 6'} + dependencies: + '@tootallnate/once': 2.0.0 + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: true + + /human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + /i18next-browser-languagedetector@7.0.1: + resolution: {integrity: sha512-Pa5kFwaczXJAeHE56CHG2aWzFBMJNUNghf0Pm4SwSrEMps/PTKqW90EYWlIvhuYStf3Sn1K0vw+gH3+TLdkH1g==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /i18next-resources-to-backend@1.1.4: + resolution: {integrity: sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /i18next@22.4.10: + resolution: {integrity: sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: true + + /ignore@5.3.1: + resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} + engines: {node: '>= 4'} + dev: true + + /immer@10.0.4: + resolution: {integrity: sha512-cuBuGK40P/sk5IzWa9QPUaAdvPHjkk1c+xYsd9oZw+YQQEV+10G0P5uMpGctZZKnyQ+ibRO08bD25nWLmYi2pw==} + dev: false + + /immutable@4.3.5: + resolution: {integrity: sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==} + + /import-fresh@3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + /import-local@3.1.0: + resolution: {integrity: sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==} + engines: {node: '>=8'} + hasBin: true + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + /imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + /internal-slot@1.0.7: + resolution: {integrity: sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.0.6 + dev: true + + /invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + dependencies: + loose-envify: 1.4.0 + dev: false + + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: false + + /is-array-buffer@3.0.4: + resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + dev: true + + /is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + /is-bigint@1.0.4: + resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} + dependencies: + has-bigints: 1.0.2 + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.3.0 + + /is-boolean-object@1.1.2: + resolution: {integrity: sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: true + + /is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + dev: true + + /is-core-module@2.13.1: + resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} + dependencies: + hasown: 2.0.2 + + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: true + + /is-date-object@1.0.5: + resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + /is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + + /is-hotkey@0.2.0: + resolution: {integrity: sha512-UknnZK4RakDmTgz4PI1wIph5yxSs/mvChWs9ifnlXsKuXgWmOkY/hAE0H/k2MIqH0RlRye0i1oC07MCRSD28Mw==} + dev: false + + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: true + + /is-number-object@1.0.7: + resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + /is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /is-plain-object@5.0.0: + resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} + engines: {node: '>=0.10.0'} + dev: false + + /is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + dev: true + + /is-regex@1.1.4: + resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: true + + /is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + /is-string@1.0.7: + resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} + engines: {node: '>= 0.4'} + dependencies: + has-tostringtag: 1.0.2 + dev: true + + /is-symbol@1.0.4: + resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} + engines: {node: '>= 0.4'} + dependencies: + has-symbols: 1.0.3 + dev: true + + /is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + dependencies: + which-typed-array: 1.1.15 + dev: true + + /is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + dependencies: + call-bind: 1.0.7 + dev: true + + /isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + dev: true + + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + /isomorphic.js@0.2.5: + resolution: {integrity: sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==} + dev: false + + /istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + /istanbul-lib-instrument@5.2.1: + resolution: {integrity: sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==} + engines: {node: '>=8'} + dependencies: + '@babel/core': 7.24.3 + '@babel/parser': 7.24.1 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + /istanbul-lib-instrument@6.0.2: + resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.24.3 + '@babel/parser': 7.24.1 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.2 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + + /istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + /istanbul-lib-source-maps@4.0.1: + resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} + engines: {node: '>=10'} + dependencies: + debug: 4.3.4 + istanbul-lib-coverage: 3.2.2 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + /istanbul-reports@3.1.7: + resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} + engines: {node: '>=8'} + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + /jackspeak@2.3.6: + resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} + engines: {node: '>=14'} + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + dev: true + + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.1.0 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + /jest-cli@29.7.0(@types/node@20.11.30): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@20.11.30) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@20.11.30) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /jest-config@29.7.0(@types/node@20.11.30): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.24.3 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + babel-jest: 29.7.0(@babel/core@7.24.3) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + + /jest-environment-jsdom@29.6.2: + resolution: {integrity: sha512-7oa/+266AAEgkzae8i1awNEfTfjwawWKLpiw2XesZmaoVVj9u9t8JOYx18cG29rbPNtkUlZ8V4b5Jb36y/VxoQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/jsdom': 20.0.1 + '@types/node': 20.11.30 + jest-mock: 29.7.0 + jest-util: 29.7.0 + jsdom: 20.0.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + jest-mock: 29.7.0 + jest-util: 29.7.0 + + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.9 + '@types/node': 20.11.30 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.24.2 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + jest-util: 29.7.0 + + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.8 + resolve.exports: 2.0.2 + slash: 3.0.0 + + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + cjs-module-lexer: 1.2.3 + collect-v8-coverage: 1.0.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.24.3 + '@babel/generator': 7.24.1 + '@babel/plugin-syntax-jsx': 7.24.1(@babel/core@7.24.3) + '@babel/plugin-syntax-typescript': 7.24.1(@babel/core@7.24.3) + '@babel/types': 7.24.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.24.3) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.6.0 + transitivePeerDependencies: + - supports-color + + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + chalk: 4.1.2 + ci-info: 3.9.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 20.11.30 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 20.11.30 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + /jest@29.5.0(@types/node@20.11.30): + resolution: {integrity: sha512-juMg3he2uru1QoXX078zTa7pO85QyB9xajZc6bU+d9yEGwrKX6+vGmJQ3UdVZsvTEUARIdObzH68QItim6OSSQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0 + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@20.11.30) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + + /js-base64@3.7.5: + resolution: {integrity: sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==} + dev: false + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + /js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsdom@20.0.3: + resolution: {integrity: sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==} + engines: {node: '>=14'} + peerDependencies: + canvas: ^2.5.0 + peerDependenciesMeta: + canvas: + optional: true + dependencies: + abab: 2.0.6 + acorn: 8.11.3 + acorn-globals: 7.0.1 + cssom: 0.5.0 + cssstyle: 2.3.0 + data-urls: 3.0.2 + decimal.js: 10.4.3 + domexception: 4.0.0 + escodegen: 2.1.0 + form-data: 4.0.0 + html-encoding-sniffer: 3.0.0 + http-proxy-agent: 5.0.0 + https-proxy-agent: 5.0.1 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.7 + parse5: 7.1.2 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.3 + w3c-xmlserializer: 4.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 2.0.0 + whatwg-mimetype: 3.0.0 + whatwg-url: 11.0.0 + ws: 8.16.0 + xml-name-validator: 4.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: true + + /jsesc@2.5.2: + resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} + engines: {node: '>=4'} + hasBin: true + + /json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + dev: true + + /json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + /json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + /jsonc-parser@3.2.1: + resolution: {integrity: sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==} + dev: true + + /jsonfile@6.1.0: + resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + dev: true + + /jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + dependencies: + array-includes: 3.1.8 + array.prototype.flat: 1.3.2 + object.assign: 4.1.5 + object.values: 1.2.0 + dev: true + + /katex@0.16.7: + resolution: {integrity: sha512-Xk9C6oGKRwJTfqfIbtr0Kes9OSv6IFsuhFGc7tW4urlpMJtuh+7YhzU6YEG9n8gmWKcMAFzkp7nr+r69kV0zrA==} + hasBin: true + dependencies: + commander: 8.3.0 + dev: false + + /keycode@2.2.1: + resolution: {integrity: sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg==} + dev: false + + /keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + dependencies: + json-buffer: 3.0.1 + dev: true + + /kleur@3.0.3: + resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} + engines: {node: '>=6'} + + /kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + dev: true + + /leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + /levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /lib0@0.2.93: + resolution: {integrity: sha512-M5IKsiFJYulS+8Eal8f+zAqf5ckm1vffW0fFDxfgxJ+uiVopvDdd3PxJmz0GsVi3YNO7QCFSq0nAsiDmNhLj9Q==} + engines: {node: '>=16'} + hasBin: true + dependencies: + isomorphic.js: 0.2.5 + dev: false + + /lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + dev: true + + /lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + /locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + dependencies: + p-locate: 4.1.0 + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + + /lodash.clonedeep@4.5.0: + resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + + /lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + + /lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + dev: true + + /lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} + dependencies: + tslib: 2.6.2 + dev: true + + /lru-cache@10.2.0: + resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} + engines: {node: 14 || >=16.14} + dev: true + + /lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + dependencies: + yallist: 3.1.1 + + /lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + + /luxon@3.4.4: + resolution: {integrity: sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==} + engines: {node: '>=12'} + dev: false + + /magic-string@0.30.8: + resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} + engines: {node: '>=12'} + dependencies: + '@jridgewell/sourcemap-codec': 1.4.15 + dev: true + + /make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + dependencies: + semver: 7.6.0 + + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: true + + /makeerror@1.0.12: + resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==} + dependencies: + tmpl: 1.0.5 + + /material-colors@1.2.6: + resolution: {integrity: sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==} + dev: false + + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: true + + /mdn-data@2.0.30: + resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} + dev: true + + /memoize-one@5.2.1: + resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} + dev: false + + /memoize-one@6.0.0: + resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} + dev: false + + /merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + /merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch@4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + + /mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimatch@9.0.4: + resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + + /mkdirp@1.0.4: + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} + engines: {node: '>=10'} + hasBin: true + dev: true + + /moment-timezone@0.5.45: + resolution: {integrity: sha512-HIWmqA86KcmCAhnMAN0wuDOARV/525R2+lOLotuGFzn4HO+FH+/645z2wx0Dt3iDv6/p61SIvKnDstISainhLQ==} + dependencies: + moment: 2.30.1 + dev: false + + /moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + dev: false + + /mrmime@2.0.0: + resolution: {integrity: sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==} + engines: {node: '>=10'} + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + /nanoid@3.3.7: + resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + /nanoid@4.0.0: + resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==} + engines: {node: ^14 || ^16 || >=18} + hasBin: true + dev: false + + /natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} + dependencies: + lower-case: 2.0.2 + tslib: 2.6.2 + dev: true + + /node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + /node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + /normalize-range@0.1.2: + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} + engines: {node: '>=0.10.0'} + dev: true + + /npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + dependencies: + path-key: 3.1.1 + + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: true + + /nwsapi@2.2.7: + resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} + dev: true + + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + /object-hash@3.0.0: + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} + engines: {node: '>= 6'} + dev: true + + /object-inspect@1.13.1: + resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} + dev: true + + /object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + dev: false + + /object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + /object.assign@4.1.5: + resolution: {integrity: sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + has-symbols: 1.0.3 + object-keys: 1.1.1 + dev: true + + /object.entries@1.1.8: + resolution: {integrity: sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /object.hasown@1.1.4: + resolution: {integrity: sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==} + engines: {node: '>= 0.4'} + dependencies: + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /object.values@1.2.0: + resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + + /onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + dependencies: + mimic-fn: 2.1.0 + + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} + engines: {node: '>= 0.8.0'} + dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + dependencies: + p-try: 2.2.0 + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + + /p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + dependencies: + p-limit: 2.3.0 + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + /param-case@3.0.4: + resolution: {integrity: sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /parchment@1.1.4: + resolution: {integrity: sha512-J5FBQt/pM2inLzg4hEWmzQx/8h8D0CiDxaG3vyp9rKrQRSDgBlhjdP5jQGgosEajXPSQouXGHOmVdgo7QmJuOg==} + + /parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + + /parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + dependencies: + '@babel/code-frame': 7.24.2 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: true + + /pascal-case@3.1.2: + resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /path-case@3.0.4: + resolution: {integrity: sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + /path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + /path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + /path-scurry@1.10.2: + resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + lru-cache: 10.2.0 + minipass: 7.0.4 + dev: true + + /path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + /performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + + /picocolors@1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + /pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + dev: true + + /pirates@4.0.6: + resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} + engines: {node: '>= 6'} + + /pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + dependencies: + find-up: 4.1.0 + + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: true + + /postcss-import@14.1.0(postcss@8.4.21): + resolution: {integrity: sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==} + engines: {node: '>=10.0.0'} + peerDependencies: + postcss: ^8.0.0 + dependencies: + postcss: 8.4.21 + postcss-value-parser: 4.2.0 + read-cache: 1.0.0 + resolve: 1.22.8 + dev: true + + /postcss-js@4.0.1(postcss@8.4.21): + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} + engines: {node: ^12 || ^14 || >= 16} + peerDependencies: + postcss: ^8.4.21 + dependencies: + camelcase-css: 2.0.1 + postcss: 8.4.21 + dev: true + + /postcss-load-config@3.1.4(postcss@8.4.21): + resolution: {integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==} + engines: {node: '>= 10'} + peerDependencies: + postcss: '>=8.0.9' + ts-node: '>=9.0.0' + peerDependenciesMeta: + postcss: + optional: true + ts-node: + optional: true + dependencies: + lilconfig: 2.1.0 + postcss: 8.4.21 + yaml: 1.10.2 + dev: true + + /postcss-nested@6.0.0(postcss@8.4.21): + resolution: {integrity: sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.2.14 + dependencies: + postcss: 8.4.21 + postcss-selector-parser: 6.0.16 + dev: true + + /postcss-selector-parser@6.0.16: + resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: true + + /postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + dev: true + + /postcss@8.4.21: + resolution: {integrity: sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + dev: true + + /postcss@8.4.38: + resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.7 + picocolors: 1.0.0 + source-map-js: 1.2.0 + + /prefix-style@2.0.1: + resolution: {integrity: sha512-gdr1MBNVT0drzTq95CbSNdsrBDoHGlb2aDJP/FoY+1e+jSDPOb1Cv554gH2MGiSr2WTcXi/zu+NaFzfcHQkfBQ==} + dev: false + + /prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-plugin-tailwindcss@0.2.2(prettier@2.8.4): + resolution: {integrity: sha512-5RjUbWRe305pUpc48MosoIp6uxZvZxrM6GyOgsbGLTce+ehePKNm7ziW2dLG2air9aXbGuXlHVSQQw4Lbosq3w==} + engines: {node: '>=12.17.0'} + peerDependencies: + '@prettier/plugin-php': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@shufo/prettier-plugin-blade': '*' + '@trivago/prettier-plugin-sort-imports': '*' + prettier: '>=2.2.0' + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + prettier-plugin-twig-melody: '*' + peerDependenciesMeta: + '@prettier/plugin-php': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@shufo/prettier-plugin-blade': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + prettier-plugin-twig-melody: + optional: true + dependencies: + prettier: 2.8.4 + dev: true + + /prettier@2.8.4: + resolution: {integrity: sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.2.0 + + /prismjs@1.29.0: + resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} + engines: {node: '>=6'} + dev: false + + /prompts@2.4.2: + resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} + engines: {node: '>= 6'} + dependencies: + kleur: 3.0.3 + sisteransi: 1.0.5 + + /prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + /protoc-gen-ts@0.8.7: + resolution: {integrity: sha512-jr4VJey2J9LVYCV7EVyVe53g1VMw28cCmYJhBe5e3YX5wiyiDwgxWxeDf9oTqAe4P1bN/YGAkW2jhlH8LohwiQ==} + hasBin: true + dev: false + + /proxy-compare@2.5.1: + resolution: {integrity: sha512-oyfc0Tx87Cpwva5ZXezSp5V9vht1c7dZBhvuV/y3ctkgMVUmiAGDVeeB0dKhGSyT0v1ZTEQYpe/RXlBVBNuCLA==} + dev: false + + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + + /psl@1.9.0: + resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} + dev: true + + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + + /pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + /querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + dev: true + + /queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + dev: true + + /quill-delta@3.6.3: + resolution: {integrity: sha512-wdIGBlcX13tCHOXGMVnnTVFtGRLoP0imqxM696fIPwIf5ODIYUHIvHbZcyvGlZFiFhK5XzDC2lpjbxRhnM05Tg==} + engines: {node: '>=0.10'} + dependencies: + deep-equal: 1.1.2 + extend: 3.0.2 + fast-diff: 1.1.2 + dev: false + + /quill-delta@4.2.2: + resolution: {integrity: sha512-qjbn82b/yJzOjstBgkhtBjN2TNK+ZHP/BgUQO+j6bRhWQQdmj2lH6hXG7+nwwLF41Xgn//7/83lxs9n2BkTtTg==} + dependencies: + fast-diff: 1.2.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: true + + /quill-delta@5.1.0: + resolution: {integrity: sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==} + engines: {node: '>= 12.0.0'} + dependencies: + fast-diff: 1.3.0 + lodash.clonedeep: 4.5.0 + lodash.isequal: 4.5.0 + dev: false + + /quill@1.3.7: + resolution: {integrity: sha512-hG/DVzh/TiknWtE6QmWAF/pxoZKYxfe3J/d/+ShUWkDvvkZQVTPeVmUJVu1uE6DDooC4fWTiCLh84ul89oNz5g==} + dependencies: + clone: 2.1.2 + deep-equal: 1.1.2 + eventemitter3: 2.0.3 + extend: 3.0.2 + parchment: 1.1.4 + quill-delta: 3.6.3 + dev: false + + /raf-schd@4.0.3: + resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} + dev: false + + /raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + dependencies: + performance-now: 2.1.0 + dev: false + + /react-beautiful-dnd@13.1.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==} + peerDependencies: + react: ^16.8.5 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.5 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + css-box-model: 1.2.1 + memoize-one: 5.2.1 + raf-schd: 4.0.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-redux: 7.2.9(react-dom@18.2.0)(react@18.2.0) + redux: 4.2.1 + use-memo-one: 1.1.3(react@18.2.0) + transitivePeerDependencies: + - react-native + dev: false + + /react-big-calendar@1.8.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-cra8WPfoTSQthFfqxi0k9xm/Shv5jWSw19LkNzpSJcnQhP6XGes/eJjd8P8g/iwaJjXIWPpg3+HB5wO5wabRyA==} + peerDependencies: + react: ^16.14.0 || ^17 || ^18 + react-dom: ^16.14.0 || ^17 || ^18 + dependencies: + '@babel/runtime': 7.24.1 + clsx: 1.2.1 + date-arithmetic: 4.1.0 + dayjs: 1.11.9 + dom-helpers: 5.2.1 + globalize: 0.1.1 + invariant: 2.2.4 + lodash: 4.17.21 + lodash-es: 4.17.21 + luxon: 3.4.4 + memoize-one: 6.0.0 + moment: 2.30.1 + moment-timezone: 0.5.45 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-overlays: 5.2.1(react-dom@18.2.0)(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + dev: false + + /react-color@2.19.3(react@18.2.0): + resolution: {integrity: sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==} + peerDependencies: + react: '*' + dependencies: + '@icons/material': 0.2.4(react@18.2.0) + lodash: 4.17.21 + lodash-es: 4.17.21 + material-colors: 1.2.6 + prop-types: 15.8.1 + react: 18.2.0 + reactcss: 1.2.3(react@18.2.0) + tinycolor2: 1.6.0 + dev: false + + /react-custom-scrollbars@4.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-VtJTUvZ7kPh/auZWIbBRceGPkE30XBYe+HktFxuMWBR2eVQQ+Ur6yFJMoaYcNpyGq22uYJ9Wx4UAEcC0K+LNPQ==} + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + react-dom: ^0.14.0 || ^15.0.0 || ^16.0.0 + dependencies: + dom-css: 2.1.0 + prop-types: 15.8.1 + raf: 3.4.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-datepicker@4.23.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-w+msqlOZ14v6H1UknTKtZw/dw9naFMgAOspf59eY130gWpvy5dvKj/bgsFICDdvxB7PtKWxDcbGlAqCloY1d2A==} + peerDependencies: + react: ^16.9.0 || ^17 || ^18 + react-dom: ^16.9.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + classnames: 2.5.1 + date-fns: 2.30.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-onclickoutside: 6.13.0(react-dom@18.2.0)(react@18.2.0) + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-dom@18.2.0(react@18.2.0): + resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} + peerDependencies: + react: ^18.2.0 + dependencies: + loose-envify: 1.4.0 + react: 18.2.0 + scheduler: 0.23.0 + + /react-error-boundary@4.0.13(react@18.2.0): + resolution: {integrity: sha512-b6PwbdSv8XeOSYvjt8LpgpKrZ0yGdtZokYwkwV2wlcZbxgopHX/hgPl5VgpnoVOWd868n1hktM8Qm4b+02MiLQ==} + peerDependencies: + react: '>=16.13.1' + dependencies: + '@babel/runtime': 7.24.1 + react: 18.2.0 + dev: false + + /react-event-listener@0.6.6(react@18.2.0): + resolution: {integrity: sha512-+hCNqfy7o9wvO6UgjqFmBzARJS7qrNoda0VqzvOuioEpoEXKutiKuv92dSz6kP7rYLmyHPyYNLesi5t/aH1gfw==} + peerDependencies: + react: ^16.3.0 + dependencies: + '@babel/runtime': 7.24.1 + prop-types: 15.8.1 + react: 18.2.0 + warning: 4.0.3 + dev: false + + /react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + /react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + dependencies: + goober: 2.1.14(csstype@3.1.3) + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + transitivePeerDependencies: + - csstype + dev: false + + /react-i18next@14.1.0(i18next@22.4.10)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-3KwX6LHpbvGQ+sBEntjV4sYW3Zovjjl3fpoHbUwSgFHf0uRBcbeCBLR5al6ikncI5+W0EFb71QXZmfop+J6NrQ==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + html-parse-stringify: 3.0.1 + i18next: 22.4.10 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + /react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + dev: false + + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} + + /react-katex@3.0.1(prop-types@15.8.1)(react@18.2.0): + resolution: {integrity: sha512-wIUW1fU5dHlkKvq4POfDkHruQsYp3fM8xNb/jnc8dnQ+nNCnaj0sx5pw7E6UyuEdLRyFKK0HZjmXBo+AtXXy0A==} + peerDependencies: + prop-types: ^15.8.1 + react: '>=15.3.2 <=18' + dependencies: + katex: 0.16.7 + prop-types: 15.8.1 + react: 18.2.0 + dev: false + + /react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + dev: false + + /react-onclickoutside@6.13.0(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==} + peerDependencies: + react: ^15.5.x || ^16.x || ^17.x || ^18.x + react-dom: ^15.5.x || ^16.x || ^17.x || ^18.x + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-overlays@5.2.1(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==} + peerDependencies: + react: '>=16.3.0' + react-dom: '>=16.3.0' + dependencies: + '@babel/runtime': 7.24.1 + '@popperjs/core': 2.11.8 + '@restart/hooks': 0.4.16(react@18.2.0) + '@types/warning': 3.0.3 + dom-helpers: 5.2.1 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + uncontrollable: 7.2.1(react@18.2.0) + warning: 4.0.3 + dev: false + + /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + dependencies: + '@popperjs/core': 2.11.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-fast-compare: 3.2.2 + warning: 4.0.3 + + /react-redux@7.2.9(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==} + peerDependencies: + react: ^16.8.3 || ^17 || ^18 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/react-redux': 7.1.33 + hoist-non-react-statics: 3.3.2 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 17.0.2 + dev: false + + /react-redux@8.0.5(@types/react-dom@18.2.22)(@types/react@18.2.66)(react-dom@18.2.0)(react@18.2.0)(redux@4.2.1): + resolution: {integrity: sha512-Q2f6fCKxPFpkXt1qNRZdEDLlScsDWyrgSj0mliK59qU6W5gvBiKkdMEG2lJzhd1rCctf0hb6EtePPLZ2e0m1uw==} + peerDependencies: + '@types/react': ^16.8 || ^17.0 || ^18.0 + '@types/react-dom': ^16.8 || ^17.0 || ^18.0 + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + react-native: '>=0.59' + redux: ^4 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + react-dom: + optional: true + react-native: + optional: true + redux: + optional: true + dependencies: + '@babel/runtime': 7.24.1 + '@types/hoist-non-react-statics': 3.3.5 + '@types/react': 18.2.66 + '@types/react-dom': 18.2.22 + '@types/use-sync-external-store': 0.0.3 + hoist-non-react-statics: 3.3.2 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-is: 18.2.0 + redux: 4.2.1 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /react-refresh@0.14.0: + resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} + engines: {node: '>=0.10.0'} + dev: true + + /react-router-dom@6.22.3(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-7ZILI7HjcE+p31oQvwbokjk6OA/bnFxrhJ19n82Ex9Ph8fNAq+Hm/7KchpMGlTgWhUxRHMMCut+vEtNpWpowKw==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + dependencies: + '@remix-run/router': 1.15.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-router: 6.22.3(react@18.2.0) + dev: false + + /react-router@6.22.3(react@18.2.0): + resolution: {integrity: sha512-dr2eb3Mj5zK2YISHK++foM9w4eBnO23eKnZEDs7c880P6oKbrjz/Svg9+nxqtHQK+oMW4OtjZca0RqPglXxguQ==} + engines: {node: '>=14.0.0'} + peerDependencies: + react: '>=16.8' + dependencies: + '@remix-run/router': 1.15.3 + react: 18.2.0 + dev: false + + /react-swipeable-views-core@0.14.0: + resolution: {integrity: sha512-0W/e9uPweNEOSPjmYtuKSC/SvKKg1sfo+WtPdnxeLF3t2L82h7jjszuOHz9C23fzkvLfdgkaOmcbAxE9w2GEjA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + warning: 4.0.3 + dev: false + + /react-swipeable-views-utils@0.14.0(react@18.2.0): + resolution: {integrity: sha512-W+fXBOsDqgFK1/g7MzRMVcDurp3LqO3ksC8UgInh2P/tKgb5DusuuB1geKHFc6o1wKl+4oyER4Zh3Lxmr8xbXA==} + engines: {node: '>=6.0.0'} + dependencies: + '@babel/runtime': 7.0.0 + keycode: 2.2.1 + prop-types: 15.8.1 + react-event-listener: 0.6.6(react@18.2.0) + react-swipeable-views-core: 0.14.0 + shallow-equal: 1.2.1 + transitivePeerDependencies: + - react + dev: false + + /react-swipeable-views@0.14.0(react@18.2.0): + resolution: {integrity: sha512-wrTT6bi2nC3JbmyNAsPXffUXLn0DVT9SbbcFr36gKpbaCgEp7rX/OFxsu5hPc/NBsUhHyoSRGvwqJNNrWTwCww==} + engines: {node: '>=6.0.0'} + peerDependencies: + react: ^15.3.0 || ^16.0.0 || ^17.0.0 + dependencies: + '@babel/runtime': 7.0.0 + prop-types: 15.8.1 + react: 18.2.0 + react-swipeable-views-core: 0.14.0 + react-swipeable-views-utils: 0.14.0(react@18.2.0) + warning: 4.0.3 + dev: false + + /react-transition-group@4.4.5(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + dependencies: + '@babel/runtime': 7.24.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-virtualized-auto-sizer@1.0.20(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-OdIyHwj4S4wyhbKHOKM1wLSj/UDXm839Z3Cvfg2a9j+He6yDa6i5p0qQvEiCnyQlGO/HyfSnigQwuxvYalaAXA==} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + react-dom: ^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0-rc + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react-vtree@2.0.4(@types/react-window@1.8.8)(react-dom@18.2.0)(react-window@1.8.10)(react@18.2.0): + resolution: {integrity: sha512-UOld0VqyAZrryF06K753X4bcEVN6/wW831exvVlMZeZAVHk9KXnlHs4rpqDAeoiBgUwJqoW/rtn0hwsokRRxPA==} + peerDependencies: + '@types/react-window': ^1.8.2 + react: ^16.13.1 + react-dom: ^16.13.1 + react-window: ^1.8.5 + dependencies: + '@babel/runtime': 7.24.1 + '@types/react-window': 1.8.8 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-window: 1.8.10(react-dom@18.2.0)(react@18.2.0) + dev: false + + /react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + dependencies: + '@babel/runtime': 7.24.1 + memoize-one: 5.2.1 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react18-input-otp@1.1.2(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-E21NiPh/KH67Bq/uEAm78E8H+croiGAyX5WcXfX49qh0im1iKrk/3RCKCTESG6WUoJYyh/fj5JY0UrHm+Mm0eQ==} + peerDependencies: + react: 16.2.0 - 18 + react-dom: 16.2.0 - 18 + dependencies: + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + + /react@18.2.0: + resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + + /reactcss@1.2.3(react@18.2.0): + resolution: {integrity: sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==} + peerDependencies: + react: '*' + dependencies: + lodash: 4.17.21 + react: 18.2.0 + dev: false + + /read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + dependencies: + pify: 2.3.0 + dev: true + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + + /redux-thunk@3.1.0(redux@5.0.1): + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + dependencies: + redux: 5.0.1 + dev: false + + /redux@4.2.1: + resolution: {integrity: sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==} + dependencies: + '@babel/runtime': 7.24.1 + dev: false + + /redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + dev: false + + /regenerator-runtime@0.12.1: + resolution: {integrity: sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==} + dev: false + + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + + /regexp.prototype.flags@1.5.2: + resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-errors: 1.3.0 + set-function-name: 2.0.2 + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + /requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + dev: true + + /reselect@5.0.1: + resolution: {integrity: sha512-D72j2ubjgHpvuCiORWkOUxndHJrxDaSolheiz5CO+roz8ka97/4msh2E8F5qay4GawR5vzBt5MkbDHT+Rdy/Wg==} + dev: false + + /resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + dependencies: + resolve-from: 5.0.0 + + /resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + /resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + /resolve.exports@2.0.2: + resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==} + engines: {node: '>=10'} + + /resolve@1.22.8: + resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + /resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + dependencies: + is-core-module: 2.13.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify@1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf@2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup@4.13.2: + resolution: {integrity: sha512-MIlLgsdMprDBXC+4hsPgzWUasLO9CE4zOkj/u6j+Z6j5A4zRY+CtiXAdJyPtgCsc42g658Aeh1DlrdVEJhsL2g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + dependencies: + '@types/estree': 1.0.5 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.13.2 + '@rollup/rollup-android-arm64': 4.13.2 + '@rollup/rollup-darwin-arm64': 4.13.2 + '@rollup/rollup-darwin-x64': 4.13.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.13.2 + '@rollup/rollup-linux-arm64-gnu': 4.13.2 + '@rollup/rollup-linux-arm64-musl': 4.13.2 + '@rollup/rollup-linux-powerpc64le-gnu': 4.13.2 + '@rollup/rollup-linux-riscv64-gnu': 4.13.2 + '@rollup/rollup-linux-s390x-gnu': 4.13.2 + '@rollup/rollup-linux-x64-gnu': 4.13.2 + '@rollup/rollup-linux-x64-musl': 4.13.2 + '@rollup/rollup-win32-arm64-msvc': 4.13.2 + '@rollup/rollup-win32-ia32-msvc': 4.13.2 + '@rollup/rollup-win32-x64-msvc': 4.13.2 + fsevents: 2.3.3 + + /run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /rxjs@7.8.0: + resolution: {integrity: sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==} + dependencies: + tslib: 2.6.2 + dev: false + + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: true + + /safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + dev: true + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: true + + /sass@1.70.0: + resolution: {integrity: sha512-uUxNQ3zAHeAx5nRFskBnrWzDUJrrvpCPD5FNAoRvTi0WwremlheES3tg+56PaVtCs5QDRX5CBLxxKMDJMEa1WQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + chokidar: 3.6.0 + immutable: 4.3.5 + source-map-js: 1.2.0 + + /saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + dependencies: + xmlchars: 2.2.0 + dev: true + + /scheduler@0.23.0: + resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + dependencies: + loose-envify: 1.4.0 + + /scroll-into-view-if-needed@3.1.0: + resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + dependencies: + compute-scroll-into-view: 3.1.0 + dev: false + + /semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + /semver@7.6.0: + resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + + /sentence-case@3.0.4: + resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + dependencies: + no-case: 3.0.4 + tslib: 2.6.2 + upper-case-first: 2.0.2 + dev: true + + /set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + + /set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + /shallow-equal@1.2.1: + resolution: {integrity: sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==} + dev: false + + /shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + + /shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + /side-channel@1.0.6: + resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + object-inspect: 1.13.1 + dev: true + + /signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + + /sirv@2.0.4: + resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.25 + mrmime: 2.0.0 + totalist: 3.0.1 + dev: true + + /sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + + /slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + /slate-history@0.100.0(slate@0.101.4): + resolution: {integrity: sha512-x5rUuWLNtH97hs9PrFovGgt3Qc5zkTm/5mcUB+0NR/TK923eLax4HsL6xACLHMs245nI6aJElyM1y6hN0y5W/Q==} + peerDependencies: + slate: '>=0.65.3' + dependencies: + is-plain-object: 5.0.0 + slate: 0.101.4 + dev: false + + /slate-react@0.101.3(react-dom@18.2.0)(react@18.2.0)(slate@0.101.4): + resolution: {integrity: sha512-KMXK9FLeS7HYhhoVcI8SUi4Qp1I9C1lTQ2EgbPH95sVXfH/vq+hbhurEGIGCe0VQ9Opj4rSKJIv/g7De1+nJMA==} + peerDependencies: + react: '>=18.2.0' + react-dom: '>=18.2.0' + slate: '>=0.99.0' + dependencies: + '@juggle/resize-observer': 3.4.0 + '@types/is-hotkey': 0.1.10 + '@types/lodash': 4.17.0 + direction: 1.0.4 + is-hotkey: 0.2.0 + is-plain-object: 5.0.0 + lodash: 4.17.21 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + scroll-into-view-if-needed: 3.1.0 + slate: 0.101.4 + tiny-invariant: 1.3.1 + dev: false + + /slate@0.101.4: + resolution: {integrity: sha512-8LazZrNDsYFKDg1wpb0HouAfX5Pw/UmOZ/vIrtqD2GSCDZvraOkV2nVJ9Ery8kIlsU1jeybwgcaCy4KkVwfvEg==} + dependencies: + immer: 10.0.4 + is-plain-object: 5.0.0 + tiny-warning: 1.0.3 + dev: false + + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} + dependencies: + dot-case: 3.0.4 + tslib: 2.6.2 + dev: true + + /source-map-js@1.2.0: + resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + engines: {node: '>=0.10.0'} + + /source-map-support@0.5.13: + resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + dev: false + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + /sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + /stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + dependencies: + escape-string-regexp: 2.0.0 + + /string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + /string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.0 + dev: true + + /string.prototype.matchall@4.0.11: + resolution: {integrity: sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + get-intrinsic: 1.2.4 + gopd: 1.0.1 + has-symbols: 1.0.3 + internal-slot: 1.0.7 + regexp.prototype.flags: 1.5.2 + set-function-name: 2.0.2 + side-channel: 1.0.6 + dev: true + + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + dev: true + + /strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + /strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + /strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + /style-dictionary@3.9.2: + resolution: {integrity: sha512-M2pcQ6hyRtqHOh+NyT6T05R3pD/gwNpuhREBKvxC1En0vyywx+9Wy9nXWT1SZ9ePzv1vAo65ItnpA16tT9ZUCg==} + engines: {node: '>=12.0.0'} + hasBin: true + dependencies: + chalk: 4.1.2 + change-case: 4.1.2 + commander: 8.3.0 + fs-extra: 10.1.0 + glob: 10.3.12 + json5: 2.2.3 + jsonc-parser: 3.2.1 + lodash: 4.17.21 + tinycolor2: 1.6.0 + dev: true + + /stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + dev: false + + /supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + dependencies: + has-flag: 3.0.0 + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + + /supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + /svg-parser@2.0.4: + resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + dev: true + + /svgo@3.2.0: + resolution: {integrity: sha512-4PP6CMW/V7l/GmKRKzsLR8xxjdHTV4IMvhTnpuHwwBazSIlw5W/5SmPjN8Dwyt7lKbSJrRDgp4t9ph0HgChFBQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + css-what: 6.1.0 + csso: 5.0.5 + picocolors: 1.0.0 + dev: true + + /symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + dev: true + + /tailwindcss@3.2.7(postcss@8.4.21): + resolution: {integrity: sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==} + engines: {node: '>=12.13.0'} + hasBin: true + peerDependencies: + postcss: ^8.0.9 + dependencies: + arg: 5.0.2 + chokidar: 3.6.0 + color-name: 1.1.4 + detective: 5.2.1 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.2 + glob-parent: 6.0.2 + is-glob: 4.0.3 + lilconfig: 2.1.0 + micromatch: 4.0.5 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.0.0 + postcss: 8.4.21 + postcss-import: 14.1.0(postcss@8.4.21) + postcss-js: 4.0.1(postcss@8.4.21) + postcss-load-config: 3.1.4(postcss@8.4.21) + postcss-nested: 6.0.0(postcss@8.4.21) + postcss-selector-parser: 6.0.16 + postcss-value-parser: 4.2.0 + quick-lru: 5.1.1 + resolve: 1.22.8 + transitivePeerDependencies: + - ts-node + dev: true + + /test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + /text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tiny-invariant@1.3.1: + resolution: {integrity: sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw==} + dev: false + + /tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + dev: false + + /tiny-warning@1.0.3: + resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + dev: false + + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + /tmpl@1.0.5: + resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} + + /to-camel-case@1.0.0: + resolution: {integrity: sha512-nD8pQi5H34kyu1QDMFjzEIYqk0xa9Alt6ZfrdEMuHCFOfTLhDG5pgTu/aAM9Wt9lXILwlXmWP43b8sav0GNE8Q==} + dependencies: + to-space-case: 1.0.0 + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + + /to-no-case@1.0.2: + resolution: {integrity: sha512-Z3g735FxuZY8rodxV4gH7LxClE4H0hTIyHNIHdk+vpQxjLm0cwnKXq/OFVZ76SOQmto7txVcwSCwkU5kqp+FKg==} + dev: false + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + + /to-space-case@1.0.0: + resolution: {integrity: sha512-rLdvwXZ39VOn1IxGL3V6ZstoTbwLRckQmn/U8ZDLuWwIXNpuZDhQ3AiRUlhTbOXFVE9C+dR51wM0CBDhk31VcA==} + dependencies: + to-no-case: 1.0.2 + dev: false + + /totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + dev: true + + /tough-cookie@4.1.3: + resolution: {integrity: sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==} + engines: {node: '>=6'} + dependencies: + psl: 1.9.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + dev: true + + /tr46@3.0.0: + resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} + engines: {node: '>=12'} + dependencies: + punycode: 2.3.1 + dev: true + + /tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + dev: true + + /ts-api-utils@1.3.0(typescript@4.9.5): + resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} + engines: {node: '>=16'} + peerDependencies: + typescript: '>=4.2.0' + dependencies: + typescript: 4.9.5 + dev: true + + /ts-jest@29.1.1(@babel/core@7.24.3)(babel-jest@29.6.2)(jest@29.5.0)(typescript@4.9.5): + resolution: {integrity: sha512-D6xjnnbP17cC85nliwGiL+tpoKN0StpgE0TeOjXQTU6MVCfsB4v7aW05CgQ/1OywGb0x/oy9hHFnN+sczTiRaA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.24.3 + babel-jest: 29.6.2(@babel/core@7.24.3) + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@20.11.30) + jest-util: 29.7.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.6.0 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + + /ts-node-dev@2.0.0(@types/node@20.11.30)(typescript@4.9.5): + resolution: {integrity: sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==} + engines: {node: '>=0.8.0'} + hasBin: true + peerDependencies: + node-notifier: '*' + typescript: '*' + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + chokidar: 3.6.0 + dynamic-dedupe: 0.3.0 + minimist: 1.2.8 + mkdirp: 1.0.4 + resolve: 1.22.8 + rimraf: 2.7.1 + source-map-support: 0.5.21 + tree-kill: 1.2.2 + ts-node: 10.9.2(@types/node@20.11.30)(typescript@4.9.5) + tsconfig: 7.0.0 + typescript: 4.9.5 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + - '@types/node' + dev: true + + /ts-node@10.9.2(@types/node@20.11.30)(typescript@4.9.5): + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.11.30 + acorn: 8.11.3 + acorn-walk: 8.3.2 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + dev: true + + /ts-results@3.3.0: + resolution: {integrity: sha512-FWqxGX2NHp5oCyaMd96o2y2uMQmSu8Dey6kvyuFdRJ2AzfmWo3kWa4UsPlCGlfQ/qu03m09ZZtppMoY8EMHuiA==} + dev: false + + /tsconfig-paths-jest@0.0.1: + resolution: {integrity: sha512-YKhUKqbteklNppC2NqL7dv1cWF8eEobgHVD5kjF1y9Q4ocqpBiaDlYslQ9eMhtbqIPRrA68RIEXqknEjlxdwxw==} + dev: true + + /tsconfig@7.0.0: + resolution: {integrity: sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==} + dependencies: + '@types/strip-bom': 3.0.0 + '@types/strip-json-comments': 0.0.30 + strip-bom: 3.0.0 + strip-json-comments: 2.0.1 + dev: true + + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + + /type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + /type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: true + + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: true + + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /ufo@1.5.3: + resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} + dev: true + + /unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + dev: true + + /uncontrollable@7.2.1(react@18.2.0): + resolution: {integrity: sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==} + peerDependencies: + react: '>=15.0.0' + dependencies: + '@babel/runtime': 7.24.1 + '@types/react': 18.2.66 + invariant: 2.2.4 + react: 18.2.0 + react-lifecycles-compat: 3.0.4 + dev: false + + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + /universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + dev: true + + /universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + dev: true + + /unsplash-js@7.0.19: + resolution: {integrity: sha512-j6qT2floy5Q2g2d939FJpwey1yw/GpQecFiSouyJtsHQPj3oqmqq3K4rI+GF8vU1zwGCT7ZwIGQd2dtCQLjYJw==} + engines: {node: '>=10'} + dev: false + + /update-browserslist-db@1.0.13(browserslist@4.23.0): + resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + dependencies: + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.0 + + /upper-case-first@2.0.2: + resolution: {integrity: sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==} + dependencies: + tslib: 2.6.2 + dev: true + + /upper-case@2.0.2: + resolution: {integrity: sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==} + dependencies: + tslib: 2.6.2 + dev: true + + /uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.3.1 + dev: true + + /url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + dev: true + + /use-memo-one@1.1.3(react@18.2.0): + resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /use-sync-external-store@1.2.0(react@18.2.0): + resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + dependencies: + react: 18.2.0 + dev: false + + /utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + dev: false + + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: true + + /uuid@9.0.0: + resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} + hasBin: true + dev: true + + /v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + dev: true + + /v8-to-istanbul@9.2.0: + resolution: {integrity: sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==} + engines: {node: '>=10.12.0'} + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + /valtio@1.12.1(@types/react@18.2.66)(react@18.2.0): + resolution: {integrity: sha512-R0V4H86Xi2Pp7pmxN/EtV4Q6jr6PMN3t1IwxEvKUp6160r8FimvPh941oWyeK1iec/DTsh9Jb3Q+GputMS8SYg==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=16.8' + react: '>=16.8' + peerDependenciesMeta: + '@types/react': + optional: true + react: + optional: true + dependencies: + '@types/react': 18.2.66 + derive-valtio: 0.1.0(valtio@1.12.1) + proxy-compare: 2.5.1 + react: 18.2.0 + use-sync-external-store: 1.2.0(react@18.2.0) + dev: false + + /vite-plugin-svgr@3.2.0(typescript@4.9.5)(vite@5.2.0): + resolution: {integrity: sha512-Uvq6niTvhqJU6ga78qLKBFJSDvxWhOnyfQSoKpDPMAGxJPo5S3+9hyjExE5YDj6Lpa4uaLkGc1cBgxXov+LjSw==} + peerDependencies: + vite: ^2.6.0 || 3 || 4 + dependencies: + '@rollup/pluginutils': 5.1.0 + '@svgr/core': 7.0.0(typescript@4.9.5) + '@svgr/plugin-jsx': 7.0.0 + vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + transitivePeerDependencies: + - rollup + - supports-color + - typescript + dev: true + + /vite-plugin-terminal@1.2.0(vite@5.2.0): + resolution: {integrity: sha512-IIw1V+IySth8xlrGmH4U7YmfTp681vTzYpa7b8A3KNCJ2oW1BGPPwW8tSz6BQTvSgbRmrP/9NsBLsfXkN4e8sA==} + engines: {node: '>=14'} + peerDependencies: + vite: ^2.0.0||^3.0.0||^4.0.0||^5.0.0 + dependencies: + '@rollup/plugin-strip': 3.0.4 + debug: 4.3.4 + kolorist: 1.8.0 + sirv: 2.0.4 + ufo: 1.5.3 + vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + transitivePeerDependencies: + - rollup + - supports-color + dev: true + + /vite-plugin-wasm@3.3.0(vite@5.2.0): + resolution: {integrity: sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==} + peerDependencies: + vite: ^2 || ^3 || ^4 || ^5 + dependencies: + vite: 5.2.0(@types/node@20.11.30)(sass@1.70.0) + dev: false + + /vite@5.2.0(@types/node@20.11.30)(sass@1.70.0): + resolution: {integrity: sha512-xMSLJNEjNk/3DJRgWlPADDwaU9AgYRodDH2t6oENhJnIlmU9Hx1Q6VpjyXua/JdMw1WJRbnAgHJ9xgET9gnIAg==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + '@types/node': 20.11.30 + esbuild: 0.20.2 + postcss: 8.4.38 + rollup: 4.13.2 + sass: 1.70.0 + optionalDependencies: + fsevents: 2.3.3 + + /void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + dev: false + + /w3c-xmlserializer@4.0.0: + resolution: {integrity: sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==} + engines: {node: '>=14'} + dependencies: + xml-name-validator: 4.0.0 + dev: true + + /walker@1.0.8: + resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} + dependencies: + makeerror: 1.0.12 + + /warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + dependencies: + loose-envify: 1.4.0 + + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + + /whatwg-encoding@2.0.0: + resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} + engines: {node: '>=12'} + dependencies: + iconv-lite: 0.6.3 + dev: true + + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + + /whatwg-url@11.0.0: + resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} + engines: {node: '>=12'} + dependencies: + tr46: 3.0.0 + webidl-conversions: 7.0.0 + dev: true + + /which-boxed-primitive@1.0.2: + resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} + dependencies: + is-bigint: 1.0.4 + is-boolean-object: 1.1.2 + is-number-object: 1.0.7 + is-string: 1.0.7 + is-symbol: 1.0.4 + dev: true + + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: true + + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + /wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + dependencies: + ansi-styles: 6.2.1 + string-width: 5.1.2 + strip-ansi: 7.1.0 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + /write-file-atomic@4.0.2: + resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + dependencies: + imurmurhash: 0.1.4 + signal-exit: 3.0.7 + + /ws@8.16.0: + resolution: {integrity: sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: true + + /xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + dev: true + + /xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + dev: true + + /xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + dev: true + + /y-protocols@1.0.6(yjs@13.5.51): + resolution: {integrity: sha512-vHRF2L6iT3rwj1jub/K5tYcTT/mEYDUppgNPXwp8fmLpui9f7Yeq3OEtTLVF012j39QnV+KEQpNqoN7CWU7Y9Q==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + peerDependencies: + yjs: ^13.0.0 + dependencies: + lib0: 0.2.93 + yjs: 13.5.51 + dev: false + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + /yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + /yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + /yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + /yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + /yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + dependencies: + cliui: 8.0.1 + escalade: 3.1.2 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + /yjs@13.5.51: + resolution: {integrity: sha512-F1Nb3z3TdandD80IAeQqgqy/2n9AhDLcXoBhZvCUX1dNVe0ef7fIwi6MjSYaGAYF2Ev8VcLcsGnmuGGOl7AWbw==} + engines: {node: '>=16.0.0', npm: '>=8.0.0'} + dependencies: + lib0: 0.2.93 + dev: false + + /yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} diff --git a/frontend/appflowy_web_app/postcss.config.cjs b/frontend/appflowy_web_app/postcss.config.cjs new file mode 100644 index 0000000000..12a703d900 --- /dev/null +++ b/frontend/appflowy_web_app/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/frontend/appflowy_web_app/public/appflowy.svg b/frontend/appflowy_web_app/public/appflowy.svg new file mode 100644 index 0000000000..b1ac8d66fb --- /dev/null +++ b/frontend/appflowy_web_app/public/appflowy.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/public/launch_splash.jpg b/frontend/appflowy_web_app/public/launch_splash.jpg new file mode 100644 index 0000000000..7e3bb9cee6 Binary files /dev/null and b/frontend/appflowy_web_app/public/launch_splash.jpg differ diff --git a/frontend/appflowy_web_app/scripts/create-symlink.cjs b/frontend/appflowy_web_app/scripts/create-symlink.cjs new file mode 100644 index 0000000000..472f511f27 --- /dev/null +++ b/frontend/appflowy_web_app/scripts/create-symlink.cjs @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); +const chalk = require('chalk'); + +const sourcePath = process.argv[2]; +const targetPath = process.argv[3]; + +// ensure source and target paths are provided +if (!sourcePath || !targetPath) { + console.error(chalk.red('source and target paths are required')); + process.exit(1); +} + +const fullSourcePath = path.resolve(sourcePath); +const fullTargetPath = path.resolve(targetPath); +// ensure source path exists +if (!fs.existsSync(fullSourcePath)) { + console.error(chalk.red(`source path does not exist: ${fullSourcePath}`)); + process.exit(1); +} + +// ensure target path exists +if (!fs.existsSync(fullTargetPath)) { + console.error(chalk.red(`target path does not exist: ${fullTargetPath}`)); + process.exit(1); +} + + +if (fs.existsSync(fullTargetPath)) { + // unlink existing symlink + console.log(chalk.yellow(`unlinking existing symlink: `) + chalk.blue(`${fullTargetPath}`)); + fs.unlinkSync(fullTargetPath); +} + +// create symlink +fs.symlink(fullSourcePath, fullTargetPath, 'junction', (err) => { + if (err) { + console.error(chalk.red(`error creating symlink: ${err.message}`)); + process.exit(1); + } + console.log(chalk.green(`symlink created: `) + chalk.blue(`${fullSourcePath}`) + ' -> ' + chalk.blue(`${fullTargetPath}`)); + +}); diff --git a/frontend/appflowy_web_app/scripts/i18n.cjs b/frontend/appflowy_web_app/scripts/i18n.cjs new file mode 100644 index 0000000000..407a03694a --- /dev/null +++ b/frontend/appflowy_web_app/scripts/i18n.cjs @@ -0,0 +1,63 @@ +const languages = [ + 'ar-SA', + 'ca-ES', + 'de-DE', + 'en', + 'es-VE', + 'eu-ES', + 'fr-FR', + 'hu-HU', + 'id-ID', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ru-RU', + 'sv-SE', + 'th-TH', + 'tr-TR', + 'zh-CN', + 'zh-TW', +]; + +const fs = require('fs'); +languages.forEach(language => { + const json = require(`../../resources/translations/${language}.json`); + const outputJSON = flattenJSON(json); + const output = JSON.stringify(outputJSON); + const isExistDir = fs.existsSync('./src/@types/translations'); + if (!isExistDir) { + fs.mkdirSync('./src/@types/translations'); + } + fs.writeFile(`./src/@types/translations/${language}.json`, new Uint8Array(Buffer.from(output)), (res) => { + if (res) { + console.error(res); + } + }) +}); + + +function flattenJSON(obj, prefix = '') { + let result = {}; + const pluralsKey = ["one", "other", "few", "many", "two", "zero"]; + + for (let key in obj) { + if (typeof obj[key] === 'object' && obj[key] !== null) { + + const nestedKeys = flattenJSON(obj[key], `${prefix}${key}.`); + result = { ...result, ...nestedKeys }; + } else { + let newKey = `${prefix}${key}`; + let replaceChar = '{' + if (pluralsKey.includes(key)) { + newKey = `${prefix.slice(0, -1)}_${key}`; + } + result[newKey] = obj[key].replaceAll('{', '{{').replaceAll('}', '}}'); + } + } + + return result; +} + diff --git a/frontend/appflowy_web_app/src-tauri/.gitignore b/frontend/appflowy_web_app/src-tauri/.gitignore new file mode 100644 index 0000000000..9e4914893d --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/.gitignore @@ -0,0 +1,4 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ +.env \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.lock b/frontend/appflowy_web_app/src-tauri/Cargo.lock new file mode 100644 index 0000000000..c1fe9eab25 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/Cargo.lock @@ -0,0 +1,8078 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "accessory" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "850bb534b9dc04744fbbb71d30ad6d25a7e4cf6dc33e223c81ef3a92ebab4e0b" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "again" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05802a5ad4d172eaf796f7047b42d0af9db513585d16d4169660a21613d34b93" +dependencies = [ + "log", + "rand 0.7.3", + "wasm-timer", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.12", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom 0.2.12", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" + +[[package]] +name = "app-error" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "getrandom 0.2.12", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tsify", + "url", + "uuid", + "wasm-bindgen", +] + +[[package]] +name = "appflowy_tauri" +version = "0.0.0" +dependencies = [ + "bytes", + "dotenv", + "flowy-config", + "flowy-core", + "flowy-date", + "flowy-document", + "flowy-error", + "flowy-notification", + "flowy-user", + "lib-dispatch", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-deep-link", + "tauri-utils", + "tracing", + "uuid", +] + +[[package]] +name = "arboard" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2041f1943049c7978768d84e6d0fd95de98b76d6c4727b09e78ec253d29fa58" +dependencies = [ + "clipboard-win", + "core-graphics 0.23.1", + "image", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot 0.12.1", + "thiserror", + "windows-sys 0.48.0", + "wl-clipboard-rs", + "x11rb", +] + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-stream" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "async-trait" +version = "0.1.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "atk" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "atomic_refcell" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bindgen" +version = "0.65.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfdf7b466f9a4903edc73f95d6d2bcd5baf8ae620638762244d3f60143643cc5" +dependencies = [ + "bitflags 1.3.2", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.55", +] + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "borsh" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58b559fd6448c6e2fd0adb5720cd98a2506594cafa4737ff98c396f3e82f667" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aadb5b6ccbd078890f6d7003694e33816e6b784358f18e15e7e6d9f065a57cd" +dependencies = [ + "once_cell", + "proc-macro-crate 3.1.0", + "proc-macro2", + "quote", + "syn 2.0.55", + "syn_derive", +] + +[[package]] +name = "brotli" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640d25bc63c50fb1f0b545ffd80207d2e10a4c965530809b40ba3386825c391" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bstr" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytemuck" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +dependencies = [ + "serde", +] + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "cairo-rs" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "glib", + "libc", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "cargo_toml" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-expr" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.4", +] + +[[package]] +name = "chrono-tz" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59ae0466b83e838b81a54256c39d5d7c20b9d7daa10510a242d9b75abd5936e" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf 0.11.2", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf 0.11.2", + "phf_codegen 0.11.2", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "client-api" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "again", + "anyhow", + "app-error", + "async-trait", + "bincode", + "brotli", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", + "database-entity", + "futures-core", + "futures-util", + "getrandom 0.2.12", + "gotrue", + "gotrue-entity", + "governor", + "mime", + "parking_lot 0.12.1", + "prost", + "reqwest", + "scraper 0.17.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "shared-entity", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tracing", + "url", + "uuid", + "wasm-bindgen-futures", + "workspace-template", + "yrs", +] + +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "clipboard-win" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee" +dependencies = [ + "error-code", +] + +[[package]] +name = "cmd_lib" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f4cbdcab51ca635c5b19c85ece4072ea42e0d2360242826a6fc96fb11f0d40" +dependencies = [ + "cmd_lib_macros", + "env_logger", + "faccess", + "lazy_static", + "log", + "os_pipe", +] + +[[package]] +name = "cmd_lib_macros" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae881960f7e2a409f91ef0b1c09558cf293031a1d6e8b45f908311f2a43f5fdf" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics 0.22.3", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "collab" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "async-trait", + "bincode", + "bytes", + "chrono", + "js-sys", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-database" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "async-trait", + "chrono", + "collab", + "collab-entity", + "collab-plugins", + "dashmap", + "getrandom 0.2.12", + "js-sys", + "lazy_static", + "nanoid", + "parking_lot 0.12.1", + "rayon", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.3", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "collab-document" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.12", + "nanoid", + "parking_lot 0.12.1", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "bytes", + "collab", + "getrandom 0.2.12", + "serde", + "serde_json", + "serde_repr", + "uuid", +] + +[[package]] +name = "collab-folder" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "chrono", + "collab", + "collab-entity", + "getrandom 0.2.12", + "parking_lot 0.12.1", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "collab-integrate" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "collab", + "collab-entity", + "collab-plugins", + "futures", + "lib-infra", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "collab-plugins" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bincode", + "bytes", + "chrono", + "collab", + "collab-entity", + "futures", + "futures-util", + "getrandom 0.2.12", + "indexed_db_futures", + "js-sys", + "lazy_static", + "parking_lot 0.12.1", + "rand 0.8.5", + "rocksdb", + "serde", + "serde_json", + "similar 2.4.0", + "smallvec", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tracing", + "tracing-wasm", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "yrs", +] + +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "collab", + "serde", + "thiserror", + "tracing", + "yrs", +] + +[[package]] +name = "collab-user" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "getrandom 0.2.12", + "parking_lot 0.12.1", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tracing", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "console" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3993e6445baa160675931ec041a5e03ca84b9c6e32a056150d3aa2bdda0a1f45" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "regex", + "terminal_size", + "unicode-width", + "winapi", +] + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7efb37c3e1ccb1ff97164ad95ac1606e8ccd35b3fa0a7d99a304c7f4a428cc24" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387461abbc748185c3a6e1673d826918b450b87ff22639429c694619a83b6cf6" +dependencies = [ + "cookie", + "idna 0.3.0", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970a29baf4110c26fedbc7f82107d42c23f7e88e404c4577ed73fe99ff85a212" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 0.4.8", + "matches", + "phf 0.8.0", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 1.0.10", + "phf 0.11.2", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.55", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctor" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" +dependencies = [ + "quote", + "syn 2.0.55", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.55", +] + +[[package]] +name = "darling_macro" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.3", + "lock_api", + "once_cell", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "data-encoding" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" + +[[package]] +name = "database-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "app-error", + "bincode", + "chrono", + "collab-entity", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "date_time_parser" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0521d96e513670773ac503e5f5239178c3aef16cffda1e77a3cdbdbe993fb5a" +dependencies = [ + "chrono", + "regex", +] + +[[package]] +name = "delegate-display" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a85201f233142ac819bbf6226e36d0b5e129a47bd325084674261c82d4cd66" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "deunicode" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6e854126756c496b8c81dec88f9a706b15b875c5849d4097a3854476b9fdf94" + +[[package]] +name = "diesel" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03fc05c17098f21b89bc7d98fe1dd3cce2c11c2ad8e145f2a44fe08ed28eb559" +dependencies = [ + "chrono", + "diesel_derives", + "libsqlite3-sys", + "r2d2", + "time", +] + +[[package]] +name = "diesel_derives" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d02eecb814ae714ffe61ddc2db2dd03e6c49a42e269b5001355500d431cce0c" +dependencies = [ + "diesel_table_macro_syntax", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "diesel_migrations" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6036b3f0120c5961381b570ee20a02432d7e2d27ea60de9578799cf9156914ac" +dependencies = [ + "diesel", + "migrations_internals", + "migrations_macros", +] + +[[package]] +name = "diesel_table_macro_syntax" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc5557efc453706fed5e4fa85006fe9817c224c3f480a34c7e5959fd700921c5" +dependencies = [ + "syn 2.0.55", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dtoa" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" + +[[package]] +name = "dtoa-short" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ego-tree" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" + +[[package]] +name = "either" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" + +[[package]] +name = "embed-resource" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6985554d0688b687c5cb73898a34fbe3ad6c24c58c238a4d91d5e840670ee9d" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.8.12", + "vswhom", + "winreg 0.52.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "error-code" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags 1.3.2", + "libc", + "winapi", +] + +[[package]] +name = "fancy-regex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0678ab2d46fa5195aaf59ad034c083d351377d4af57f3e073c074d0da3e3c766" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + +[[package]] +name = "fancy_constructor" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f71f317e4af73b2f8f608fac190c52eac4b1879d2145df1db2fe48881ca69435" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "fastrand" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" + +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flowy-ast" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "flowy-codegen" +version = "0.1.0" +dependencies = [ + "cmd_lib", + "console", + "fancy-regex 0.10.0", + "flowy-ast", + "itertools", + "lazy_static", + "log", + "phf 0.8.0", + "protoc-bin-vendored", + "protoc-rust", + "quote", + "serde", + "serde_json", + "similar 1.3.0", + "syn 1.0.109", + "tera", + "toml 0.5.11", + "walkdir", +] + +[[package]] +name = "flowy-config" +version = "0.1.0" +dependencies = [ + "bytes", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", +] + +[[package]] +name = "flowy-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bytes", + "client-api", + "collab", + "collab-entity", + "collab-integrate", + "collab-plugins", + "diesel", + "flowy-config", + "flowy-database-pub", + "flowy-database2", + "flowy-date", + "flowy-document", + "flowy-document-pub", + "flowy-error", + "flowy-folder", + "flowy-folder-pub", + "flowy-server", + "flowy-server-pub", + "flowy-sqlite", + "flowy-storage", + "flowy-user", + "flowy-user-pub", + "futures", + "futures-core", + "lib-dispatch", + "lib-infra", + "lib-log", + "parking_lot 0.12.1", + "semver", + "serde", + "serde_json", + "serde_repr", + "sysinfo", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "walkdir", +] + +[[package]] +name = "flowy-database-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "lib-infra", +] + +[[package]] +name = "flowy-database2" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-stream", + "async-trait", + "bytes", + "chrono", + "chrono-tz", + "collab", + "collab-database", + "collab-entity", + "collab-integrate", + "collab-plugins", + "csv", + "dashmap", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-database-pub", + "flowy-derive", + "flowy-error", + "flowy-notification", + "futures", + "indexmap 2.2.6", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "rayon", + "rust_decimal", + "rusty-money", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.3", + "tokio", + "tracing", + "url", + "validator", +] + +[[package]] +name = "flowy-date" +version = "0.1.0" +dependencies = [ + "bytes", + "chrono", + "date_time_parser", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "lib-dispatch", + "protobuf", + "strum_macros 0.21.1", + "tracing", +] + +[[package]] +name = "flowy-derive" +version = "0.1.0" +dependencies = [ + "dashmap", + "flowy-ast", + "flowy-codegen", + "lazy_static", + "proc-macro2", + "quote", + "serde_json", + "syn 1.0.109", + "walkdir", +] + +[[package]] +name = "flowy-document" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "collab", + "collab-document", + "collab-entity", + "collab-integrate", + "collab-plugins", + "dashmap", + "flowy-codegen", + "flowy-derive", + "flowy-document-pub", + "flowy-error", + "flowy-notification", + "flowy-storage", + "futures", + "getrandom 0.2.12", + "indexmap 2.2.6", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "scraper 0.18.1", + "serde", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "uuid", + "validator", +] + +[[package]] +name = "flowy-document-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-document", + "flowy-error", + "lib-infra", +] + +[[package]] +name = "flowy-encrypt" +version = "0.1.0" +dependencies = [ + "aes-gcm", + "anyhow", + "base64 0.21.7", + "getrandom 0.2.12", + "hmac", + "pbkdf2 0.12.2", + "rand 0.8.5", + "sha2", +] + +[[package]] +name = "flowy-error" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "client-api", + "collab-database", + "collab-document", + "collab-folder", + "collab-plugins", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-sqlite", + "lib-dispatch", + "protobuf", + "r2d2", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio", + "url", + "validator", +] + +[[package]] +name = "flowy-folder" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "chrono", + "collab", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "flowy-codegen", + "flowy-derive", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "lazy_static", + "lib-dispatch", + "lib-infra", + "nanoid", + "parking_lot 0.12.1", + "protobuf", + "serde_json", + "strum_macros 0.21.1", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-folder-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "collab", + "collab-entity", + "collab-folder", + "lib-infra", + "uuid", +] + +[[package]] +name = "flowy-notification" +version = "0.1.0" +dependencies = [ + "bytes", + "flowy-codegen", + "flowy-derive", + "lazy_static", + "lib-dispatch", + "protobuf", + "serde", + "tracing", +] + +[[package]] +name = "flowy-server" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "chrono", + "client-api", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "collab-plugins", + "flowy-database-pub", + "flowy-document-pub", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-server-pub", + "flowy-storage", + "flowy-user-pub", + "futures", + "futures-util", + "hex", + "hyper", + "lazy_static", + "lib-dispatch", + "lib-infra", + "mime_guess", + "parking_lot 0.12.1", + "postgrest", + "rand 0.8.5", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-retry", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "uuid", + "yrs", +] + +[[package]] +name = "flowy-server-pub" +version = "0.1.0" +dependencies = [ + "flowy-error", + "serde", + "serde_repr", +] + +[[package]] +name = "flowy-sqlite" +version = "0.1.0" +dependencies = [ + "anyhow", + "diesel", + "diesel_derives", + "diesel_migrations", + "libsqlite3-sys", + "parking_lot 0.12.1", + "r2d2", + "scheduled-thread-pool", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "flowy-storage" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "flowy-error", + "fxhash", + "lib-infra", + "mime", + "mime_guess", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "flowy-user" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bytes", + "chrono", + "collab", + "collab-database", + "collab-document", + "collab-entity", + "collab-folder", + "collab-integrate", + "collab-plugins", + "collab-user", + "diesel", + "diesel_derives", + "fancy-regex 0.11.0", + "flowy-codegen", + "flowy-derive", + "flowy-encrypt", + "flowy-error", + "flowy-folder-pub", + "flowy-notification", + "flowy-server-pub", + "flowy-sqlite", + "flowy-user-pub", + "lazy_static", + "lib-dispatch", + "lib-infra", + "once_cell", + "parking_lot 0.12.1", + "protobuf", + "semver", + "serde", + "serde_json", + "serde_repr", + "strum", + "strum_macros 0.25.3", + "tokio", + "tokio-stream", + "tracing", + "unicode-segmentation", + "uuid", + "validator", +] + +[[package]] +name = "flowy-user-pub" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64 0.21.7", + "chrono", + "collab", + "collab-entity", + "flowy-error", + "flowy-folder-pub", + "lib-infra", + "serde", + "serde_json", + "serde_repr", + "tokio", + "tokio-stream", + "tracing", + "uuid", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +dependencies = [ + "bitflags 1.3.2", + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gdk-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps 6.2.2", +] + +[[package]] +name = "gdkx11-sys" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps 6.2.2", + "x11", +] + +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows 0.48.0", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "gethostname" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0176e0459c2e4a1fe232f984bca6890e681076abb9934f6cea7c326f3fc47818" +dependencies = [ + "libc", + "windows-targets 0.48.5", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "gio" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-io", + "gio-sys", + "glib", + "libc", + "once_cell", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", + "winapi", +] + +[[package]] +name = "glib" +version = "0.15.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +dependencies = [ + "bitflags 1.3.2", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.15.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +dependencies = [ + "anyhow", + "heck 0.4.1", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "glib-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +dependencies = [ + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags 1.3.2", + "ignore", + "walkdir", +] + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gobject-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +dependencies = [ + "glib-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "gotrue" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "futures-util", + "getrandom 0.2.12", + "gotrue-entity", + "infra", + "reqwest", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "gotrue-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "app-error", + "chrono", + "jsonwebtoken", + "lazy_static", + "serde", + "serde_json", +] + +[[package]] +name = "governor" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.1", + "portable-atomic", + "quanta", + "rand 0.8.5", + "smallvec", + "spinning_top", +] + +[[package]] +name = "gtk" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +dependencies = [ + "atk", + "bitflags 1.3.2", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "once_cell", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "gtk3-macros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +dependencies = [ + "anyhow", + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "h2" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbd2820c5e49886948654ab546d0688ff24530286bdcf8fca3cefb16d4618eb" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "html5ever" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa 1.0.10", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "http-range" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humansize" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7" +dependencies = [ + "libm", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa 1.0.10", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3804960be0bb5e4edb1e1ad67afd321a9ecfd875c3e65c099468fd2717d7cae" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + +[[package]] +name = "ignore" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata 0.4.6", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexed_db_futures" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "fancy_constructor", + "js-sys", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown 0.14.3", + "serde", +] + +[[package]] +name = "infer" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +dependencies = [ + "cfb", +] + +[[package]] +name = "infra" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "reqwest", + "serde", + "serde_json", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "interprocess" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb" +dependencies = [ + "cfg-if", + "libc", + "rustc_version", + "to_method", + "winapi", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "is-terminal" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "javascriptcore-rs" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ff1e1486799e3f64129f8ccad108b38290df9cd7015cd31bed17239f0789d6" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "treediff", +] + +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + +[[package]] +name = "kuchikiki" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" +dependencies = [ + "cssparser 0.27.2", + "html5ever", + "indexmap 1.9.3", + "matches", + "selectors 0.22.0", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lib-dispatch" +version = "0.1.0" +dependencies = [ + "bincode", + "bytes", + "derivative", + "dyn-clone", + "futures", + "futures-channel", + "futures-core", + "futures-util", + "getrandom 0.2.12", + "nanoid", + "parking_lot 0.12.1", + "pin-project", + "protobuf", + "serde", + "serde_json", + "serde_repr", + "thread-id", + "tokio", + "tracing", + "validator", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "lib-infra" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "atomic_refcell", + "bytes", + "chrono", + "futures-core", + "md5", + "pin-project", + "tempfile", + "tokio", + "tracing", + "validator", + "walkdir", + "zip", +] + +[[package]] +name = "lib-log" +version = "0.1.0" +dependencies = [ + "chrono", + "lazy_static", + "serde", + "serde_json", + "tracing", + "tracing-appender", + "tracing-bunyan-formatter", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets 0.52.4", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.5.0", + "libc", + "redox_syscall 0.4.1", +] + +[[package]] +name = "librocksdb-sys" +version = "0.11.0+8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "zstd-sys", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "line-wrap" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd1bc4d24ad230d21fb898d1116b1801d7adfc449d42026475862ab48b11e70e" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "loom" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "macroific" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05c00ac596022625d01047c421a0d97d7f09a18e429187b341c201cb631b9dd" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "macroific_core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13198c120864097a565ccb3ff947672d969932b7975ebd4085732c9f09435e55" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "macroific_macro" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c9853143cbed7f1e41dc39fee95f9b361bec65c8dc2a01bf609be01b61f5ae" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "markup5ever" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +dependencies = [ + "log", + "phf 0.10.1", + "phf_codegen 0.10.0", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "migrations_internals" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f23f71580015254b020e856feac3df5878c2c7a8812297edd6c0a485ac9dada" +dependencies = [ + "serde", + "toml 0.7.8", +] + +[[package]] +name = "migrations_macros" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce3325ac70e67bbab5bd837a31cae01f1a6db64e0e744a33cb03a543469ef08" +dependencies = [ + "migrations_internals", + "proc-macro2", + "quote", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.48.0", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "nanoid" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" +dependencies = [ + "rand 0.8.5", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", + "objc_exception", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c71324e4180d0899963fc83d9d241ac39e699609fc1025a850aadac8257459" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + +[[package]] +name = "objc_exception" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +dependencies = [ + "cc", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "open" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +dependencies = [ + "pathdiff", + "windows-sys 0.42.0", +] + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags 2.5.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.2.3+3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cff92b6f71555b61bb9315f7c64da3ca43d87531622120fea0195fc761b4843" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pango" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +dependencies = [ + "bitflags 1.3.2", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.15.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps 6.2.2", +] + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.6", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall 0.2.16", + "smallvec", + "winapi", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap 2.2.6", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_macros 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_shared 0.10.0", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_macros 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared 0.11.2", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" +dependencies = [ + "phf_generator 0.11.2", + "phf_shared 0.11.2", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plist" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9d34169e64b3c7a80c8621a48adaf44e0cf62c78a9b25dd9dd35f1881a17cf9" +dependencies = [ + "base64 0.21.7", + "indexmap 2.2.6", + "line-wrap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + +[[package]] +name = "postgrest" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a966c650b47a064e7082170b4be74fca08c088d893244fc4b70123e3c1f3ee7" +dependencies = [ + "reqwest", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3928fb5db768cb86f891ff014f0144589297e3c6a1aba6ed7cecfdace270c7" +dependencies = [ + "proc-macro2", + "syn 2.0.55", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +dependencies = [ + "toml_edit 0.21.1", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c289cda302b98a28d40c8b3b90498d6e526dd24ac2ecea73e4e491685b94a" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2" +dependencies = [ + "bytes", + "heck 0.4.1", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.55", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "prost-types" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "193898f59edcf43c26227dcd4c8427f00d99d61e95dcde58dabd49fa291d470e" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "protobuf-codegen" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +dependencies = [ + "protobuf", +] + +[[package]] +name = "protoc" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0218039c514f9e14a5060742ecd50427f8ac4f85a6dc58f2ddb806e318c55ee" +dependencies = [ + "log", + "which", +] + +[[package]] +name = "protoc-bin-vendored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d" +dependencies = [ + "protoc-bin-vendored-linux-aarch_64", + "protoc-bin-vendored-linux-ppcle_64", + "protoc-bin-vendored-linux-x86_32", + "protoc-bin-vendored-linux-x86_64", + "protoc-bin-vendored-macos-x86_64", + "protoc-bin-vendored-win32", +] + +[[package]] +name = "protoc-bin-vendored-linux-aarch_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435" + +[[package]] +name = "protoc-bin-vendored-linux-ppcle_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516" + +[[package]] +name = "protoc-bin-vendored-linux-x86_32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0" + +[[package]] +name = "protoc-bin-vendored-linux-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924" + +[[package]] +name = "protoc-bin-vendored-macos-x86_64" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537" + +[[package]] +name = "protoc-bin-vendored-win32" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804" + +[[package]] +name = "protoc-rust" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f8a182bb17c485f20bdc4274a8c39000a61024cfe461c799b50fec77267838" +dependencies = [ + "protobuf", + "protobuf-codegen", + "protoc", + "tempfile", +] + +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + +[[package]] +name = "quanta" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca0b7bac0b97248c40bb77288fc52029cf1459c0461ea1b05ee32ccf011de2c" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot 0.12.1", + "scheduled-thread-pool", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.12", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-cpuid" +version = "11.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d86a7c4638d42c44551f4791a20e687dbb4c3de1f33c43dd71e355cd429def1" +dependencies = [ + "bitflags 2.5.0", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom 0.2.12", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.6", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "cookie", + "cookie_store", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "mime_guess", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots", + "winreg 0.50.0", +] + +[[package]] +name = "rfd" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +dependencies = [ + "block", + "dispatch", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "lazy_static", + "log", + "objc", + "objc-foundation", + "objc_id", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.37.0", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.12", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cba464629b3394fc4dbc6f940ff8f5b4ff5c7aef40f29166fd4ad12acbc99c0" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7dddfff8de25e6f62b9d64e6e432bf1c6736c57d20323e15ee10435fbda7c65" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rocksdb" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +dependencies = [ + "libc", + "librocksdb-sys", +] + +[[package]] +name = "rust_decimal" +version = "1.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39449a79f45e8da28c57c341891b69a183044b29518bb8f86dbac9df60bb7df" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e418701588729bef95e7a655f2b483ad64bb97c46e8e79fde83efd92aaab6d82" +dependencies = [ + "quote", + "rust_decimal", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags 2.5.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + +[[package]] +name = "rusty-money" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b28f881005eac7ad8d46b6f075da5f322bd7f4f83a38720fc069694ddadd683" +dependencies = [ + "rust_decimal", + "rust_decimal_macros", +] + +[[package]] +name = "ryu" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot 0.12.1", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scraper" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c95a930e03325234c18c7071fd2b60118307e025d6fff3e12745ffbf63a3d29c" +dependencies = [ + "ahash 0.8.11", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors 0.25.0", + "smallvec", + "tendril", +] + +[[package]] +name = "scraper" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" +dependencies = [ + "ahash 0.8.11", + "cssparser 0.31.2", + "ego-tree", + "getopts", + "html5ever", + "once_cell", + "selectors 0.25.0", + "tendril", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "security-framework" +version = "2.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.27.2", + "derive_more", + "fxhash", + "log", + "matches", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.1.1", + "smallvec", + "thin-slice", +] + +[[package]] +name = "selectors" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" +dependencies = [ + "bitflags 2.5.0", + "cssparser 0.31.2", + "derive_more", + "fxhash", + "log", + "new_debug_unreachable", + "phf 0.10.1", + "phf_codegen 0.10.0", + "precomputed-hash", + "servo_arc 0.3.0", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.197" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_json" +version = "1.0.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +dependencies = [ + "indexmap 2.2.6", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2e6b945e9d3df726b65d6ee24060aff8e3533d431f677a9695db04eff9dfdb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.10", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee80b0e361bbf88fd2f6e242ccd19cfda072cb0faa6ae694ecee08199938569a" +dependencies = [ + "base64 0.21.7", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6561dc161a9224638a31d876ccdfefbc1df91d3f3a8342eddb35f055d48c7655" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9823f2d3b6a81d98228151fdeaf848206a7855a7a042bbf9bf870449a66cafb" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74064874e9f6a15f04c1f3cb627902d0e6b410abbf36668afa873c61889f1763" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "servo_arc" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shared-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "app-error", + "chrono", + "collab-entity", + "database-entity", + "gotrue-entity", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "uuid", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +dependencies = [ + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "similar" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec" + +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slug" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bd94acec9c8da640005f8e135a39fc0372e74535e6b368b7a04b875f784c8c4" +dependencies = [ + "deunicode", + "wasm-bindgen", +] + +[[package]] +name = "smallstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" +dependencies = [ + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "soup2" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +dependencies = [ + "bitflags 1.3.2", + "gio", + "glib", + "libc", + "once_cell", + "soup2-sys", +] + +[[package]] +name = "soup2-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +dependencies = [ + "bitflags 1.3.2", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps 5.0.0", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "state" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" +dependencies = [ + "loom", +] + +[[package]] +name = "string_cache" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot 0.12.1", + "phf_shared 0.10.0", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" + +[[package]] +name = "strum_macros" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d06aaeeee809dbc59eb4556183dd927df67db1540de5be8d3ec0b6636358a5ec" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.55", +] + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn_derive" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sysinfo" +version = "0.30.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c385888ef380a852a16209afc8cfad22795dd8873d69c9a14d2e2088f118d18" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows 0.52.0", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" +dependencies = [ + "cfg-expr 0.9.1", + "heck 0.3.3", + "pkg-config", + "toml 0.5.11", + "version-compare 0.0.11", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr 0.15.7", + "heck 0.5.0", + "pkg-config", + "toml 0.8.12", + "version-compare 0.2.0", +] + +[[package]] +name = "tao" +version = "0.16.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22205b267a679ca1c590b9f178488d50981fc3e48a1b91641ae31593db875ce" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "cc", + "cocoa", + "core-foundation", + "core-graphics 0.22.3", + "crossbeam-channel", + "dispatch", + "gdk", + "gdk-pixbuf", + "gdk-sys", + "gdkwayland-sys", + "gdkx11-sys", + "gio", + "glib", + "glib-sys", + "gtk", + "image", + "instant", + "jni", + "lazy_static", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc", + "once_cell", + "parking_lot 0.12.1", + "png", + "raw-window-handle", + "scopeguard", + "serde", + "tao-macros", + "unicode-segmentation", + "uuid", + "windows 0.39.0", + "windows-implement", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec114582505d158b669b136e6851f85840c109819d77c42bb7c0709f727d18c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + +[[package]] +name = "tauri" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f078117725e36d55d29fafcbb4b1e909073807ca328ae8deb8c0b3843aac0fed" +dependencies = [ + "anyhow", + "cocoa", + "dirs-next", + "dunce", + "embed_plist", + "encoding_rs", + "flate2", + "futures-util", + "glib", + "glob", + "gtk", + "heck 0.4.1", + "http", + "ignore", + "objc", + "once_cell", + "open", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "regex", + "rfd", + "semver", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "state", + "tar", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "tempfile", + "thiserror", + "tokio", + "url", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-build" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9914a4715e0b75d9f387a285c7e26b5bbfeb1249ad9f842675a82481565c532" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs-next", + "heck 0.4.1", + "json-patch", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1554c5857f65dbc377cefb6b97c8ac77b1cb2a90d30d3448114d5d6b48a77fc" +dependencies = [ + "base64 0.21.7", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "regex", + "semver", + "serde", + "serde_json", + "sha2", + "tauri-utils", + "thiserror", + "time", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "277abf361a3a6993ec16bcbb179de0d6518009b851090a01adfea12ac89fa875" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin-deep-link" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4536f5f6602e8fdfaa7b3b185076c2a0704f8eb7015f4e58461eb483ec3ed1f8" +dependencies = [ + "dirs", + "interprocess", + "log", + "objc2", + "once_cell", + "tauri-utils", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + +[[package]] +name = "tauri-runtime" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2d0652aa2891ff3e9caa2401405257ea29ab8372cce01f186a5825f1bd0e76" +dependencies = [ + "gtk", + "http", + "http-range", + "rand 0.8.5", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror", + "url", + "uuid", + "webview2-com", + "windows 0.39.0", +] + +[[package]] +name = "tauri-runtime-wry" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "067c56fc153b3caf406d7cd6de4486c80d1d66c0f414f39e94cb2f5543f6445f" +dependencies = [ + "arboard", + "cocoa", + "gtk", + "percent-encoding", + "rand 0.8.5", + "raw-window-handle", + "tauri-runtime", + "tauri-utils", + "uuid", + "webkit2gtk", + "webview2-com", + "windows 0.39.0", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75ad0bbb31fccd1f4c56275d0a5c3abdf1f59999f72cb4ef8b79b4ed42082a21" +dependencies = [ + "brotli", + "ctor", + "dunce", + "glob", + "heck 0.4.1", + "html5ever", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.2", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "serde_with", + "thiserror", + "url", + "walkdir", + "windows-version", +] + +[[package]] +name = "tauri-winres" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +dependencies = [ + "embed-resource", + "toml 0.7.8", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tera" +version = "1.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "970dff17c11e884a4a09bc76e3a17ef71e01bb13447a11e85226e254fe6d10b8" +dependencies = [ + "chrono", + "chrono-tz", + "globwalk", + "humansize", + "lazy_static", + "percent-encoding", + "pest", + "pest_derive", + "rand 0.8.5", + "regex", + "serde", + "serde_json", + "slug", + "unic-segment", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "thin-slice" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall 0.1.57", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +dependencies = [ + "deranged", + "itoa 1.0.10", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "to_method" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8" + +[[package]] +name = "tokio" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "parking_lot 0.12.1", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-retry" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f" +dependencies = [ + "pin-project", + "rand 0.8.5", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "native-tls", + "tokio", + "tokio-native-tls", + "tungstenite", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", +] + +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.9", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +dependencies = [ + "indexmap 2.2.6", + "toml_datetime", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap 2.2.6", + "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.6.5", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c266b9ac83dedf0e0385ad78514949e6d89491269e7065bee51d2bb8ec7373" +dependencies = [ + "ahash 0.8.11", + "gethostname 0.2.3", + "log", + "serde", + "serde_json", + "time", + "tracing", + "tracing-core", + "tracing-log 0.1.4", + "tracing-subscriber", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log 0.2.0", + "tracing-serde", +] + +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + +[[package]] +name = "tree_magic_mini" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + +[[package]] +name = "treediff" +version = "4.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d127780145176e2b5d16611cc25a900150e86e9fd79d3bde6ff3a37359c9cb5" +dependencies = [ + "serde_json", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.55", +] + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "native-tls", + "rand 0.8.5", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23" +dependencies = [ + "unic-ucd-segment", +] + +[[package]] +name = "unic-ucd-segment" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + +[[package]] +name = "unicode-width" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna 0.5.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "uuid" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e395fcf16a7a3d8127ec99782007af141946b4795001f876d54fb0d55978560" +dependencies = [ + "getrandom 0.2.12", + "serde", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "validator" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" +dependencies = [ + "idna 0.4.0", + "lazy_static", + "regex", + "serde", + "serde_derive", + "serde_json", + "url", + "validator_derive", +] + +[[package]] +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.109", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" +dependencies = [ + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b17ae1f6c8a2b28506cd96d412eebf83b4a0ff2cbefeeb952f2f9dfa44ba18" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.55", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + +[[package]] +name = "wasm-streams" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasm-timer" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be0ecb0db480561e9a7642b5d3e4187c128914e58aa84330b9493e3eb68c5e7f" +dependencies = [ + "futures", + "js-sys", + "parking_lot 0.11.2", + "pin-utils", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags 2.5.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.5.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webkit2gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup2", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +dependencies = [ + "atk-sys", + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pango-sys", + "pkg-config", + "soup2-sys", + "system-deps 6.2.2", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webview2-com" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "webview2-com-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "webview2-com-sys" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +dependencies = [ + "regex", + "serde", + "serde_json", + "thiserror", + "windows 0.39.0", + "windows-bindgen", + "windows-metadata", +] + +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +dependencies = [ + "windows_aarch64_msvc 0.37.0", + "windows_i686_gnu 0.37.0", + "windows_i686_msvc 0.37.0", + "windows_x86_64_gnu 0.37.0", + "windows_x86_64_msvc 0.37.0", +] + +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows-implement", + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-bindgen" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +dependencies = [ + "windows-metadata", + "windows-tokens", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-implement" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" +dependencies = [ + "syn 1.0.109", + "windows-tokens", +] + +[[package]] +name = "windows-metadata" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + +[[package]] +name = "windows-tokens" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" + +[[package]] +name = "windows-version" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75aa004c988e080ad34aff5739c39d0312f4684699d6d71fc8a198d057b8b9b4" +dependencies = [ + "windows-targets 0.52.4", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" + +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" + +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +dependencies = [ + "derive-new", + "libc", + "log", + "nix", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + +[[package]] +name = "workspace-template" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "collab", + "collab-document", + "collab-entity", + "collab-folder", + "getrandom 0.2.12", + "indexmap 2.2.6", + "nanoid", + "serde", + "serde_json", + "tokio", + "uuid", +] + +[[package]] +name = "wry" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ad85d0e067359e409fcb88903c3eac817c392e5d638258abfb3da5ad8ba6fc4" +dependencies = [ + "base64 0.13.1", + "block", + "cocoa", + "core-graphics 0.22.3", + "crossbeam-channel", + "dunce", + "gdk", + "gio", + "glib", + "gtk", + "html5ever", + "http", + "kuchikiki", + "libc", + "log", + "objc", + "objc_id", + "once_cell", + "serde", + "serde_json", + "sha2", + "soup2", + "tao", + "thiserror", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.39.0", + "windows-implement", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f25ead8c7e4cba123243a6367da5d3990e0d3affa708ea19dce96356bd9f1a" +dependencies = [ + "gethostname 0.4.3", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e63e71c4b8bd9ffec2c963173a4dc4cbde9ee96961d4fcb4429db9929b606c34" + +[[package]] +name = "xattr" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" +dependencies = [ + "libc", + "linux-raw-sys", + "rustix", +] + +[[package]] +name = "yrs" +version = "0.17.2" +source = "git+https://github.com/appflowy/y-crdt?rev=3f25bb510ca5274e7657d3713fbed41fb46b4487#3f25bb510ca5274e7657d3713fbed41fb46b4487" +dependencies = [ + "atomic_refcell", + "rand 0.7.3", + "serde", + "serde_json", + "smallstr", + "smallvec", + "thiserror", +] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.55", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.9+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/frontend/appflowy_web_app/src-tauri/Cargo.toml b/frontend/appflowy_web_app/src-tauri/Cargo.toml new file mode 100644 index 0000000000..073af93458 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/Cargo.toml @@ -0,0 +1,106 @@ +[package] +name = "appflowy_tauri" +version = "0.0.0" +description = "A Tauri App" +authors = ["you"] +license = "" +repository = "" +edition = "2021" +rust-version = "1.57" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[build-dependencies] +tauri-build = { version = "1.5", features = [] } + +[workspace.dependencies] +anyhow = "1.0" +tracing = "0.1.40" +bytes = "1.5.0" +serde = "1.0" +serde_json = "1.0.108" +protobuf = { version = "2.28.0" } +diesel = { version = "2.1.0", features = ["sqlite", "chrono", "r2d2"] } +uuid = { version = "1.5.0", features = ["serde", "v4"] } +serde_repr = "0.1" +parking_lot = "0.12" +futures = "0.3.29" +tokio = "1.34.0" +tokio-stream = "0.1.14" +async-trait = "0.1.74" +chrono = { version = "0.4.31", default-features = false, features = ["clock"] } + +[dependencies] +serde_json.workspace = true +serde.workspace = true +tauri = { version = "1.5", features = [ + "dialog-all", + "clipboard-all", + "fs-all", + "shell-open", +] } +tauri-utils = "1.5.2" +bytes.workspace = true +tracing.workspace = true +lib-dispatch = { path = "../../rust-lib/lib-dispatch", features = [ + "use_serde", +] } +flowy-core = { path = "../../rust-lib/flowy-core", features = [ + "rev-sqlite", + "ts", +] } +flowy-user = { path = "../../rust-lib/flowy-user", features = ["tauri_ts"] } +flowy-config = { path = "../../rust-lib/flowy-config", features = ["tauri_ts"] } +flowy-date = { path = "../../rust-lib/flowy-date", features = ["tauri_ts"] } +flowy-error = { path = "../../rust-lib/flowy-error", features = [ + "impl_from_sqlite", + "impl_from_dispatch_error", + "impl_from_appflowy_cloud", + "impl_from_reqwest", + "impl_from_serde", + "tauri_ts", +] } +flowy-document = { path = "../../rust-lib/flowy-document", features = [ + "tauri_ts", +] } +flowy-notification = { path = "../../rust-lib/flowy-notification", features = [ + "tauri_ts", +] } + +uuid = "1.5.0" +tauri-plugin-deep-link = "0.1.2" +dotenv = "0.15.0" + +[features] +# by default Tauri runs in production mode +# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL +default = ["custom-protocol"] +# this feature is used used for production builds where `devPath` points to the filesystem +# DO NOT remove this +custom-protocol = ["tauri/custom-protocol"] + +[patch.crates-io] +yrs = { git = "https://github.com/appflowy/y-crdt", rev = "3f25bb510ca5274e7657d3713fbed41fb46b4487" } + +# Please using the following command to update the revision id +# Current directory: frontend +# Run the script: +# scripts/tool/update_client_api_rev.sh new_rev_id +# ⚠️⚠️⚠️️ +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" } +# Please use the following script to update collab. +# Working directory: frontend +# +# To update the commit ID, run: +# scripts/tool/update_collab_rev.sh new_rev_id +# +# To switch to the local path, run: +# scripts/tool/update_collab_source.sh +# ⚠️⚠️⚠️️ +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } diff --git a/frontend/appflowy_web_app/src-tauri/Info.plist b/frontend/appflowy_web_app/src-tauri/Info.plist new file mode 100644 index 0000000000..25b430c049 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/Info.plist @@ -0,0 +1,19 @@ + + + + + + CFBundleURLTypes + + + CFBundleURLName + + appflowy-flutter + CFBundleURLSchemes + + appflowy-flutter + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/build.rs b/frontend/appflowy_web_app/src-tauri/build.rs new file mode 100644 index 0000000000..795b9b7c83 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/frontend/appflowy_web_app/src-tauri/env.development b/frontend/appflowy_web_app/src-tauri/env.development new file mode 100644 index 0000000000..188835e3d0 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/env.development @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://test.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://test.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://test.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_web_app/src-tauri/env.production b/frontend/appflowy_web_app/src-tauri/env.production new file mode 100644 index 0000000000..b03c328b84 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/env.production @@ -0,0 +1,4 @@ +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_BASE_URL=https://beta.appflowy.cloud +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_WS_BASE_URL=wss://beta.appflowy.cloud/ws/v1 +APPFLOWY_CLOUD_ENV_APPFLOWY_CLOUD_GOTRUE_URL=https://beta.appflowy.cloud/gotrue +APPFLOWY_CLOUD_ENV_CLOUD_TYPE=2 diff --git a/frontend/appflowy_web_app/src-tauri/icons/128x128.png b/frontend/appflowy_web_app/src-tauri/icons/128x128.png new file mode 100644 index 0000000000..3a51041313 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/128x128.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/128x128@2x.png b/frontend/appflowy_web_app/src-tauri/icons/128x128@2x.png new file mode 100644 index 0000000000..9076de3a4b Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/128x128@2x.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/32x32.png b/frontend/appflowy_web_app/src-tauri/icons/32x32.png new file mode 100644 index 0000000000..6ae6683fef Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/32x32.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square107x107Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square107x107Logo.png new file mode 100644 index 0000000000..b08dcf7d21 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square107x107Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square142x142Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000..f3e437b76e Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square142x142Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square150x150Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square150x150Logo.png new file mode 100644 index 0000000000..6a1dc04864 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square150x150Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square284x284Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square284x284Logo.png new file mode 100644 index 0000000000..2f2d9d6fe6 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square284x284Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square30x30Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000..46e3802c0b Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square30x30Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square310x310Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square310x310Logo.png new file mode 100644 index 0000000000..230b1abe58 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square310x310Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square44x44Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000..ad188037a3 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square44x44Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square71x71Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000..ceae9ad1bb Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square71x71Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/Square89x89Logo.png b/frontend/appflowy_web_app/src-tauri/icons/Square89x89Logo.png new file mode 100644 index 0000000000..123dcea650 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/Square89x89Logo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/StoreLogo.png b/frontend/appflowy_web_app/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000..d7906c3c03 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/StoreLogo.png differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/icon.icns b/frontend/appflowy_web_app/src-tauri/icons/icon.icns new file mode 100644 index 0000000000..74b585f25d Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/icon.icns differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/icon.ico b/frontend/appflowy_web_app/src-tauri/icons/icon.ico new file mode 100644 index 0000000000..cd9ad402d1 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/icon.ico differ diff --git a/frontend/appflowy_web_app/src-tauri/icons/icon.png b/frontend/appflowy_web_app/src-tauri/icons/icon.png new file mode 100644 index 0000000000..7cc3853d67 Binary files /dev/null and b/frontend/appflowy_web_app/src-tauri/icons/icon.png differ diff --git a/frontend/appflowy_web_app/src-tauri/rust-toolchain.toml b/frontend/appflowy_web_app/src-tauri/rust-toolchain.toml new file mode 100644 index 0000000000..6d833ff506 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "1.75" diff --git a/frontend/appflowy_web_app/src-tauri/rustfmt.toml b/frontend/appflowy_web_app/src-tauri/rustfmt.toml new file mode 100644 index 0000000000..5cb0d67ee5 --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/rustfmt.toml @@ -0,0 +1,12 @@ +# https://rust-lang.github.io/rustfmt/?version=master&search= +max_width = 100 +tab_spaces = 2 +newline_style = "Auto" +match_block_trailing_comma = true +use_field_init_shorthand = true +use_try_shorthand = true +reorder_imports = true +reorder_modules = true +remove_nested_parens = true +merge_derives = true +edition = "2021" \ No newline at end of file diff --git a/frontend/appflowy_web_app/src-tauri/src/init.rs b/frontend/appflowy_web_app/src-tauri/src/init.rs new file mode 100644 index 0000000000..40c0e5d47b --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/init.rs @@ -0,0 +1,59 @@ +use flowy_core::config::AppFlowyCoreConfig; +use flowy_core::{AppFlowyCore, DEFAULT_NAME}; +use lib_dispatch::runtime::AFPluginRuntime; +use std::sync::Arc; + +use dotenv::dotenv; + +pub fn read_env() { + dotenv().ok(); + + let env = if cfg!(debug_assertions) { + include_str!("../env.development") + } else { + include_str!("../env.production") + }; + + for line in env.lines() { + if let Some((key, value)) = line.split_once('=') { + // Check if the environment variable is not already set in the system + let current_value = std::env::var(key).unwrap_or_default(); + if current_value.is_empty() { + std::env::set_var(key, value); + } + } + } +} + +pub fn init_flowy_core() -> AppFlowyCore { + let config_json = include_str!("../tauri.conf.json"); + let config: tauri_utils::config::Config = serde_json::from_str(config_json).unwrap(); + + let app_version = config.package.version.clone().map(|v| v.to_string()).unwrap_or_else(|| "0.0.0".to_string()); + let mut data_path = tauri::api::path::app_local_data_dir(&config).unwrap(); + if cfg!(debug_assertions) { + data_path.push("data_dev"); + } else { + data_path.push("data"); + } + + let custom_application_path = data_path.to_str().unwrap().to_string(); + let application_path = data_path.to_str().unwrap().to_string(); + let device_id = uuid::Uuid::new_v4().to_string(); + + read_env(); + std::env::set_var("RUST_LOG", "trace"); + + let config = AppFlowyCoreConfig::new( + app_version, + custom_application_path, + application_path, + device_id, + DEFAULT_NAME.to_string(), + ) + .log_filter("trace", vec!["appflowy_tauri".to_string()]); + + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + runtime.block_on(async move { AppFlowyCore::new(config, cloned_runtime).await }) +} diff --git a/frontend/appflowy_web_app/src-tauri/src/main.rs b/frontend/appflowy_web_app/src-tauri/src/main.rs new file mode 100644 index 0000000000..6a69de07fd --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/main.rs @@ -0,0 +1,71 @@ +#![cfg_attr( + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" +)] + +#[allow(dead_code)] +pub const DEEP_LINK_SCHEME: &str = "appflowy-flutter"; +pub const OPEN_DEEP_LINK: &str = "open_deep_link"; + +mod init; +mod notification; +mod request; + +use flowy_notification::{register_notification_sender, unregister_all_notification_sender}; +use init::*; +use notification::*; +use request::*; +use tauri::Manager; +extern crate dotenv; + +fn main() { + tauri_plugin_deep_link::prepare(DEEP_LINK_SCHEME); + + let flowy_core = init_flowy_core(); + tauri::Builder::default() + .invoke_handler(tauri::generate_handler![invoke_request]) + .manage(flowy_core) + .on_window_event(|_window_event| {}) + .on_menu_event(|_menu| {}) + .on_page_load(|window, _payload| { + let app_handler = window.app_handle(); + // Make sure hot reload won't register the notification sender twice + unregister_all_notification_sender(); + register_notification_sender(TSNotificationSender::new(app_handler.clone())); + // tauri::async_runtime::spawn(async move {}); + + window.listen_global(AF_EVENT, move |event| { + on_event(app_handler.clone(), event); + }); + }) + .setup(|_app| { + let splashscreen_window = _app.get_window("splashscreen").unwrap(); + let window = _app.get_window("main").unwrap(); + let handle = _app.handle(); + + // we perform the initialization code on a new task so the app doesn't freeze + tauri::async_runtime::spawn(async move { + // initialize your app here instead of sleeping :) + std::thread::sleep(std::time::Duration::from_secs(2)); + + // After it's done, close the splashscreen and display the main window + splashscreen_window.close().unwrap(); + window.show().unwrap(); + // If you need macOS support this must be called in .setup() ! + // Otherwise this could be called right after prepare() but then you don't have access to tauri APIs + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + tauri_plugin_deep_link::register( + DEEP_LINK_SCHEME, + move |request| { + dbg!(&request); + handle.emit_all(OPEN_DEEP_LINK, request).unwrap(); + }, + ) + .unwrap(/* If listening to the scheme is optional for your app, you don't want to unwrap here. */); + }); + + Ok(()) + }) + .run(tauri::generate_context!()) + .expect("error while running tauri application"); +} diff --git a/frontend/appflowy_web_app/src-tauri/src/notification.rs b/frontend/appflowy_web_app/src-tauri/src/notification.rs new file mode 100644 index 0000000000..b42541edec --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/notification.rs @@ -0,0 +1,35 @@ +use flowy_notification::entities::SubscribeObject; +use flowy_notification::NotificationSender; +use serde::Serialize; +use tauri::{AppHandle, Event, Manager, Wry}; + +#[allow(dead_code)] +pub const AF_EVENT: &str = "af-event"; +pub const AF_NOTIFICATION: &str = "af-notification"; + +#[tracing::instrument(level = "trace")] +pub fn on_event(app_handler: AppHandle, event: Event) {} + +#[allow(dead_code)] +pub fn send_notification(app_handler: AppHandle, payload: P) { + app_handler.emit_all(AF_NOTIFICATION, payload).unwrap(); +} + +pub struct TSNotificationSender { + handler: AppHandle, +} + +impl TSNotificationSender { + pub fn new(handler: AppHandle) -> Self { + Self { handler } + } +} + +impl NotificationSender for TSNotificationSender { + fn send_subject(&self, subject: SubscribeObject) -> Result<(), String> { + self + .handler + .emit_all(AF_NOTIFICATION, subject) + .map_err(|e| format!("{:?}", e)) + } +} diff --git a/frontend/appflowy_web_app/src-tauri/src/request.rs b/frontend/appflowy_web_app/src-tauri/src/request.rs new file mode 100644 index 0000000000..029e71c18c --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/src/request.rs @@ -0,0 +1,45 @@ +use flowy_core::AppFlowyCore; +use lib_dispatch::prelude::{ + AFPluginDispatcher, AFPluginEventResponse, AFPluginRequest, StatusCode, +}; +use tauri::{AppHandle, Manager, State, Wry}; + +#[derive(Clone, Debug, serde::Deserialize)] +pub struct AFTauriRequest { + ty: String, + payload: Vec, +} + +impl std::convert::From for AFPluginRequest { + fn from(event: AFTauriRequest) -> Self { + AFPluginRequest::new(event.ty).payload(event.payload) + } +} + +#[derive(Clone, serde::Serialize)] +pub struct AFTauriResponse { + code: StatusCode, + payload: Vec, +} + +impl std::convert::From for AFTauriResponse { + fn from(response: AFPluginEventResponse) -> Self { + Self { + code: response.status_code, + payload: response.payload.to_vec(), + } + } +} + +// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command +#[tauri::command] +pub async fn invoke_request( + request: AFTauriRequest, + app_handler: AppHandle, +) -> AFTauriResponse { + let request: AFPluginRequest = request.into(); + let state: State = app_handler.state(); + let dispatcher = state.inner().dispatcher(); + let response = AFPluginDispatcher::async_send(dispatcher.as_ref(), request).await; + response.into() +} diff --git a/frontend/appflowy_web_app/src-tauri/tauri.conf.json b/frontend/appflowy_web_app/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..ea11f47def --- /dev/null +++ b/frontend/appflowy_web_app/src-tauri/tauri.conf.json @@ -0,0 +1,113 @@ +{ + "build": { + "beforeBuildCommand": "npm run build:tauri", + "beforeDevCommand": "npm run dev:tauri", + "devPath": "http://localhost:5173", + "distDir": "../dist", + "withGlobalTauri": false + }, + "package": { + "productName": "AppFlowy", + "version": "0.0.1" + }, + "tauri": { + "allowlist": { + "all": false, + "shell": { + "all": false, + "open": true + }, + "fs": { + "all": true, + "scope": [ + "$APPLOCALDATA/**" + ], + "readFile": true, + "writeFile": true, + "readDir": true, + "copyFile": true, + "createDir": true, + "removeDir": true, + "removeFile": true, + "renameFile": true, + "exists": true + }, + "clipboard": { + "all": true, + "writeText": true, + "readText": true + }, + "dialog": { + "all": true, + "ask": true, + "confirm": true, + "message": true, + "open": true, + "save": true + } + }, + "bundle": { + "active": true, + "category": "DeveloperTool", + "copyright": "", + "deb": { + "depends": [] + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": [], + "identifier": "com.appflowy.tauri", + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null, + "minimumSystemVersion": "10.15.0" + }, + "resources": [], + "shortDescription": "", + "targets": "all", + "windows": { + "certificateThumbprint": null, + "digestAlgorithm": "sha256", + "timestampUrl": "" + } + }, + "security": { + "csp": null + }, + "updater": { + "active": false + }, + "windows": [ + { + "fileDropEnabled": false, + "fullscreen": false, + "height": 800, + "resizable": true, + "title": "AppFlowy", + "width": 1200, + "minWidth": 800, + "minHeight": 600, + "visible": false, + "label": "main" + }, + { + "height": 300, + "width": 549, + "decorations": false, + "url": "launch_splash.jpg", + "label": "splashscreen", + "center": true, + "visible": true + } + ] + } +} diff --git a/frontend/appflowy_web_app/src/@types/i18next.d.ts b/frontend/appflowy_web_app/src/@types/i18next.d.ts new file mode 100644 index 0000000000..6adbb4a512 --- /dev/null +++ b/frontend/appflowy_web_app/src/@types/i18next.d.ts @@ -0,0 +1,8 @@ +import resources from './resources'; + +declare module 'i18next' { + interface CustomTypeOptions { + defaultNS: 'translation'; + resources: typeof resources; + } +} diff --git a/frontend/appflowy_web_app/src/@types/resources.ts b/frontend/appflowy_web_app/src/@types/resources.ts new file mode 100644 index 0000000000..6bd90364e0 --- /dev/null +++ b/frontend/appflowy_web_app/src/@types/resources.ts @@ -0,0 +1,7 @@ +import translation from './translations/en.json'; + +const resources = { + translation, +} as const; + +export default resources; diff --git a/frontend/appflowy_web_app/src/App.tsx b/frontend/appflowy_web_app/src/App.tsx new file mode 100644 index 0000000000..8959868616 --- /dev/null +++ b/frontend/appflowy_web_app/src/App.tsx @@ -0,0 +1,35 @@ +import { BrowserRouter, Route, Routes } from 'react-router-dom'; +import { Provider } from 'react-redux'; +import { store } from './stores/store'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorHandlerPage } from './components/error/ErrorHandlerPage'; +import '@/i18n/config'; +import AppTheme from '@/AppTheme'; +import { Toaster } from 'react-hot-toast'; +import ProtectedRoutes from '@/components/auth/ProtectedRoutes'; +import AppConfig from '@/AppConfig'; + +function App() { + return ( + + + + + + + }> + {/*} />*/} + {/*} />*/} + {/*} />*/} + + + + + + + + + ); +} + +export default App; diff --git a/frontend/appflowy_web_app/src/AppConfig.tsx b/frontend/appflowy_web_app/src/AppConfig.tsx new file mode 100644 index 0000000000..452d88e593 --- /dev/null +++ b/frontend/appflowy_web_app/src/AppConfig.tsx @@ -0,0 +1,35 @@ +import React, { createContext, useEffect, useMemo, useState } from 'react'; +import { AFService } from '@/application/services/services.type'; +import { getService } from '@/application/services'; +import { useAppSelector } from '@/stores/store'; + +export const AFConfigContext = createContext< + | { + service: AFService | undefined; + } + | undefined +>(undefined); + + +function AppConfig({ children }: { children: React.ReactNode }) { + const appConfig = useAppSelector((state) => state.app.appConfig); + const [service, setService] = useState(); + + useEffect(() => { + void (async () => { + if (!appConfig) return; + setService(await getService(appConfig)); + })(); + }, [appConfig]); + + const config = useMemo( + () => ({ + service, + }), + [service] + ); + + return {children}; +} + +export default AppConfig; diff --git a/frontend/appflowy_web_app/src/AppTheme.tsx b/frontend/appflowy_web_app/src/AppTheme.tsx new file mode 100644 index 0000000000..b55daaa9c9 --- /dev/null +++ b/frontend/appflowy_web_app/src/AppTheme.tsx @@ -0,0 +1,177 @@ +import React, { useMemo } from 'react'; +import { createTheme, ThemeProvider } from '@mui/material'; + +function AppTheme({ children }: { + children: React.ReactNode; +}) { + const isDark = false; + const theme = useMemo(() => createTheme({ + typography: { + fontFamily: ['Poppins'].join(','), + fontSize: 12, + button: { + textTransform: 'none', + }, + }, + components: { + MuiMenuItem: { + defaultProps: { + sx: { + '&.Mui-selected.Mui-focusVisible': { + backgroundColor: 'var(--fill-list-hover)', + }, + '&.Mui-focusVisible': { + backgroundColor: 'unset', + }, + }, + }, + }, + MuiIconButton: { + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + borderRadius: '4px', + padding: '2px', + }, + }, + }, + MuiButton: { + styleOverrides: { + contained: { + color: 'var(--content-on-fill)', + boxShadow: 'var(--shadow)', + }, + containedPrimary: { + '&:hover': { + backgroundColor: 'var(--fill-default)', + }, + }, + containedInherit: { + color: 'var(--text-title)', + backgroundColor: isDark ? 'rgba(0, 0, 0, 0.4)' : 'rgba(255, 255, 255, 0.4)', + '&:hover': { + backgroundColor: 'var(--bg-body)', + boxShadow: 'var(--shadow)', + }, + }, + outlinedInherit: { + color: 'var(--text-title)', + borderColor: 'var(--line-border)', + '&:hover': { + boxShadow: 'var(--shadow)', + }, + }, + }, + }, + MuiButtonBase: { + defaultProps: { + sx: { + '&.Mui-selected:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + }, + }, + styleOverrides: { + root: { + '&:hover': { + backgroundColor: 'var(--fill-list-hover)', + }, + '&:active': { + backgroundColor: 'var(--fill-list-hover)', + }, + borderRadius: '4px', + padding: '2px', + boxShadow: 'none', + }, + }, + }, + MuiPaper: { + styleOverrides: { + root: { + backgroundImage: 'none', + }, + }, + }, + MuiDialog: { + defaultProps: { + sx: { + '& .MuiBackdrop-root': { + backgroundColor: 'var(--bg-mask)', + }, + }, + }, + }, + + MuiTooltip: { + styleOverrides: { + arrow: { + color: 'var(--bg-tips)', + }, + tooltip: { + backgroundColor: 'var(--bg-tips)', + color: 'var(--text-title)', + fontSize: '0.85rem', + borderRadius: '8px', + fontWeight: 400, + }, + }, + }, + MuiInputBase: { + styleOverrides: { + input: { + backgroundColor: 'transparent !important', + }, + }, + }, + MuiDivider: { + styleOverrides: { + root: { + borderColor: 'var(--line-divider)', + }, + }, + }, + }, + palette: { + mode: isDark ? 'dark' : 'light', + primary: { + main: '#00BCF0', + dark: '#00BCF0', + }, + error: { + main: '#FB006D', + dark: '#D32772', + }, + warning: { + main: '#FFC107', + dark: '#E9B320', + }, + info: { + main: '#00BCF0', + dark: '#2E9DBB', + }, + success: { + main: '#66CF80', + dark: '#3BA856', + }, + text: { + primary: isDark ? '#E2E9F2' : '#333333', + secondary: isDark ? '#7B8A9D' : '#828282', + disabled: isDark ? '#363D49' : '#F2F2F2', + }, + divider: isDark ? '#59647A' : '#BDBDBD', + background: { + default: isDark ? '#1A202C' : '#FFFFFF', + paper: isDark ? '#1A202C' : '#FFFFFF', + }, + }, + + }), [isDark]); + + return ( + {children} + ); +} + +export default AppTheme; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/index.ts b/frontend/appflowy_web_app/src/application/services/index.ts new file mode 100644 index 0000000000..c8c14c8e78 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/index.ts @@ -0,0 +1,12 @@ +import { AFService, AFServiceConfig } from '@/application/services/services.type'; +import { AFClientService } from '$client-services'; + +let service: AFService; + +export async function getService(config: AFServiceConfig) { + if (service) return service; + + service = new AFClientService(config); + await service.load(); + return service; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts new file mode 100644 index 0000000000..917d6cafe3 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/auth.service.ts @@ -0,0 +1,46 @@ +import { AuthService } from '@/application/services/services.type'; +import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/services/user.type'; +import { HttpClient } from '@/application/services/js-services/http/client'; +import { ACCESS_TOKEN_NAME, REFRESH_TOKEN_NAME, TOKEN_TYPE_NAME } from '@/application/services/js-services/http/const'; +import { AFWasmService } from '@/application/services/wasm-services'; + + +export class JSAuthService implements AuthService { + + constructor (private httpClient: HttpClient, private wasmService: AFWasmService) { + // Do nothing + } + + getOAuthURL = async (_provider: ProviderType): Promise => { + return Promise.reject('Not implemented'); + }; + + signInWithOAuth = async ({ uri }: { uri: string }): Promise => { + const params = uri.split('#')[1].split('&'); + const data: Record = {}; + + params.forEach((param) => { + const [key, value] = param.split('='); + + data[key] = value; + }); + + sessionStorage.setItem(TOKEN_TYPE_NAME, data.token_type); + sessionStorage.setItem(ACCESS_TOKEN_NAME, data.access_token); + sessionStorage.setItem(REFRESH_TOKEN_NAME, data.refresh_token); + return this.httpClient.getUser(); + }; + signupWithEmailPassword = async (_params: SignUpWithEmailPasswordParams): Promise => { + return Promise.reject('Not implemented'); + }; + + signinWithEmailPassword = async (email: string, password: string): Promise => { + await this.wasmService.cloudService.signIn(email, password); + return Promise.reject('Not implemented'); + // return this.httpClient.signInWithEmailPassword(email, password); + }; + + signOut = async (): Promise => { + return this.httpClient.logout(); + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts new file mode 100644 index 0000000000..d55ad1771e --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/document.service.ts @@ -0,0 +1,17 @@ +import { DocumentService } from '@/application/services/services.type'; +import { HttpClient } from '@/application/services/js-services/http/client'; +import { CollabType } from '@/application/services/js-services/http/http.type'; + +export class JSDocumentService implements DocumentService { + constructor(private httpClient: HttpClient) {} + + async openDocument(docID: string): Promise { + const workspaceId = '9eebea03-3ed5-4298-86b2-a7f77856d48b'; + const docId = '26d5c8c1-1c66-459c-bc6c-f4da1a663348'; + const data = await this.httpClient.getObject(workspaceId, docId, CollabType.Document); + + console.log(docID, data); + + return; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/client.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/client.ts new file mode 100644 index 0000000000..fc5b99d4b2 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/client.ts @@ -0,0 +1,86 @@ +import { AxiosInstance } from 'axios'; +import { UserProfile, Workspace } from '@/application/services/user.type'; +import { + CollabType, + EncodedCollab, + UserProfilePB, + WorkspacePB, +} from '@/application/services/js-services/http/http.type'; +import { + parseUserPBToUserProfile, + getAxiosInstances, + parseWorkspacePBToWorkspace, +} from '@/application/services/js-services/http/utils'; +import { + ACCESS_TOKEN_NAME, + baseHttpUrls, + gotrueHttpUrls, + REFRESH_TOKEN_NAME, + URL_NAME, +} from '@/application/services/js-services/http/const'; + +export class HttpClient { + private gotrueAPI: AxiosInstance; + private baseAPI: AxiosInstance; + + constructor(private config: { baseURL: string; gotrueURL: string }) { + const { baseInstance, gotrueInstance } = getAxiosInstances(config.baseURL, config.gotrueURL); + + this.gotrueAPI = gotrueInstance; + this.baseAPI = baseInstance; + } + + async signInWithEmailPassword(email: string, password: string): Promise { + const { data } = await this.gotrueAPI.post<{ + access_token: string; + refresh_token: string; + }>(gotrueHttpUrls[URL_NAME.SIGN_IN_WITH_EMAIL], { + email, + password, + }); + + sessionStorage.setItem(ACCESS_TOKEN_NAME, data.access_token); + sessionStorage.setItem(REFRESH_TOKEN_NAME, data.refresh_token); + + return this.getUser(); + } + + async getUser(): Promise { + const { data } = await this.gotrueAPI.get(gotrueHttpUrls[URL_NAME.GET_USER]); + + return parseUserPBToUserProfile(data); + } + + async logout() { + await this.gotrueAPI.post(gotrueHttpUrls[URL_NAME.LOGOUT]); + sessionStorage.removeItem(REFRESH_TOKEN_NAME); + sessionStorage.removeItem(ACCESS_TOKEN_NAME); + } + + async getWorkspaces(): Promise { + const { data } = await this.baseAPI.get(baseHttpUrls[URL_NAME.GET_WORKSPACES]); + + return data.map(parseWorkspacePBToWorkspace); + } + + /** + * Get object(document/database/view) from workspace + * @param workspaceId - workspace id + * @param objectId - document id or database id or view id + * @param objectType - type of object [CollabType] + */ + async getObject(workspaceId: string, objectId: string, objectType: CollabType): Promise { + // const workspaces = await this.getWorkspaces(); + // + // console.log(workspaces); + const { data } = await this.baseAPI.get(baseHttpUrls[URL_NAME.GET_OBJECT](workspaceId, objectId), { + data: JSON.stringify({ + workspace_id: workspaceId, + object_id: objectId, + collab_type: objectType, + }), + }); + + return data; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/const.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/const.ts new file mode 100644 index 0000000000..13977219c3 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/const.ts @@ -0,0 +1,26 @@ +export enum URL_NAME { + SIGN_IN_WITH_EMAIL, + GET_USER, + LOGOUT, + REFRESH_TOKEN, + GET_WORKSPACES, + GET_OBJECT, +} + +export const gotrueHttpUrls = { + [URL_NAME.SIGN_IN_WITH_EMAIL]: '/token?grant_type=password', + [URL_NAME.GET_USER]: '/user', + [URL_NAME.LOGOUT]: '/logout', + [URL_NAME.REFRESH_TOKEN]: '/token?grant_type=refresh_token', +}; + +export const baseHttpUrls = { + [URL_NAME.GET_WORKSPACES]: '/api/workspace', + [URL_NAME.GET_OBJECT]: (workspaceId: string, objectId: string) => `/api/workspace/${workspaceId}/collab/${objectId}`, +}; + +export const ACCESS_TOKEN_NAME = 'access_token'; +export const REFRESH_TOKEN_NAME = 'refresh_token'; +export const TOKEN_TYPE_NAME = 'token_type'; + +export const AUTHORIZATION_NAME = 'Authorization'; diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/http.type.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/http.type.ts new file mode 100644 index 0000000000..d062c7d0e2 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/http.type.ts @@ -0,0 +1,40 @@ +export interface UserProfilePB { + id: string; + name: string; + email: string; + user_metadata: { + avatar_url: string; + full_name: string; + }; +} + +export interface WorkspacePB { + workspace_id: string; + database_storage_id: string; + owner_uid: number; + owner_name: string; + workspace_type: number; + workspace_name: string; + created_at: string; + icon: string; +} + +export enum EncoderVersion { + V1 = 0, + V2 = 1, +} + +export enum CollabType { + Document = 0, + Database = 1, + WorkspaceDatabase = 2, + Folder = 3, + DatabaseRow = 4, + UserAwareness = 5, +} + +export interface EncodedCollab { + state_vector: Uint8Array; + doc_state: Uint8Array; + version: EncoderVersion; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts b/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts new file mode 100644 index 0000000000..7c420a11e6 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/http/utils.ts @@ -0,0 +1,110 @@ +import { UserProfilePB, WorkspacePB } from '@/application/services/js-services/http/http.type'; +import { Authenticator, UserProfile, Workspace } from '@/application/services/user.type'; +import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosRequestConfig } from 'axios'; +import { + ACCESS_TOKEN_NAME, + AUTHORIZATION_NAME, + gotrueHttpUrls, + REFRESH_TOKEN_NAME, + TOKEN_TYPE_NAME, + URL_NAME, +} from '@/application/services/js-services/http/const'; + +async function refreshToken(instance: AxiosInstance) { + const refreshToken = sessionStorage.getItem(REFRESH_TOKEN_NAME); + + if (!refreshToken) { + throw new Error('Refresh token not found'); + } + + const { data } = await instance.post(gotrueHttpUrls[URL_NAME.REFRESH_TOKEN], { + refresh_token: refreshToken, + }); + + sessionStorage.setItem(ACCESS_TOKEN_NAME, data.access_token); + sessionStorage.setItem(REFRESH_TOKEN_NAME, data.refresh_token); + + return data.access_token; +} + +export function getAxiosInstances(baseURL: string, gotrueURL: string) { + const gotrueInstance = axios.create({ + baseURL: gotrueURL, + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + }); + const baseInstance = axios.create({ + baseURL, + headers: { + 'Content-Type': 'application/json', + Accept: '*/*', + }, + }); + + const requestInterceptor = async (config: InternalAxiosRequestConfig) => { + const accessToken = sessionStorage.getItem(ACCESS_TOKEN_NAME); + const tokenType = sessionStorage.getItem(TOKEN_TYPE_NAME) || 'Bearer'; + + if (accessToken) { + config.headers[AUTHORIZATION_NAME] = `${tokenType} ${accessToken}`; + } + + return config; + }; + + const errorInterceptor = async (error: { + response?: AxiosResponse; + config: AxiosRequestConfig; + }) => { + if (error.response?.status === 401 && !error.config.url?.includes(gotrueHttpUrls[URL_NAME.LOGOUT])) { + try { + const tokenType = sessionStorage.getItem(TOKEN_TYPE_NAME) || 'Bearer'; + const accessToken = await refreshToken(gotrueInstance); + + const config = { + ...error.config, + [AUTHORIZATION_NAME]: `${tokenType} ${accessToken}`, + } + + return gotrueInstance.request(config); + } catch (e) { + // do nothing + } + } + + return Promise.reject(error); + }; + + gotrueInstance.interceptors.request.use(requestInterceptor); + gotrueInstance.interceptors.response.use((response) => response, errorInterceptor); + + baseInstance.interceptors.request.use(requestInterceptor); + baseInstance.interceptors.response.use((response) => response, errorInterceptor); + return { + baseInstance, + gotrueInstance, + }; +} + +export function parseUserPBToUserProfile(userPB: UserProfilePB): UserProfile { + return { + id: userPB.id, + email: userPB.email, + authenticator: Authenticator.AppFlowyCloud, + iconUrl: userPB.user_metadata.avatar_url, + }; +} + +export function parseWorkspacePBToWorkspace(workspacePB: WorkspacePB): Workspace { + return { + id: workspacePB.workspace_id, + name: workspacePB.workspace_name, + icon: workspacePB.icon, + owner: { + id: workspacePB.owner_uid, + name: workspacePB.owner_name, + }, + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/index.ts b/frontend/appflowy_web_app/src/application/services/js-services/index.ts new file mode 100644 index 0000000000..d81f2188fb --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/index.ts @@ -0,0 +1,48 @@ +import { + AFService, + AFServiceConfig, + AuthService, + DocumentService, + UserService, +} from '@/application/services/services.type'; +import { JSUserService } from '@/application/services/js-services/user.service'; +import { JSAuthService } from '@/application/services/js-services/auth.service'; +import { AFWasmService } from '@/application/services/wasm-services'; +import { HttpClient } from '@/application/services/js-services/http/client'; +import { JSDocumentService } from '@/application/services/js-services/document.service'; +import { nanoid } from 'nanoid'; + +export class AFClientService implements AFService { + authService: AuthService; + userService: UserService; + wasmService: AFWasmService; + httpClient: HttpClient; + documentService: DocumentService; + private deviceId: string = nanoid(8); + private clientId: string = 'web'; + getDeviceID = (): string => { + return this.deviceId; + }; + + getClientID = (): string => { + return this.clientId; + }; + + constructor(private config: AFServiceConfig) { + this.wasmService = new AFWasmService(config, { + deviceId: this.deviceId, + clientId: this.clientId, + }); + this.httpClient = new HttpClient({ + baseURL: config.cloudConfig.baseURL, + gotrueURL: config.cloudConfig.gotrueURL, + }); + this.authService = new JSAuthService(this.httpClient, this.wasmService); + this.userService = new JSUserService(this.httpClient); + this.documentService = new JSDocumentService(this.httpClient); + } + + async load() { + await this.wasmService.load(); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts new file mode 100644 index 0000000000..fc54da9500 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/js-services/user.service.ts @@ -0,0 +1,11 @@ +import { UserService } from '@/application/services/services.type'; +import { UserProfile } from '@/application/services/user.type'; +import { HttpClient } from '@/application/services/js-services/http/client'; + +export class JSUserService implements UserService { + constructor(private httpClient: HttpClient) {} + + async getUserProfile(): Promise { + return this.httpClient.getUser(); + } +} diff --git a/frontend/appflowy_web_app/src/application/services/services.type.ts b/frontend/appflowy_web_app/src/application/services/services.type.ts new file mode 100644 index 0000000000..ab54d9e97f --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/services.type.ts @@ -0,0 +1,37 @@ +import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/services/user.type'; + +export interface AFService { + getDeviceID: () => string; + getClientID: () => string; + authService: AuthService; + userService: UserService; + documentService: DocumentService; + load: () => Promise; +} + +export interface AFServiceConfig { + cloudConfig: AFCloudConfig; +} + +export interface AFCloudConfig { + baseURL: string; + gotrueURL: string; + wsURL: string; +} + +export interface AuthService { + + getOAuthURL: (provider: ProviderType) => Promise; + signInWithOAuth: (params: { uri: string }) => Promise; + signupWithEmailPassword: (params: SignUpWithEmailPasswordParams) => Promise; + signinWithEmailPassword: (email: string, password: string) => Promise; + signOut: () => Promise; +} + +export interface DocumentService { + openDocument: (docID: string) => Promise; +} + +export interface UserService { + getUserProfile: () => Promise; +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts new file mode 100644 index 0000000000..9a97b5fb9b --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/auth.service.ts @@ -0,0 +1,121 @@ +import { AFCloudConfig, AuthService } from '@/application/services/services.type'; +import { + AuthenticatorPB, + OauthProviderPB, + OauthSignInPB, + SignInPayloadPB, + SignUpPayloadPB, + UserEventGetOauthURLWithProvider, + UserEventOauthSignIn, + UserEventSignInWithEmailPassword, + UserEventSignOut, + UserEventSignUp, + UserProfilePB, +} from './backend/events/flowy-user'; +import { ProviderType, SignUpWithEmailPasswordParams, UserProfile } from '@/application/services/user.type'; + +export class TauriAuthService implements AuthService { + + constructor (private cloudConfig: AFCloudConfig, private clientConfig: { + deviceId: string; + clientId: string; + + }) {} + + getDeviceID = (): string => { + return this.clientConfig.deviceId; + }; + getOAuthURL = async (provider: ProviderType): Promise => { + const providerDataRes = await UserEventGetOauthURLWithProvider( + OauthProviderPB.fromObject({ + provider: provider as number, + }), + ); + + if (!providerDataRes.ok) { + throw new Error(providerDataRes.val.msg); + } + + const providerData = providerDataRes.val; + + return providerData.oauth_url; + }; + + signInWithOAuth = async ({ uri }: { uri: string }): Promise => { + const payload = OauthSignInPB.fromObject({ + authenticator: AuthenticatorPB.AppFlowyCloud, + map: { + sign_in_url: uri, + device_id: this.getDeviceID(), + }, + }); + + const res = await UserEventOauthSignIn(payload); + + if (!res.ok) { + throw new Error(res.val.msg); + } + + return parseUserProfileFrom(res.val); + }; + signinWithEmailPassword = async (email: string, password: string): Promise => { + const payload = SignInPayloadPB.fromObject({ + email, + password, + }); + + const res = await UserEventSignInWithEmailPassword(payload); + + if (!res.ok) { + return Promise.reject(res.val.msg); + } + + return parseUserProfileFrom(res.val); + }; + + signupWithEmailPassword = async (params: SignUpWithEmailPasswordParams): Promise => { + const payload = SignUpPayloadPB.fromObject({ + name: params.name, + email: params.email, + password: params.password, + device_id: this.getDeviceID(), + }); + + const res = await UserEventSignUp(payload); + + if (!res.ok) { + console.error(res.val.msg); + return Promise.reject(res.val.msg); + } + + return parseUserProfileFrom(res.val); + }; + + signOut = async () => { + const res = await UserEventSignOut(); + + if (!res.ok) { + return Promise.reject(res.val.msg); + } + + return; + }; +} + +export function parseUserProfileFrom (userPB: UserProfilePB): UserProfile { + const user = userPB.toObject(); + + return { + id: String(user.id), + email: user.email, + name: user.name, + token: user.token, + iconUrl: user.icon_url, + openaiKey: user.openai_key, + authenticator: user.authenticator as number, + encryptionSign: user.encryption_sign, + encryptionType: user.encryption_type as number, + workspaceId: user.workspace_id, + stabilityAiKey: user.stability_ai_key, + }; +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/backend/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/backend/index.ts new file mode 100644 index 0000000000..38a126a402 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/backend/index.ts @@ -0,0 +1,7 @@ +export * from "./models/flowy-user"; +export * from "./models/flowy-database2"; +export * from "./models/flowy-folder"; +export * from "./models/flowy-document"; +export * from "./models/flowy-error"; +export * from "./models/flowy-config"; +export * from "./models/flowy-date"; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts new file mode 100644 index 0000000000..8e7cc8438a --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/document.service.ts @@ -0,0 +1,68 @@ +import { DocumentService } from '@/application/services/services.type'; +import { OpenDocumentPayloadPB } from './backend'; +import { DocumentEventOpenDocument } from './backend/events/flowy-document'; + +export class TauriDocumentService implements DocumentService { + async openDocument(docId: string): Promise { + const payload = OpenDocumentPayloadPB.fromObject({ + document_id: docId, + }); + + const result = await DocumentEventOpenDocument(payload); + + if (!result.ok) { + return Promise.reject(result.val); + } + + return; + + // const documentDataPB = result.val; + // + // if (!documentDataPB) { + // return Promise.reject('documentDataPB is null'); + // } + // + // const data: { + // viewId: string; + // rootId: string; + // nodeMap: Record; + // childrenMap: Record; + // relativeMap: Record; + // deltaMap: Record; + // externalIdMap: Record; + // } = { + // viewId: docId, + // rootId: documentDataPB.page_id, + // nodeMap: {}, + // childrenMap: {}, + // relativeMap: {}, + // deltaMap: {}, + // externalIdMap: {}, + // }; + // + // get(documentDataPB, BLOCK_MAP_NAME).forEach((block) => { + // Object.assign(data.nodeMap, { + // [block.id]: blockPB2Node(block), + // }); + // data.relativeMap[block.children_id] = block.id; + // if (block.external_id) { + // data.externalIdMap[block.external_id] = block.id; + // } + // }); + // + // get(documentDataPB, [META_NAME, CHILDREN_MAP_NAME]).forEach((child, key) => { + // const blockId = data.relativeMap[key]; + // + // data.childrenMap[blockId] = child.children; + // }); + // + // get(documentDataPB, [META_NAME, TEXT_MAP_NAME]).forEach((delta, key) => { + // const blockId = data.externalIdMap[key]; + // + // data.deltaMap[blockId] = delta ? JSON.parse(delta) : []; + // }); + // + // // return data; + // return; + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts new file mode 100644 index 0000000000..b012f272ce --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/index.ts @@ -0,0 +1,39 @@ +import { + AFService, + AFServiceConfig, + AuthService, + DocumentService, + UserService, +} from '@/application/services/services.type'; +import { TauriAuthService } from '@/application/services/tauri-services/auth.service'; +import { TauriUserService } from '@/application/services/tauri-services/user.service'; +import { TauriDocumentService } from '@/application/services/tauri-services/document.service'; +import { nanoid } from 'nanoid'; + +export class AFClientService implements AFService { + authService: AuthService; + userService: UserService; + documentService: DocumentService; + private deviceId: string = nanoid(8); + private clientId: string = 'web'; + getDeviceID = (): string => { + return this.deviceId; + }; + + getClientID = (): string => { + return this.clientId; + }; + + constructor(config: AFServiceConfig) { + this.authService = new TauriAuthService(config.cloudConfig, { + deviceId: this.deviceId, + clientId: this.clientId, + }); + this.userService = new TauriUserService(); + this.documentService = new TauriDocumentService(); + } + + async load() { + // Do nothing + } +} diff --git a/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts b/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts new file mode 100644 index 0000000000..0f76fde6c1 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/tauri-services/user.service.ts @@ -0,0 +1,16 @@ +import { UserService } from '@/application/services/services.type'; +import { UserProfile } from '@/application/services/user.type'; +import { UserEventGetUserProfile } from './backend/events/flowy-user'; +import { parseUserProfileFrom } from '@/application/services/tauri-services/auth.service'; + +export class TauriUserService implements UserService { + async getUserProfile (): Promise { + const res = await UserEventGetUserProfile(); + + if (res.ok) { + return parseUserProfileFrom(res.val); + } + + return null; + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/user.type.ts b/frontend/appflowy_web_app/src/application/services/user.type.ts new file mode 100644 index 0000000000..cda8647761 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/user.type.ts @@ -0,0 +1,73 @@ +export enum Authenticator { + Local = 0, + Supabase = 1, + AppFlowyCloud = 2, +} + +export enum EncryptionType { + NoEncryption = 0, + Symmetric = 1, +} + +export interface UserProfile { + id?: string; + email?: string; + name?: string; + token?: string; + iconUrl?: string; + openaiKey?: string; + authenticator?: Authenticator; + encryptionSign?: string; + encryptionType?: EncryptionType; + workspaceId?: string; + stabilityAiKey?: string; +} + +export interface Workspace { + id: string; + name: string; + icon: string; + owner: { + id: number; + name: string; + }; +} + +export interface SignUpWithEmailPasswordParams { + name: string; + email: string; + password: string; +} + +export enum ProviderType { + Apple = 0, + Azure = 1, + Bitbucket = 2, + Discord = 3, + Facebook = 4, + Figma = 5, + Github = 6, + Gitlab = 7, + Google = 8, + Keycloak = 9, + Kakao = 10, + Linkedin = 11, + Notion = 12, + Spotify = 13, + Slack = 14, + Workos = 15, + Twitch = 16, + Twitter = 17, + Email = 18, + Phone = 19, + Zoom = 20, +} + +export interface UserSetting { + workspaceId: string; + latestView?: { + id: string; + name: string; + }; + hasLatestView: boolean; +} diff --git a/frontend/appflowy_web_app/src/application/services/wasm-services/cloud.service.ts b/frontend/appflowy_web_app/src/application/services/wasm-services/cloud.service.ts new file mode 100644 index 0000000000..5eb4032f47 --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/wasm-services/cloud.service.ts @@ -0,0 +1,35 @@ +import { CloudServiceConfig } from '@/application/services/wasm-services/cloud.type'; +import { ClientAPI } from '@appflowyinc/client-api-wasm'; + +export class CloudService { + private client?: ClientAPI; + + constructor (private config: CloudServiceConfig) { + // Do nothing + } + + async init () { + this.client = ClientAPI.new({ + base_url: this.config.baseURL, + ws_addr: this.config.wsURL, + gotrue_url: this.config.gotrueURL, + device_id: this.config.deviceId, + client_id: this.config.clientId, + configuration: { + compression_quality: 8, + compression_buffer_size: 10240, + }, + }); + + } + + async signIn (email: string, password: string) { + try { + const res = await this.client?.sign_in_password(email, password); + + console.log(res); + } catch (error) { + console.error(error); + } + } +} diff --git a/frontend/appflowy_web_app/src/application/services/wasm-services/cloud.type.ts b/frontend/appflowy_web_app/src/application/services/wasm-services/cloud.type.ts new file mode 100644 index 0000000000..1eff10225d --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/wasm-services/cloud.type.ts @@ -0,0 +1,7 @@ +import { AFCloudConfig } from '@/application/services/services.type'; + +export type CloudServiceEventPayload = Record; +export type CloudServiceConfig = AFCloudConfig & { + deviceId: string; + clientId: string; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/application/services/wasm-services/index.ts b/frontend/appflowy_web_app/src/application/services/wasm-services/index.ts new file mode 100644 index 0000000000..59bb5425cd --- /dev/null +++ b/frontend/appflowy_web_app/src/application/services/wasm-services/index.ts @@ -0,0 +1,20 @@ +import { AFServiceConfig } from '@/application/services/services.type'; +import { CloudService } from '@/application/services/wasm-services/cloud.service'; + +export class AFWasmService { + cloudService: CloudService; + + constructor (private config: AFServiceConfig, clientConfig: { + deviceId: string; + clientId: string; + }) { + this.cloudService = new CloudService({ + ...config.cloudConfig, + ...clientConfig, + }); + } + + async load () { + await this.cloudService.init(); + } +} diff --git a/frontend/appflowy_web_app/src/assets/add.svg b/frontend/appflowy_web_app/src/assets/add.svg new file mode 100644 index 0000000000..049be05cec --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/add.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/align-center.svg b/frontend/appflowy_web_app/src/assets/align-center.svg new file mode 100644 index 0000000000..f4f4999514 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/align-center.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/align-left.svg b/frontend/appflowy_web_app/src/assets/align-left.svg new file mode 100644 index 0000000000..23957285c7 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/align-left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/align-right.svg b/frontend/appflowy_web_app/src/assets/align-right.svg new file mode 100644 index 0000000000..bca2d14fc7 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/align-right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/arrow-left.svg b/frontend/appflowy_web_app/src/assets/arrow-left.svg new file mode 100644 index 0000000000..e4ab9068be --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/arrow-right.svg b/frontend/appflowy_web_app/src/assets/arrow-right.svg new file mode 100644 index 0000000000..dc40ae52a6 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/board.svg b/frontend/appflowy_web_app/src/assets/board.svg new file mode 100644 index 0000000000..0bb0e3fabe --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/board.svg @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/bold.svg b/frontend/appflowy_web_app/src/assets/bold.svg new file mode 100644 index 0000000000..878b6329b3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/bold.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/close.svg b/frontend/appflowy_web_app/src/assets/close.svg new file mode 100644 index 0000000000..b519b419c0 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/copy.svg b/frontend/appflowy_web_app/src/assets/copy.svg new file mode 100644 index 0000000000..e21e6cb082 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/dark-logo.svg b/frontend/appflowy_web_app/src/assets/dark-logo.svg new file mode 100644 index 0000000000..80d8c4132e --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/dark-logo.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg new file mode 100644 index 0000000000..15632e4ea6 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/checkbox-check.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg b/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg new file mode 100644 index 0000000000..6c487795c6 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/checkbox-uncheck.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg b/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-attach.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg b/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg new file mode 100644 index 0000000000..3a88d236a1 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-checklist.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-date.svg b/frontend/appflowy_web_app/src/assets/database/field-type-date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg b/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg new file mode 100644 index 0000000000..634af3e361 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-last-edited-time.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-multi-select.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-number.svg b/frontend/appflowy_web_app/src/assets/database/field-type-number.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-number.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-person.svg b/frontend/appflowy_web_app/src/assets/database/field-type-person.svg new file mode 100644 index 0000000000..2fc04be065 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-person.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg b/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg new file mode 100644 index 0000000000..f82a41d226 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-relation.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg b/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg new file mode 100644 index 0000000000..8ccbc9a2e3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-single-select.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-text.svg b/frontend/appflowy_web_app/src/assets/database/field-type-text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/database/field-type-url.svg b/frontend/appflowy_web_app/src/assets/database/field-type-url.svg new file mode 100644 index 0000000000..f00f5c7aa2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/database/field-type-url.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/date.svg b/frontend/appflowy_web_app/src/assets/date.svg new file mode 100644 index 0000000000..78243f1e75 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/date.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/delete.svg b/frontend/appflowy_web_app/src/assets/delete.svg new file mode 100644 index 0000000000..9e51636798 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/delete.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/details.svg b/frontend/appflowy_web_app/src/assets/details.svg new file mode 100644 index 0000000000..22c6830916 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/details.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/document.svg b/frontend/appflowy_web_app/src/assets/document.svg new file mode 100644 index 0000000000..b00e1cfb38 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/document.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/drag.svg b/frontend/appflowy_web_app/src/assets/drag.svg new file mode 100644 index 0000000000..627c959f9f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/drag.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/dropdown.svg b/frontend/appflowy_web_app/src/assets/dropdown.svg new file mode 100644 index 0000000000..95e4964b53 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/dropdown.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/edit.svg b/frontend/appflowy_web_app/src/assets/edit.svg new file mode 100644 index 0000000000..ae93287114 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/edit.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_close.svg b/frontend/appflowy_web_app/src/assets/eye_close.svg new file mode 100644 index 0000000000..116c715ca8 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/eye_close.svg @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/eye_open.svg b/frontend/appflowy_web_app/src/assets/eye_open.svg new file mode 100644 index 0000000000..fa3017c04d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/eye_open.svg @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/grid.svg b/frontend/appflowy_web_app/src/assets/grid.svg new file mode 100644 index 0000000000..c397af8130 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/grid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/h1.svg b/frontend/appflowy_web_app/src/assets/h1.svg new file mode 100644 index 0000000000..b33bd52135 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/h1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/h2.svg b/frontend/appflowy_web_app/src/assets/h2.svg new file mode 100644 index 0000000000..7449c57391 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/h2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/h3.svg b/frontend/appflowy_web_app/src/assets/h3.svg new file mode 100644 index 0000000000..0976945974 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/h3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/hide-menu.svg b/frontend/appflowy_web_app/src/assets/hide-menu.svg new file mode 100644 index 0000000000..ce88af8ea7 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/hide-menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/hide.svg b/frontend/appflowy_web_app/src/assets/hide.svg new file mode 100644 index 0000000000..22001ef65d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/hide.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/image.svg b/frontend/appflowy_web_app/src/assets/image.svg new file mode 100644 index 0000000000..0739605066 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/images/default_cover.jpg b/frontend/appflowy_web_app/src/assets/images/default_cover.jpg new file mode 100644 index 0000000000..aeaa6a0f29 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/images/default_cover.jpg differ diff --git a/frontend/appflowy_web_app/src/assets/information.svg b/frontend/appflowy_web_app/src/assets/information.svg new file mode 100644 index 0000000000..37ca4d5837 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/information.svg @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/inline-code.svg b/frontend/appflowy_web_app/src/assets/inline-code.svg new file mode 100644 index 0000000000..3585603096 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/inline-code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/italic.svg b/frontend/appflowy_web_app/src/assets/italic.svg new file mode 100644 index 0000000000..b295c230f0 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/italic.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/left.svg b/frontend/appflowy_web_app/src/assets/left.svg new file mode 100644 index 0000000000..0f771a3858 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/light-logo.svg b/frontend/appflowy_web_app/src/assets/light-logo.svg new file mode 100644 index 0000000000..f5cd761ba7 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/light-logo.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/link.svg b/frontend/appflowy_web_app/src/assets/link.svg new file mode 100644 index 0000000000..5fbcc8d787 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/link.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/list-dropdown.svg b/frontend/appflowy_web_app/src/assets/list-dropdown.svg new file mode 100644 index 0000000000..4a8424c5f8 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/list-dropdown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/list.svg b/frontend/appflowy_web_app/src/assets/list.svg new file mode 100644 index 0000000000..97a2e9c434 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/logo.svg b/frontend/appflowy_web_app/src/assets/logo.svg new file mode 100644 index 0000000000..b1ac8d66fb --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/mention.svg b/frontend/appflowy_web_app/src/assets/mention.svg new file mode 100644 index 0000000000..b98318132c --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/mention.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/more.svg b/frontend/appflowy_web_app/src/assets/more.svg new file mode 100644 index 0000000000..b191e64a10 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/more.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/numbers.svg b/frontend/appflowy_web_app/src/assets/numbers.svg new file mode 100644 index 0000000000..9d8b98d10d --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/numbers.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/open.svg b/frontend/appflowy_web_app/src/assets/open.svg new file mode 100644 index 0000000000..b443c8b993 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/open.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/quote.svg b/frontend/appflowy_web_app/src/assets/quote.svg new file mode 100644 index 0000000000..57839231ff --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/quote.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/react.svg b/frontend/appflowy_web_app/src/assets/react.svg new file mode 100644 index 0000000000..6c87de9bb3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/right.svg b/frontend/appflowy_web_app/src/assets/right.svg new file mode 100644 index 0000000000..7d738f4e69 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/appflowy_web_app/src/assets/search.svg b/frontend/appflowy_web_app/src/assets/search.svg new file mode 100644 index 0000000000..a8a92df509 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/select-check.svg b/frontend/appflowy_web_app/src/assets/select-check.svg new file mode 100644 index 0000000000..05caec861a --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/select-check.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/settings.svg b/frontend/appflowy_web_app/src/assets/settings.svg new file mode 100644 index 0000000000..92140a3c23 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/settings/account.svg b/frontend/appflowy_web_app/src/assets/settings/account.svg new file mode 100644 index 0000000000..fddfca7575 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/settings/account.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/assets/settings/check_circle.svg b/frontend/appflowy_web_app/src/assets/settings/check_circle.svg new file mode 100644 index 0000000000..c6fa56067b --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/settings/check_circle.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/settings/dark.png b/frontend/appflowy_web_app/src/assets/settings/dark.png new file mode 100644 index 0000000000..15a2db5eb8 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/dark.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/discord.png b/frontend/appflowy_web_app/src/assets/settings/discord.png new file mode 100644 index 0000000000..f71e68c6ed Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/discord.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/github.png b/frontend/appflowy_web_app/src/assets/settings/github.png new file mode 100644 index 0000000000..597883b7a3 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/github.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/google.png b/frontend/appflowy_web_app/src/assets/settings/google.png new file mode 100644 index 0000000000..60032628a8 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/google.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/light.png b/frontend/appflowy_web_app/src/assets/settings/light.png new file mode 100644 index 0000000000..09b2d9c475 Binary files /dev/null and b/frontend/appflowy_web_app/src/assets/settings/light.png differ diff --git a/frontend/appflowy_web_app/src/assets/settings/workplace.svg b/frontend/appflowy_web_app/src/assets/settings/workplace.svg new file mode 100644 index 0000000000..2076ea3e2c --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/settings/workplace.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/show-menu.svg b/frontend/appflowy_web_app/src/assets/show-menu.svg new file mode 100644 index 0000000000..8baf55bffd --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/show-menu.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/appflowy_web_app/src/assets/sort.svg b/frontend/appflowy_web_app/src/assets/sort.svg new file mode 100644 index 0000000000..e3b6a49a56 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/sort.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/assets/strikethrough.svg b/frontend/appflowy_web_app/src/assets/strikethrough.svg new file mode 100644 index 0000000000..c118422a15 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/strikethrough.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/text.svg b/frontend/appflowy_web_app/src/assets/text.svg new file mode 100644 index 0000000000..7befa5080f --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/text.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/todo-list.svg b/frontend/appflowy_web_app/src/assets/todo-list.svg new file mode 100644 index 0000000000..37f52c47ed --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/todo-list.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/underline.svg b/frontend/appflowy_web_app/src/assets/underline.svg new file mode 100644 index 0000000000..f5d53f0ec2 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/underline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/appflowy_web_app/src/assets/up.svg b/frontend/appflowy_web_app/src/assets/up.svg new file mode 100644 index 0000000000..bd8f3067d3 --- /dev/null +++ b/frontend/appflowy_web_app/src/assets/up.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/appflowy_web_app/src/components/_shared/notify/index.ts b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts new file mode 100644 index 0000000000..1086cabdfd --- /dev/null +++ b/frontend/appflowy_web_app/src/components/_shared/notify/index.ts @@ -0,0 +1,27 @@ +import toast from 'react-hot-toast'; + +const commonOptions = { + style: { + background: 'var(--bg-base)', + color: 'var(--text-title)', + shadows: 'var(--shadow)', + }, +}; + +export const notify = { + success: (message: string) => { + toast.success(message, commonOptions); + }, + error: (message: string) => { + toast.error(message, commonOptions); + }, + loading: (message: string) => { + toast.loading(message, commonOptions); + }, + info: (message: string) => { + toast(message, commonOptions); + }, + clear: () => { + toast.dismiss(); + }, +}; diff --git a/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx b/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx new file mode 100644 index 0000000000..49bb649b2a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/LoginButtonGroup.tsx @@ -0,0 +1,66 @@ +import Button from '@mui/material/Button'; +import GoogleIcon from '@/assets/settings/google.png'; +import GithubIcon from '@/assets/settings/github.png'; +import DiscordIcon from '@/assets/settings/discord.png'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from './auth.hooks'; +import { ProviderType } from '@/application/services/user.type'; +import { useState } from 'react'; +import { EmailOutlined } from '@mui/icons-material'; +import SignInWithEmail from './SignInWithEmail'; + +export const LoginButtonGroup = () => { + const { t } = useTranslation(); + const [openSignInWithEmail, setOpenSignInWithEmail] = useState(false); + const { signInWithProvider } = useAuth(); + + return ( +
+ + + + + setOpenSignInWithEmail(false)} /> +
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx b/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx new file mode 100644 index 0000000000..96a95c2343 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/ProtectedRoutes.tsx @@ -0,0 +1,74 @@ +import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import { useAuth } from '@/components/auth/auth.hooks'; +import { currentUserActions, LoginState } from '@/stores/currentUser/slice'; +import { useAppDispatch } from '@/stores/store'; +import { getPlatform } from '@/utils/platform'; +import SplashScreen from '@/components/auth/SplashScreen'; +import { CircularProgress, Portal } from '@mui/material'; +import { ReactComponent as Logo } from '@/assets/logo.svg'; + +const TauriAuth = lazy(() => import('@/components/tauri/TauriAuth')); + +function ProtectedRoutes() { + const { currentUser, checkUser } = useAuth(); + + const isLoading = currentUser?.loginState === LoginState.LOADING; + const [checked, setChecked] = useState(false); + + const checkUserStatus = useCallback(async () => { + try { + await checkUser(); + } finally { + setChecked(true); + } + }, [checkUser]); + + useEffect(() => { + void checkUserStatus(); + }, [checkUserStatus]); + + const platform = useMemo(() => getPlatform(), []); + + return ( +
+ {checked ? ( + + ) : ( +
+ +
+ )} + + {isLoading && } + {platform.isTauri && } +
+ ); +} + +export default ProtectedRoutes; + +const StartLoading = () => { + const dispatch = useAppDispatch(); + + useEffect(() => { + const preventDefault = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + dispatch(currentUserActions.resetLoginState()); + } + }; + + document.addEventListener('keydown', preventDefault, true); + + return () => { + document.removeEventListener('keydown', preventDefault, true); + }; + }, [dispatch]); + return ( + +
+ +
+
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx b/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx new file mode 100644 index 0000000000..020af731ac --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/SignInWithEmail.tsx @@ -0,0 +1,70 @@ +import { Button, CircularProgress, Dialog, DialogActions, DialogContent, TextField } from '@mui/material'; +import React, { useState } from 'react'; +import { useAuth } from '@/components/auth/auth.hooks'; +import { useTranslation } from 'react-i18next'; + +function SignInWithEmail({ open, onClose }: { open: boolean; onClose: () => void }) { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const { signInWithEmailPassword } = useAuth(); + const handleSignIn = async () => { + setLoading(true); + try { + await signInWithEmailPassword(email, password); + } catch (e) { + // Handle error + } + + setLoading(false); + }; + + return ( + { + if (e.key === 'Enter') { + e.preventDefault(); + void handleSignIn(); + } + }} + > + + setEmail(e.target.value)} + /> + setPassword(e.target.value)} + /> + + + + + + + ); +} + +export default SignInWithEmail; diff --git a/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx b/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx new file mode 100644 index 0000000000..4757ecdd5d --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/SplashScreen.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { Outlet } from 'react-router-dom'; +import Layout from '@/components/layout/Layout'; +import Welcome from './Welcome'; + +function SplashScreen({ + isAuthenticated, +}: { + isAuthenticated: boolean; +}) { + if (isAuthenticated) { + return ( + + + + ); + } else { + return ; + } +} + +export default SplashScreen; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/auth/Welcome.tsx b/frontend/appflowy_web_app/src/components/auth/Welcome.tsx new file mode 100644 index 0000000000..b40bd25ded --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/Welcome.tsx @@ -0,0 +1,51 @@ +import { ReactComponent as AppflowyLogo } from '@/assets/logo.svg'; +import Button from '@mui/material/Button'; +import { useTranslation } from 'react-i18next'; +import { useAuth } from './auth.hooks'; +import { LoginButtonGroup } from './LoginButtonGroup'; + +export const Welcome = () => { + const { signInAsAnonymous } = useAuth(); + const { t } = useTranslation(); + + return ( + <> +
e.preventDefault()} method="POST"> +
+
+ +
+ +
+ + {t('welcomeTo')} {t('appName')} + +
+ +
+ +
+
+ {t('signIn.or')} +
+
+
+ +
+
+
+ + + ); +}; + +export default Welcome; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts new file mode 100644 index 0000000000..b21e7f881a --- /dev/null +++ b/frontend/appflowy_web_app/src/components/auth/auth.hooks.ts @@ -0,0 +1,185 @@ +import { useAppDispatch, useAppSelector } from '@/stores/store'; +import { useCallback, useContext } from 'react'; +import { nanoid } from 'nanoid'; +import { open } from '@tauri-apps/api/shell'; +import { ProviderType, UserProfile } from '@/application/services/user.type'; +import { currentUserActions } from '@/stores/currentUser/slice'; +import { AFConfigContext } from '@/AppConfig'; +import { notify } from '@/components/_shared/notify'; + +export const useAuth = () => { + const dispatch = useAppDispatch(); + const AFConfig = useContext(AFConfigContext); + const currentUser = useAppSelector((state) => state.currentUser); + + const handleSuccess = useCallback(() => { + notify.clear(); + dispatch(currentUserActions.loginSuccess()); + }, [dispatch]); + const setUser = useCallback( + async (userProfile: Partial) => { + handleSuccess(); + dispatch(currentUserActions.updateUser(userProfile)); + }, + [dispatch, handleSuccess] + ); + + const handleStart = useCallback(() => { + notify.clear(); + notify.loading('Loading...'); + dispatch(currentUserActions.loginStart()); + }, [dispatch]); + + const handleError = useCallback( + ({ message }: { message: string }) => { + notify.clear(); + notify.error(message); + dispatch(currentUserActions.loginError()); + }, + [dispatch] + ); + + // Check if the user is authenticated + const checkUser = useCallback(async () => { + handleStart(); + try { + const userProfile = await AFConfig?.service?.userService.getUserProfile(); + + if (!userProfile) { + throw new Error('Failed to check user'); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to check user', + }); + + return Promise.reject('Failed to check user'); + } + }, [AFConfig?.service?.userService, handleError, handleStart, setUser]); + + const register = useCallback( + async (email: string, password: string, name: string): Promise => { + handleStart(); + try { + const userProfile = await AFConfig?.service?.authService.signupWithEmailPassword({ + email, + password, + name, + }); + + if (!userProfile) { + throw new Error('Failed to register'); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to register', + }); + return null; + } + }, + [handleStart, AFConfig?.service?.authService, setUser, handleError] + ); + + const logout = useCallback(async () => { + try { + await AFConfig?.service?.authService.signOut(); + dispatch(currentUserActions.logout()); + } catch (e) { + handleError({ + message: 'Failed to logout', + }); + } + }, [AFConfig?.service?.authService, dispatch, handleError]); + + const signInAsAnonymous = useCallback(async () => { + const fakeEmail = nanoid(8) + '@appflowy.io'; + const fakePassword = 'AppFlowy123@'; + const fakeName = 'Me'; + + await register(fakeEmail, fakePassword, fakeName); + }, [register]); + + const signInWithProvider = useCallback( + async (provider: ProviderType) => { + handleStart(); + try { + const url = await AFConfig?.service?.authService.getOAuthURL(provider); + + if (!url) { + throw new Error('Failed to sign in'); + } + + await open(url); + } catch { + handleError({ + message: 'Failed to sign in', + }); + } + }, + [AFConfig?.service?.authService, handleError, handleStart] + ); + + const signInWithOAuth = useCallback( + async (uri: string) => { + handleStart(); + try { + await AFConfig?.service?.authService.signInWithOAuth({ uri }); + const userProfile = await AFConfig?.service?.userService.getUserProfile(); + + if (!userProfile) { + throw new Error('Failed to sign in'); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to sign in', + }); + } + }, + [AFConfig?.service?.authService, AFConfig?.service?.userService, handleError, handleStart, setUser] + ); + + const signInWithEmailPassword = useCallback( + async (email: string, password: string) => { + handleStart(); + try { + const userProfile = await AFConfig?.service?.authService.signinWithEmailPassword(email, password); + + if (!userProfile) { + throw new Error('Failed to sign in'); + } + + await setUser(userProfile); + + return userProfile; + } catch (e) { + handleError({ + message: 'Failed to sign in', + }); + } + }, + [AFConfig?.service?.authService, handleError, handleStart, setUser] + ); + + return { + currentUser, + checkUser, + register, + logout, + signInWithProvider, + signInAsAnonymous, + signInWithOAuth, + signInWithEmailPassword, + }; +}; diff --git a/frontend/appflowy_web_app/src/components/error/Error.hooks.ts b/frontend/appflowy_web_app/src/components/error/Error.hooks.ts new file mode 100644 index 0000000000..a9da4ed829 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/Error.hooks.ts @@ -0,0 +1,39 @@ +import { useAppDispatch, useAppSelector } from '@/stores/store'; +import { useCallback, useEffect, useState } from 'react'; +import {errorActions} from "@/stores/error/slice"; + +export const useError = (e: Error) => { + const dispatch = useAppDispatch(); + const error = useAppSelector((state) => state.error); + const [errorMessage, setErrorMessage] = useState(''); + const [displayError, setDisplayError] = useState(false); + + useEffect(() => { + setDisplayError(error.display); + setErrorMessage(error.message); + }, [error]); + + const showError = useCallback( + (msg: string) => { + dispatch(errorActions.showError(msg)); + }, + [dispatch] + ); + + useEffect(() => { + if (e) { + showError(e.message); + } + }, [e, showError]); + + const hideError = () => { + dispatch(errorActions.hideError()); + }; + + return { + showError, + hideError, + errorMessage, + displayError, + }; +}; diff --git a/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx b/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx new file mode 100644 index 0000000000..1bb15f2ca3 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/ErrorHandlerPage.tsx @@ -0,0 +1,8 @@ +import { useError } from './Error.hooks'; +import { ErrorModal } from './ErrorModal'; + +export const ErrorHandlerPage = ({ error }: { error: Error }) => { + const { hideError, errorMessage, displayError } = useError(error); + + return displayError ? : <>; +}; diff --git a/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx b/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx new file mode 100644 index 0000000000..c4382c8182 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/error/ErrorModal.tsx @@ -0,0 +1,33 @@ +import { ReactComponent as InformationSvg } from '@/assets/information.svg'; +import { ReactComponent as CloseSvg } from '@/assets/close.svg'; +import { Button } from "@mui/material"; + +export const ErrorModal = ({ message, onClose }: { message: string; onClose: () => void }) => { + return ( +
+
+ +
+ +
+

Oops.. something went wrong

+

{message}

+ + +
+
+ ); +}; diff --git a/frontend/appflowy_web_app/src/components/layout/Layout.tsx b/frontend/appflowy_web_app/src/components/layout/Layout.tsx new file mode 100644 index 0000000000..ebc9bcba56 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/layout/Layout.tsx @@ -0,0 +1,26 @@ +import React, { useContext } from 'react'; +import { Button } from '@mui/material'; +import { useAuth } from '@/components/auth/auth.hooks'; +import { AFConfigContext } from '@/AppConfig'; + +function Layout({ children }: { children: React.ReactNode }) { + const { logout } = useAuth(); + const AFConfig = useContext(AFConfigContext); + + return ( +
+
hello world
+ + + {children} +
+ ); +} + +export default Layout; diff --git a/frontend/appflowy_web_app/src/components/tauri/TauriAuth.tsx b/frontend/appflowy_web_app/src/components/tauri/TauriAuth.tsx new file mode 100644 index 0000000000..5ee605463e --- /dev/null +++ b/frontend/appflowy_web_app/src/components/tauri/TauriAuth.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react'; +import { useDeepLink } from '@/components/tauri/tauri.hooks'; + +function TauriAuth() { + const { + onDeepLink, + } = useDeepLink(); + + useEffect(() => { + void onDeepLink(); + }, [onDeepLink]); + + return null; +} + +export default TauriAuth; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts b/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts new file mode 100644 index 0000000000..f95c2ca696 --- /dev/null +++ b/frontend/appflowy_web_app/src/components/tauri/tauri.hooks.ts @@ -0,0 +1,44 @@ +import { useCallback } from 'react'; +import { notify } from '@/components/_shared/notify'; +import { useAuth } from '@/components/auth/auth.hooks'; + +export function useDeepLink() { + const { + signInWithOAuth, + } = useAuth(); + const onDeepLink = useCallback(async () => { + const { event } = await import('@tauri-apps/api'); + + // On macOS You still have to install a .app bundle you got from tauri build --debug for this to work! + return await event.listen('open_deep_link', async (e) => { + const payload = e.payload as string; + + const [, hash] = payload.split('//#'); + const obj = parseHash(hash); + + if (!obj.access_token) { + notify.error('Failed to sign in, the access token is missing'); + // update login state to error + return; + } + + await signInWithOAuth(payload); + }); + }, [signInWithOAuth]); + + return { + onDeepLink, + }; + +} + +function parseHash(hash: string) { + const hashParams = new URLSearchParams(hash); + const hashObject: Record = {}; + + for (const [key, value] of hashParams) { + hashObject[key] = value; + } + + return hashObject; +} diff --git a/frontend/appflowy_web_app/src/i18n/config.ts b/frontend/appflowy_web_app/src/i18n/config.ts new file mode 100644 index 0000000000..b2a116e0b6 --- /dev/null +++ b/frontend/appflowy_web_app/src/i18n/config.ts @@ -0,0 +1,15 @@ +import i18next from 'i18next'; +import LanguageDetector from 'i18next-browser-languagedetector'; +import { initReactI18next } from 'react-i18next'; +import resourcesToBackend from 'i18next-resources-to-backend'; + +void i18next + .use(resourcesToBackend((language: string) => import(`../@types/translations/${language}.json`))) + .use(LanguageDetector) + .use(initReactI18next) + .init({ + lng: 'en', + defaultNS: 'translation', + debug: false, + fallbackLng: 'en', + }); diff --git a/frontend/appflowy_web_app/src/main.tsx b/frontend/appflowy_web_app/src/main.tsx new file mode 100644 index 0000000000..f1236efb5f --- /dev/null +++ b/frontend/appflowy_web_app/src/main.tsx @@ -0,0 +1,7 @@ +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './styles/tailwind.css'; +import './styles/font.css'; +import './styles/template.css'; + +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/frontend/appflowy_web_app/src/stores/app/slice.ts b/frontend/appflowy_web_app/src/stores/app/slice.ts new file mode 100644 index 0000000000..2aac422b83 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/app/slice.ts @@ -0,0 +1,36 @@ +import { AFServiceConfig } from '@/application/services/services.type'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +const defaultConfig: AFServiceConfig = { + cloudConfig: { + baseURL: import.meta.env.DEV + ? import.meta.env.AF_BASE_URL || 'https://test.appflowy.cloud' + : 'https://beta.appflowy.cloud', + gotrueURL: import.meta.env.DEV + ? import.meta.env.AF_GOTRUE_URL || 'https://test.appflowy.cloud/gotrue' + : 'https://beta.appflowy.cloud/gotrue', + wsURL: import.meta.env.DEV + ? import.meta.env.AF_WS_URL || 'wss://test.appflowy.cloud/ws/v1' + : 'wss://beta.appflowy.cloud/ws/v1', + }, +}; + +export interface AppState { + appConfig: AFServiceConfig; +} + +const initialState: AppState = { + appConfig: defaultConfig, +}; + +export const slice = createSlice({ + name: 'app', + initialState, + reducers: { + setAppConfig: (state, action: PayloadAction) => { + state.appConfig = action.payload; + }, + }, +}); + +export const { setAppConfig } = slice.actions; \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/stores/currentUser/slice.ts b/frontend/appflowy_web_app/src/stores/currentUser/slice.ts new file mode 100644 index 0000000000..40ba1300a8 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/currentUser/slice.ts @@ -0,0 +1,52 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { UserProfile, UserSetting } from '@/application/services/user.type'; + +export enum LoginState { + IDLE = 'idle', + LOADING = 'loading', + SUCCESS = 'success', + ERROR = 'error', +} + +export interface InitialState { + user?: UserProfile; + isAuthenticated: boolean; + userSetting?: UserSetting; + loginState?: LoginState; +} + +const initialState: InitialState = { + isAuthenticated: false, +}; + +export const currentUserSlice = createSlice({ + name: 'currentUser', + initialState: initialState, + reducers: { + updateUser: (state, action: PayloadAction) => { + state.user = action.payload; + state.isAuthenticated = true; + }, + logout: (state) => { + state.user = undefined; + state.isAuthenticated = false; + }, + setUserSetting: (state, action: PayloadAction) => { + state.userSetting = action.payload; + }, + loginStart: (state) => { + state.loginState = LoginState.LOADING; + }, + loginSuccess: (state) => { + state.loginState = LoginState.SUCCESS; + }, + loginError: (state) => { + state.loginState = LoginState.ERROR; + }, + resetLoginState: (state) => { + state.loginState = LoginState.IDLE; + }, + }, +}); + +export const currentUserActions = currentUserSlice.actions; diff --git a/frontend/appflowy_web_app/src/stores/error/slice.ts b/frontend/appflowy_web_app/src/stores/error/slice.ts new file mode 100644 index 0000000000..9b47df7777 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/error/slice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface IErrorOptions { + display: boolean; + message: string; +} + +const initialState: IErrorOptions = { + display: false, + message: '', +}; + +export const errorSlice = createSlice({ + name: 'error', + initialState: initialState, + reducers: { + showError(state, action: PayloadAction) { + return { + display: true, + message: action.payload, + }; + }, + hideError() { + return { + display: false, + message: '', + }; + }, + }, +}); + +export const errorActions = errorSlice.actions; diff --git a/frontend/appflowy_web_app/src/stores/store.ts b/frontend/appflowy_web_app/src/stores/store.ts new file mode 100644 index 0000000000..b75363e911 --- /dev/null +++ b/frontend/appflowy_web_app/src/stores/store.ts @@ -0,0 +1,45 @@ +import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; +import { + configureStore, + createListenerMiddleware, + TypedStartListening, + TypedAddListener, + ListenerEffectAPI, + addListener, +} from '@reduxjs/toolkit'; +import { errorSlice } from '@/stores/error/slice'; +import { currentUserSlice } from '@/stores/currentUser/slice'; +import { slice as appSlice } from '@/stores/app/slice'; + +const listenerMiddlewareInstance = createListenerMiddleware({ + onError: () => console.error, +}); + +const store = configureStore({ + reducer: { + [appSlice.name]: appSlice.reducer, + [errorSlice.name]: errorSlice.reducer, + [currentUserSlice.name]: currentUserSlice.reducer, + }, + middleware: (gDM) => gDM({ serializableCheck: false }).prepend(listenerMiddlewareInstance.middleware), +}); + +export { store }; + +// Infer the `RootState` and `AppDispatch` types from the store itself +export type RootState = ReturnType; +// @see https://redux-toolkit.js.org/usage/usage-with-typescript#getting-the-dispatch-type +export type AppDispatch = typeof store.dispatch; + +export type AppListenerEffectAPI = ListenerEffectAPI; + +// @see https://redux-toolkit.js.org/api/createListenerMiddleware#typescript-usage +export type AppStartListening = TypedStartListening; +export type AppAddListener = TypedAddListener; + +export const startAppListening = listenerMiddlewareInstance.startListening as AppStartListening; +export const addAppListener = addListener as AppAddListener; + +// Use throughout your app instead of plain `useDispatch` and `useSelector` +export const useAppDispatch = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/frontend/appflowy_web_app/src/styles/font.css b/frontend/appflowy_web_app/src/styles/font.css new file mode 100644 index 0000000000..84f9f90d09 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/font.css @@ -0,0 +1,107 @@ +.poppins-thin { + font-family: "Poppins", sans-serif; + font-weight: 100; + font-style: normal; +} + +.poppins-extralight { + font-family: "Poppins", sans-serif; + font-weight: 200; + font-style: normal; +} + +.poppins-light { + font-family: "Poppins", sans-serif; + font-weight: 300; + font-style: normal; +} + +.poppins-regular { + font-family: "Poppins", sans-serif; + font-weight: 400; + font-style: normal; +} + +.poppins-medium { + font-family: "Poppins", sans-serif; + font-weight: 500; + font-style: normal; +} + +.poppins-semibold { + font-family: "Poppins", sans-serif; + font-weight: 600; + font-style: normal; +} + +.poppins-bold { + font-family: "Poppins", sans-serif; + font-weight: 700; + font-style: normal; +} + +.poppins-extrabold { + font-family: "Poppins", sans-serif; + font-weight: 800; + font-style: normal; +} + +.poppins-black { + font-family: "Poppins", sans-serif; + font-weight: 900; + font-style: normal; +} + +.poppins-thin-italic { + font-family: "Poppins", sans-serif; + font-weight: 100; + font-style: italic; +} + +.poppins-extralight-italic { + font-family: "Poppins", sans-serif; + font-weight: 200; + font-style: italic; +} + +.poppins-light-italic { + font-family: "Poppins", sans-serif; + font-weight: 300; + font-style: italic; +} + +.poppins-regular-italic { + font-family: "Poppins", sans-serif; + font-weight: 400; + font-style: italic; +} + +.poppins-medium-italic { + font-family: "Poppins", sans-serif; + font-weight: 500; + font-style: italic; +} + +.poppins-semibold-italic { + font-family: "Poppins", sans-serif; + font-weight: 600; + font-style: italic; +} + +.poppins-bold-italic { + font-family: "Poppins", sans-serif; + font-weight: 700; + font-style: italic; +} + +.poppins-extrabold-italic { + font-family: "Poppins", sans-serif; + font-weight: 800; + font-style: italic; +} + +.poppins-black-italic { + font-family: "Poppins", sans-serif; + font-weight: 900; + font-style: italic; +} diff --git a/frontend/appflowy_web_app/src/styles/tailwind.css b/frontend/appflowy_web_app/src/styles/tailwind.css new file mode 100644 index 0000000000..b5c61c9567 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/tailwind.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/frontend/appflowy_web_app/src/styles/template.css b/frontend/appflowy_web_app/src/styles/template.css new file mode 100644 index 0000000000..ebad0ce253 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/template.css @@ -0,0 +1,68 @@ +@import "./variables/light.variables.css"; +@import "./variables/dark.variables.css"; + + +:root { + /* resize popover shadow */ + --shadow-resize-popover: 0px 5px 5px -3px rgba(0, 0, 0, 0.2), 0px 8px 10px 1px rgba(0, 0, 0, 0.14), 0px 3px 14px 2px rgba(0, 0, 0, 0.12); +} + +* { + margin: 0; + padding: 0; +} + +/* stop body from scrolling */ +html, +body { + margin: 0; + height: 100%; + overflow: hidden; +} + +[contenteditable] { + -webkit-tap-highlight-color: transparent; +} + +input, +textarea { + outline: 0; + background: transparent; +} + +body { + font-family: Poppins, Roboto, serif; +} + +::-webkit-scrollbar { + width: 8px; +} + + +:root[data-dark-mode=true] body { + scrollbar-color: #fff var(--bg-body); +} + +body { + scrollbar-track-color: var(--bg-body); + scrollbar-shadow-color: var(--bg-body); +} + + +.btn { + @apply rounded-xl border border-line-divider px-4 py-3; +} + +.btn-primary { + @apply bg-fill-default text-text-title hover:bg-fill-list-hover; +} + +.input { + @apply rounded-xl border border-line-divider px-[18px] py-[14px] text-sm; +} + + +th { + @apply text-left font-normal; +} + diff --git a/frontend/appflowy_web_app/src/styles/variables/dark.variables.css b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css new file mode 100644 index 0000000000..b82d97e5be --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/variables/dark.variables.css @@ -0,0 +1,121 @@ +/** +* Do not edit directly +* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated from $pnpm css:variables +*/ + +:root[data-dark-mode=true] { + --base-light-neutral-50: #f9fafd; + --base-light-neutral-100: #edeef2; + --base-light-neutral-200: #e2e4eb; + --base-light-neutral-300: #f2f2f2; + --base-light-neutral-400: #e0e0e0; + --base-light-neutral-500: #bdbdbd; + --base-light-neutral-600: #828282; + --base-light-neutral-700: #4f4f4f; + --base-light-neutral-800: #333333; + --base-light-neutral-900: #1f2329; + --base-light-neutral-1000: #000000; + --base-light-neutral-00: #ffffff; + --base-light-blue-50: #f2fcff; + --base-light-blue-100: #e0f8ff; + --base-light-blue-200: #a6ecff; + --base-light-blue-300: #52d1f4; + --base-light-blue-400: #00bcf0; + --base-light-blue-500: #05ade2; + --base-light-blue-600: #009fd1; + --base-light-color-deep-red: #fb006d; + --base-light-color-deep-yellow: #ffd667; + --base-light-color-deep-green: #66cf80; + --base-light-color-deep-blue: #00bcf0; + --base-light-color-light-purple: #e8e0ff; + --base-light-color-light-pink: #ffe7ee; + --base-light-color-light-orange: #ffefe3; + --base-light-color-light-yellow: #fff2cd; + --base-light-color-light-lime: #f5ffdc; + --base-light-color-light-green: #ddffd6; + --base-light-color-light-aqua: #defff1; + --base-light-color-light-blue: #e1fbff; + --base-light-color-light-red: #ffdddd; + --base-black-neutral-100: #252F41; + --base-black-neutral-200: #313c51; + --base-black-neutral-300: #3c4557; + --base-black-neutral-400: #525A69; + --base-black-neutral-500: #59647a; + --base-black-neutral-600: #87A0BF; + --base-black-neutral-700: #99a6b8; + --base-black-neutral-800: #e2e9f2; + --base-black-neutral-900: #eff4fb; + --base-black-neutral-1000: #ffffff; + --base-black-neutral-n50: #232b38; + --base-black-neutral-n00: #1a202c; + --base-black-blue-50: #232b38; + --base-black-blue-100: #005174; + --base-black-blue-200: #a6ecff; + --base-black-blue-300: #52d1f4; + --base-black-blue-400: #00bcf0; + --base-black-blue-500: #05ade2; + --base-black-blue-600: #009fd1; + --base-black-color-deep-red: #d32772; + --base-black-color-deep-yellow: #e9b320; + --base-black-color-deep-green: #3ba856; + --base-black-color-deep-blue: #2e9dbb; + --base-black-color-light-purple: #4D4078; + --base-black-color-light-blue: #2C3B58; + --base-black-color-light-green: #3C5133; + --base-black-color-light-yellow: #695E3E; + --base-black-color-light-pink: #5E3C5E; + --base-black-color-light-red: #56363F; + --base-black-color-light-aqua: #1B3849; + --base-black-color-light-lime: #394027; + --base-black-color-light-orange: #5E3C3C; + --base-else-brand: #2c144b; + --text-title: #e2e9f2; + --text-caption: #87A0BF; + --text-placeholder: #3c4557; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #005174; + --icon-primary: #e2e9f2; + --icon-secondary: #59647a; + --icon-disabled: #525A69; + --icon-on-toolbar: white; + --line-border: #59647a; + --line-divider: #252F41; + --line-on-toolbar: #99a6b8; + --fill-default: #00bcf0; + --fill-hover: #005174; + --fill-toolbar: #0F111C; + --fill-selector: #232b38; + --fill-list-active: #3c4557; + --fill-list-hover: #005174; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #005174; + --content-on-fill: #1a202c; + --content-on-tag: #99a6b8; + --content-blue-50: #232b38; + --bg-body: #1a202c; + --bg-base: #232b38; + --bg-mask: rgba(0,0,0,0.7); + --bg-tips: #005174; + --bg-brand: #2c144b; + --function-error: #d32772; + --function-warning: #e9b320; + --function-success: #3ba856; + --function-info: #2e9dbb; + --tint-red: #56363F; + --tint-green: #3C5133; + --tint-purple: #4D4078; + --tint-blue: #2C3B58; + --tint-yellow: #695E3E; + --tint-pink: #5E3C5E; + --tint-lime: #394027; + --tint-aqua: #1B3849; + --tint-orange: #5E3C3C; + --shadow: 0px 0px 25px 0px rgba(0,0,0,0.3); + --scrollbar-track: #252F41; + --scrollbar-thumb: #3c4557; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/styles/variables/light.variables.css b/frontend/appflowy_web_app/src/styles/variables/light.variables.css new file mode 100644 index 0000000000..0477655f66 --- /dev/null +++ b/frontend/appflowy_web_app/src/styles/variables/light.variables.css @@ -0,0 +1,124 @@ +/** +* Do not edit directly +* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated from $pnpm css:variables +*/ + +:root { + --base-light-neutral-50: #f9fafd; + --base-light-neutral-100: #edeef2; + --base-light-neutral-200: #e2e4eb; + --base-light-neutral-300: #f2f2f2; + --base-light-neutral-400: #e0e0e0; + --base-light-neutral-500: #bdbdbd; + --base-light-neutral-600: #828282; + --base-light-neutral-700: #4f4f4f; + --base-light-neutral-800: #333333; + --base-light-neutral-900: #1f2329; + --base-light-neutral-1000: #000000; + --base-light-neutral-00: #ffffff; + --base-light-blue-50: #f2fcff; + --base-light-blue-100: #e0f8ff; + --base-light-blue-200: #a6ecff; + --base-light-blue-300: #52d1f4; + --base-light-blue-400: #00bcf0; + --base-light-blue-500: #05ade2; + --base-light-blue-600: #009fd1; + --base-light-color-deep-red: #fb006d; + --base-light-color-deep-yellow: #ffd667; + --base-light-color-deep-green: #66cf80; + --base-light-color-deep-blue: #00bcf0; + --base-light-color-light-purple: #e8e0ff; + --base-light-color-light-pink: #ffe7ee; + --base-light-color-light-orange: #ffefe3; + --base-light-color-light-yellow: #fff2cd; + --base-light-color-light-lime: #f5ffdc; + --base-light-color-light-green: #ddffd6; + --base-light-color-light-aqua: #defff1; + --base-light-color-light-blue: #e1fbff; + --base-light-color-light-red: #ffdddd; + --base-black-neutral-100: #252F41; + --base-black-neutral-200: #313c51; + --base-black-neutral-300: #3c4557; + --base-black-neutral-400: #525A69; + --base-black-neutral-500: #59647a; + --base-black-neutral-600: #87A0BF; + --base-black-neutral-700: #99a6b8; + --base-black-neutral-800: #e2e9f2; + --base-black-neutral-900: #eff4fb; + --base-black-neutral-1000: #ffffff; + --base-black-neutral-n50: #232b38; + --base-black-neutral-n00: #1a202c; + --base-black-blue-50: #232b38; + --base-black-blue-100: #005174; + --base-black-blue-200: #a6ecff; + --base-black-blue-300: #52d1f4; + --base-black-blue-400: #00bcf0; + --base-black-blue-500: #05ade2; + --base-black-blue-600: #009fd1; + --base-black-color-deep-red: #d32772; + --base-black-color-deep-yellow: #e9b320; + --base-black-color-deep-green: #3ba856; + --base-black-color-deep-blue: #2e9dbb; + --base-black-color-light-purple: #4D4078; + --base-black-color-light-blue: #2C3B58; + --base-black-color-light-green: #3C5133; + --base-black-color-light-yellow: #695E3E; + --base-black-color-light-pink: #5E3C5E; + --base-black-color-light-red: #56363F; + --base-black-color-light-aqua: #1B3849; + --base-black-color-light-lime: #394027; + --base-black-color-light-orange: #5E3C3C; + --base-else-brand: #2c144b; + --text-title: #333333; + --text-caption: #828282; + --text-placeholder: #bdbdbd; + --text-disabled: #e0e0e0; + --text-link-default: #00bcf0; + --text-link-hover: #52d1f4; + --text-link-pressed: #009fd1; + --text-link-disabled: #e0f8ff; + --icon-primary: #333333; + --icon-secondary: #59647a; + --icon-disabled: #e0e0e0; + --icon-on-toolbar: #ffffff; + --line-border: #bdbdbd; + --line-divider: #edeef2; + --line-on-toolbar: #4f4f4f; + --fill-toolbar: #333333; + --fill-default: #00bcf0; + --fill-hover: #52d1f4; + --fill-pressed: #009fd1; + --fill-active: #e0f8ff; + --fill-list-hover: #e0f8ff; + --fill-list-active: #edeef2; + --content-blue-400: #00bcf0; + --content-blue-300: #52d1f4; + --content-blue-600: #009fd1; + --content-blue-100: #e0f8ff; + --content-blue-50: #f2fcff; + --content-on-fill-hover: #00bcf0; + --content-on-fill: #ffffff; + --content-on-tag: #4f4f4f; + --bg-body: #ffffff; + --bg-base: #f9fafd; + --bg-mask: rgba(0,0,0,0.55); + --bg-tips: #e0f8ff; + --bg-brand: #2c144b; + --function-error: #fb006d; + --function-waring: #ffd667; + --function-success: #66cf80; + --function-info: #00bcf0; + --tint-purple: #e8e0ff; + --tint-pink: #ffe7ee; + --tint-red: #ffdddd; + --tint-lime: #f5ffdc; + --tint-green: #ddffd6; + --tint-aqua: #defff1; + --tint-blue: #e1fbff; + --tint-orange: #ffefe3; + --tint-yellow: #fff2cd; + --shadow: 0px 0px 10px 0px rgba(0,0,0,0.1); + --scrollbar-thumb: #bdbdbd; + --scrollbar-track: #edeef2; +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/src/utils/platform.ts b/frontend/appflowy_web_app/src/utils/platform.ts new file mode 100644 index 0000000000..2196237aff --- /dev/null +++ b/frontend/appflowy_web_app/src/utils/platform.ts @@ -0,0 +1,5 @@ +export function getPlatform() { + return { + isTauri: import.meta.env.TAURI_MODE, + }; +} diff --git a/frontend/appflowy_web_app/src/vite-env.d.ts b/frontend/appflowy_web_app/src/vite-env.d.ts new file mode 100644 index 0000000000..561122aa17 --- /dev/null +++ b/frontend/appflowy_web_app/src/vite-env.d.ts @@ -0,0 +1,3 @@ +/// +/// +/// diff --git a/frontend/appflowy_web_app/style-dictionary/config.cjs b/frontend/appflowy_web_app/style-dictionary/config.cjs new file mode 100644 index 0000000000..10d7084060 --- /dev/null +++ b/frontend/appflowy_web_app/style-dictionary/config.cjs @@ -0,0 +1,114 @@ +const StyleDictionary = require('style-dictionary'); +const fs = require('fs'); +const path = require('path'); + +// Add comment header to generated files +StyleDictionary.registerFormat({ + name: 'css/variables', + formatter: function(dictionary, config) { + const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; + const allProperties = dictionary.allProperties; + const properties = allProperties.map(prop => { + const { name, value } = prop; + return ` --${name}: ${value};` + }).join('\n'); + // generate tailwind config + generateTailwindConfig(allProperties); + return header + `:root${this.selector} {\n${properties}\n}` + } +}); + +// expand shadow tokens into a single string +StyleDictionary.registerTransform({ + name: 'shadow/spreadShadow', + type: 'value', + matcher: function (prop) { + return prop.type === 'boxShadow'; + }, + transformer: function (prop) { + // destructure shadow values from original token value + const { x, y, blur, spread, color } = prop.original.value; + + return `${x}px ${y}px ${blur}px ${spread}px ${color}`; + }, +}); + +const transforms = ['attribute/cti', 'name/cti/kebab', 'shadow/spreadShadow']; + +// Generate Light CSS variables +StyleDictionary.extend({ + source: ['./style-dictionary/tokens/base.json', './style-dictionary/tokens/light.json'], + platforms: { + css: { + transformGroup: 'css', + buildPath: './src/styles/variables/', + files: [ + { + format: 'css/variables', + destination: 'light.variables.css', + selector: '', + options: { + outputReferences: true + } + }, + ], + transforms, + }, + }, +}).buildAllPlatforms(); + +// Generate Dark CSS variables +StyleDictionary.extend({ + source: ['./style-dictionary/tokens/base.json', './style-dictionary/tokens/dark.json'], + platforms: { + css: { + transformGroup: 'css', + buildPath: './src/styles/variables/', + files: [ + { + format: 'css/variables', + destination: 'dark.variables.css', + selector: '[data-dark-mode=true]', + }, + ], + transforms, + }, + }, +}).buildAllPlatforms(); + + +function set(obj, path, value) { + const lastKey = path.pop(); + const lastObj = path.reduce((obj, key) => + obj[key] = obj[key] || {}, + obj); + lastObj[lastKey] = value; +} + +function writeFile (file, data) { + const header = `/**\n` + '* Do not edit directly\n' + `* Generated on ${new Date().toUTCString()}\n` + `* Generated from $pnpm css:variables \n` + `*/\n\n`; + const exportString = `module.exports = ${JSON.stringify(data, null, 2)}`; + fs.writeFileSync(path.join(__dirname, file), header + exportString); +} + +function generateTailwindConfig(allProperties) { + const tailwindColors = {}; + const tailwindBoxShadow = {}; + allProperties.forEach(prop => { + const { path, type, name, value } = prop; + if (path[0] === 'Base') { + return; + } + if (type === 'color') { + if (name.includes('fill')) { + console.log(prop); + } + set(tailwindColors, path, `var(--${name})`); + } + if (type === 'boxShadow') { + set(tailwindBoxShadow, ['md'], `var(--${name})`); + } + }); + writeFile('./tailwind/colors.cjs', tailwindColors); + writeFile('./tailwind/box-shadow.cjs', tailwindBoxShadow); +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs new file mode 100644 index 0000000000..00647333e2 --- /dev/null +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/box-shadow.cjs @@ -0,0 +1,9 @@ +/** +* Do not edit directly +* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated from $pnpm css:variables +*/ + +module.exports = { + "md": "var(--shadow)" +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs new file mode 100644 index 0000000000..798741f06c --- /dev/null +++ b/frontend/appflowy_web_app/style-dictionary/tailwind/colors.cjs @@ -0,0 +1,75 @@ +/** +* Do not edit directly +* Generated on Mon, 25 Mar 2024 05:19:13 GMT +* Generated from $pnpm css:variables +*/ + +module.exports = { + "text": { + "title": "var(--text-title)", + "caption": "var(--text-caption)", + "placeholder": "var(--text-placeholder)", + "link-default": "var(--text-link-default)", + "link-hover": "var(--text-link-hover)", + "link-pressed": "var(--text-link-pressed)", + "link-disabled": "var(--text-link-disabled)" + }, + "icon": { + "primary": "var(--icon-primary)", + "secondary": "var(--icon-secondary)", + "disabled": "var(--icon-disabled)", + "on-toolbar": "var(--icon-on-toolbar)" + }, + "line": { + "border": "var(--line-border)", + "divider": "var(--line-divider)", + "on-toolbar": "var(--line-on-toolbar)" + }, + "fill": { + "default": "var(--fill-default)", + "hover": "var(--fill-hover)", + "toolbar": "var(--fill-toolbar)", + "selector": "var(--fill-selector)", + "list": { + "active": "var(--fill-list-active)", + "hover": "var(--fill-list-hover)" + } + }, + "content": { + "blue-400": "var(--content-blue-400)", + "blue-300": "var(--content-blue-300)", + "blue-600": "var(--content-blue-600)", + "blue-100": "var(--content-blue-100)", + "on-fill": "var(--content-on-fill)", + "on-tag": "var(--content-on-tag)", + "blue-50": "var(--content-blue-50)" + }, + "bg": { + "body": "var(--bg-body)", + "base": "var(--bg-base)", + "mask": "var(--bg-mask)", + "tips": "var(--bg-tips)", + "brand": "var(--bg-brand)" + }, + "function": { + "error": "var(--function-error)", + "warning": "var(--function-warning)", + "success": "var(--function-success)", + "info": "var(--function-info)" + }, + "tint": { + "red": "var(--tint-red)", + "green": "var(--tint-green)", + "purple": "var(--tint-purple)", + "blue": "var(--tint-blue)", + "yellow": "var(--tint-yellow)", + "pink": "var(--tint-pink)", + "lime": "var(--tint-lime)", + "aqua": "var(--tint-aqua)", + "orange": "var(--tint-orange)" + }, + "scrollbar": { + "track": "var(--scrollbar-track)", + "thumb": "var(--scrollbar-thumb)" + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/base.json b/frontend/appflowy_web_app/style-dictionary/tokens/base.json new file mode 100644 index 0000000000..4e31b0523d --- /dev/null +++ b/frontend/appflowy_web_app/style-dictionary/tokens/base.json @@ -0,0 +1,290 @@ +{ + "Base": { + "Light": { + "neutral": { + "50": { + "value": "#f9fafd", + "type": "color" + }, + "100": { + "value": "#edeef2", + "type": "color" + }, + "200": { + "value": "#e2e4eb", + "type": "color" + }, + "300": { + "value": "#f2f2f2", + "type": "color" + }, + "400": { + "value": "#e0e0e0", + "type": "color" + }, + "500": { + "value": "#bdbdbd", + "type": "color" + }, + "600": { + "value": "#828282", + "type": "color" + }, + "700": { + "value": "#4f4f4f", + "type": "color" + }, + "800": { + "value": "#333333", + "type": "color" + }, + "900": { + "value": "#1f2329", + "type": "color" + }, + "1000": { + "value": "#000000", + "type": "color" + }, + "00": { + "value": "#ffffff", + "type": "color" + } + }, + "blue": { + "50": { + "value": "#f2fcff", + "type": "color" + }, + "100": { + "value": "#e0f8ff", + "type": "color" + }, + "200": { + "value": "#a6ecff", + "type": "color" + }, + "300": { + "value": "#52d1f4", + "type": "color" + }, + "400": { + "value": "#00bcf0", + "type": "color" + }, + "500": { + "value": "#05ade2", + "type": "color" + }, + "600": { + "value": "#009fd1", + "type": "color" + } + }, + "color": { + "deep": { + "red": { + "value": "#fb006d", + "type": "color" + }, + "yellow": { + "value": "#ffd667", + "type": "color" + }, + "green": { + "value": "#66cf80", + "type": "color" + }, + "blue": { + "value": "#00bcf0", + "type": "color" + } + }, + "light": { + "purple": { + "value": "#e8e0ff", + "type": "color" + }, + "pink": { + "value": "#ffe7ee", + "type": "color" + }, + "orange": { + "value": "#ffefe3", + "type": "color" + }, + "yellow": { + "value": "#fff2cd", + "type": "color" + }, + "lime": { + "value": "#f5ffdc", + "type": "color" + }, + "green": { + "value": "#ddffd6", + "type": "color" + }, + "aqua": { + "value": "#defff1", + "type": "color" + }, + "blue": { + "value": "#e1fbff", + "type": "color" + }, + "red": { + "value": "#ffdddd", + "type": "color" + } + } + } + }, + "black": { + "neutral": { + "100": { + "value": "#252F41", + "type": "color" + }, + "200": { + "value": "#313c51", + "type": "color" + }, + "300": { + "value": "#3c4557", + "type": "color" + }, + "400": { + "value": "#525A69", + "type": "color" + }, + "500": { + "value": "#59647a", + "type": "color" + }, + "600": { + "value": "#87A0BF", + "type": "color" + }, + "700": { + "value": "#99a6b8", + "type": "color" + }, + "800": { + "value": "#e2e9f2", + "type": "color" + }, + "900": { + "value": "#eff4fb", + "type": "color" + }, + "1000": { + "value": "#ffffff", + "type": "color" + }, + "N50": { + "value": "#232b38", + "type": "color" + }, + "N00": { + "value": "#1a202c", + "type": "color" + } + }, + "blue": { + "50": { + "value": "#232b38", + "type": "color" + }, + "100": { + "value": "#005174", + "type": "color" + }, + "200": { + "value": "#a6ecff", + "type": "color" + }, + "300": { + "value": "#52d1f4", + "type": "color" + }, + "400": { + "value": "#00bcf0", + "type": "color" + }, + "500": { + "value": "#05ade2", + "type": "color" + }, + "600": { + "value": "#009fd1", + "type": "color" + } + }, + "color": { + "deep": { + "red": { + "value": "#d32772", + "type": "color" + }, + "yellow": { + "value": "#e9b320", + "type": "color" + }, + "green": { + "value": "#3ba856", + "type": "color" + }, + "blue": { + "value": "#2e9dbb", + "type": "color" + } + }, + "light": { + "purple": { + "value": "#4D4078", + "type": "color" + }, + "blue": { + "value": "#2C3B58", + "type": "color" + }, + "green": { + "value": "#3C5133", + "type": "color" + }, + "yellow": { + "value": "#695E3E", + "type": "color" + }, + "pink": { + "value": "#5E3C5E", + "type": "color" + }, + "red": { + "value": "#56363F", + "type": "color" + }, + "aqua": { + "value": "#1B3849", + "type": "color" + }, + "lime": { + "value": "#394027", + "type": "color" + }, + "orange": { + "value": "#5E3C3C", + "type": "color" + } + } + } + }, + "else": { + "brand": { + "value": "#2c144b", + "type": "color" + } + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/dark.json b/frontend/appflowy_web_app/style-dictionary/tokens/dark.json new file mode 100644 index 0000000000..c67af7c9ec --- /dev/null +++ b/frontend/appflowy_web_app/style-dictionary/tokens/dark.json @@ -0,0 +1,221 @@ +{ + "text": { + "title": { + "value": "{Base.black.neutral.800}", + "type": "color" + }, + "caption": { + "value": "{Base.black.neutral.600}", + "type": "color" + }, + "placeholder": { + "value": "{Base.black.neutral.300}", + "type": "color" + }, + "link-default": { + "value": "{Base.black.blue.400}", + "type": "color" + }, + "link-hover": { + "value": "{Base.black.blue.300}", + "type": "color" + }, + "link-pressed": { + "value": "{Base.black.blue.600}", + "type": "color" + }, + "link-disabled": { + "value": "{Base.black.blue.100}", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "{Base.black.neutral.800}", + "type": "color" + }, + "secondary": { + "value": "{Base.black.neutral.500}", + "type": "color" + }, + "disabled": { + "value": "{Base.black.neutral.400}", + "type": "color" + }, + "on-toolbar": { + "value": "white", + "type": "color" + } + }, + "line": { + "border": { + "value": "{Base.black.neutral.500}", + "type": "color" + }, + "divider": { + "value": "{Base.black.neutral.100}", + "type": "color" + }, + "on-toolbar": { + "value": "{Base.black.neutral.700}", + "type": "color" + } + }, + "fill": { + "default": { + "value": "{Base.black.blue.400}", + "type": "color" + }, + "hover": { + "value": "{Base.black.blue.100}", + "type": "color" + }, + "toolbar": { + "value": "#0F111C", + "type": "color" + }, + "selector": { + "value": "{Base.black.blue.50}", + "type": "color" + }, + "list": { + "active": { + "value": "{Base.black.neutral.300}", + "type": "color" + }, + "hover": { + "value": "{Base.black.blue.100}", + "type": "color" + } + } + }, + "content": { + "blue-400": { + "value": "{Base.black.blue.400}", + "type": "color" + }, + "blue-300": { + "value": "{Base.black.blue.300}", + "type": "color" + }, + "blue-600": { + "value": "{Base.black.blue.600}", + "type": "color" + }, + "blue-100": { + "value": "{Base.black.blue.100}", + "type": "color" + }, + "on-fill": { + "value": "{Base.black.neutral.N00}", + "type": "color" + }, + "on-tag": { + "value": "{Base.black.neutral.700}", + "type": "color" + }, + "blue-50": { + "value": "{Base.black.blue.50}", + "type": "color" + } + }, + "bg": { + "body": { + "value": "{Base.black.neutral.N00}", + "type": "color" + }, + "base": { + "value": "{Base.black.blue.50}", + "type": "color" + }, + "mask": { + "value": "rgba(0,0,0,0.7)", + "type": "color" + }, + "tips": { + "value": "{Base.black.blue.100}", + "type": "color" + }, + "brand": { + "value": "{Base.else.brand}", + "type": "color" + } + }, + "function": { + "error": { + "value": "{Base.black.color.deep.red}", + "type": "color" + }, + "warning": { + "value": "{Base.black.color.deep.yellow}", + "type": "color" + }, + "success": { + "value": "#3ba856", + "type": "color" + }, + "info": { + "value": "#2e9dbb", + "type": "color" + } + }, + "tint": { + "red": { + "value": "{Base.black.color.light.red}", + "type": "color" + }, + "green": { + "value": "{Base.black.color.light.green}", + "type": "color" + }, + "purple": { + "value": "{Base.black.color.light.purple}", + "type": "color" + }, + "blue": { + "value": "{Base.black.color.light.blue}", + "type": "color" + }, + "yellow": { + "value": "{Base.black.color.light.yellow}", + "type": "color" + }, + "pink": { + "value": "{Base.black.color.light.pink}", + "type": "color" + }, + "lime": { + "value": "{Base.black.color.light.lime}", + "type": "color" + }, + "aqua": { + "value": "{Base.black.color.light.aqua}", + "type": "color" + }, + "orange": { + "value": "{Base.black.color.light.orange}", + "type": "color" + } + }, + "shadow": { + "value": { + "x": "0", + "y": "0", + "blur": "25", + "spread": "0", + "color": "rgba(0,0,0,0.3)", + "type": "innerShadow" + }, + "type": "boxShadow" + }, + "scrollbar": { + "track": { + "value": "{Base.black.neutral.100}", + "type": "color" + }, + "thumb": { + "value": "{Base.black.neutral.300}", + "type": "color" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/style-dictionary/tokens/light.json b/frontend/appflowy_web_app/style-dictionary/tokens/light.json new file mode 100644 index 0000000000..173f3d35aa --- /dev/null +++ b/frontend/appflowy_web_app/style-dictionary/tokens/light.json @@ -0,0 +1,233 @@ +{ + "text": { + "title": { + "value": "{Base.Light.neutral.800}", + "type": "color" + }, + "caption": { + "value": "{Base.Light.neutral.600}", + "type": "color" + }, + "placeholder": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "disabled": { + "value": "{Base.Light.neutral.400}", + "type": "color" + }, + "link-default": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "link-hover": { + "value": "{Base.Light.blue.300}", + "type": "color" + }, + "link-pressed": { + "value": "{Base.Light.blue.600}", + "type": "color" + }, + "link-disabled": { + "value": "{Base.Light.blue.100}", + "type": "color" + } + }, + "icon": { + "primary": { + "value": "{Base.Light.neutral.800}", + "type": "color" + }, + "secondary": { + "value": "{Base.black.neutral.500}", + "type": "color" + }, + "disabled": { + "value": "{Base.Light.neutral.400}", + "type": "color" + }, + "on-toolbar": { + "value": "{Base.Light.neutral.00}", + "type": "color" + } + }, + "line": { + "border": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "divider": { + "value": "{Base.Light.neutral.100}", + "type": "color" + }, + "on-toolbar": { + "value": "{Base.Light.neutral.700}", + "type": "color" + } + }, + "fill": { + "toolbar": { + "value": "{Base.Light.neutral.800}", + "type": "color" + }, + "default": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "hover": { + "value": "{Base.Light.blue.300}", + "type": "color" + }, + "pressed": { + "value": "{Base.Light.blue.600}", + "type": "color" + }, + "active": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "list": { + "hover": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "active": { + "value": "{Base.Light.neutral.100}", + "type": "color" + } + } + }, + "content": { + "blue-400": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "blue-300": { + "value": "{Base.Light.blue.300}", + "type": "color" + }, + "blue-600": { + "value": "{Base.Light.blue.600}", + "type": "color" + }, + "blue-100": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "blue-50": { + "value": "{Base.Light.blue.50}", + "type": "color" + }, + "on-fill-hover": { + "value": "{Base.Light.blue.400}", + "type": "color" + }, + "on-fill": { + "value": "{Base.Light.neutral.00}", + "type": "color" + }, + "on-tag": { + "value": "{Base.Light.neutral.700}", + "type": "color" + } + }, + "bg": { + "body": { + "value": "{Base.Light.neutral.00}", + "type": "color" + }, + "base": { + "value": "{Base.Light.neutral.50}", + "type": "color" + }, + "mask": { + "value": "rgba(0,0,0,0.55)", + "type": "color" + }, + "tips": { + "value": "{Base.Light.blue.100}", + "type": "color" + }, + "brand": { + "value": "{Base.else.brand}", + "type": "color" + } + }, + "function": { + "error": { + "value": "{Base.Light.color.deep.red}", + "type": "color" + }, + "waring": { + "value": "{Base.Light.color.deep.yellow}", + "type": "color" + }, + "success": { + "value": "{Base.Light.color.deep.green}", + "type": "color" + }, + "info": { + "value": "{Base.Light.color.deep.blue}", + "type": "color" + } + }, + "tint": { + "purple": { + "value": "{Base.Light.color.light.purple}", + "type": "color" + }, + "pink": { + "value": "{Base.Light.color.light.pink}", + "type": "color" + }, + "red": { + "value": "{Base.Light.color.light.red}", + "type": "color" + }, + "lime": { + "value": "{Base.Light.color.light.lime}", + "type": "color" + }, + "green": { + "value": "{Base.Light.color.light.green}", + "type": "color" + }, + "aqua": { + "value": "{Base.Light.color.light.aqua}", + "type": "color" + }, + "blue": { + "value": "{Base.Light.color.light.blue}", + "type": "color" + }, + "orange": { + "value": "{Base.Light.color.light.orange}", + "type": "color" + }, + "yellow": { + "value": "{Base.Light.color.light.yellow}", + "type": "color" + } + }, + "shadow": { + "value": { + "x": "0", + "y": "0", + "blur": "10", + "spread": "0", + "color": "rgba(0,0,0,0.1)", + "type": "dropShadow" + }, + "type": "boxShadow" + }, + "scrollbar": { + "thumb": { + "value": "{Base.Light.neutral.500}", + "type": "color" + }, + "track": { + "value": "{Base.Light.neutral.100}", + "type": "color" + } + } +} \ No newline at end of file diff --git a/frontend/appflowy_web_app/tailwind.config.cjs b/frontend/appflowy_web_app/tailwind.config.cjs new file mode 100644 index 0000000000..06390d938f --- /dev/null +++ b/frontend/appflowy_web_app/tailwind.config.cjs @@ -0,0 +1,20 @@ +const colors = require('./style-dictionary/tailwind/colors.cjs'); +const boxShadow = require('./style-dictionary/tailwind/box-shadow.cjs'); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [ + './index.html', + './src/**/*.{js,ts,jsx,tsx}', + './node_modules/react-tailwindcss-datepicker/dist/index.esm.js', + ], + important: '#body', + darkMode: 'class', + theme: { + extend: { + colors, + boxShadow, + }, + }, + plugins: [], +}; diff --git a/frontend/appflowy_web_app/tsconfig.json b/frontend/appflowy_web_app/tsconfig.json new file mode 100644 index 0000000000..39e1d62e66 --- /dev/null +++ b/frontend/appflowy_web_app/tsconfig.json @@ -0,0 +1,51 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": [ + "DOM", + "DOM.Iterable", + "ESNext" + ], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "types": [ + "node", + "jest" + ], + "baseUrl": "./", + "paths": { + "@/*": [ + "src/*" + ], + "src/*": [ + "src/*" + ], + "$client-services": [ + "src/application/services/js-services" + ] + } + }, + "include": [ + "src", + "vite.config.ts" + ], + "exclude": [ + "node_modules" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/appflowy_web_app/tsconfig.node.json b/frontend/appflowy_web_app/tsconfig.node.json new file mode 100644 index 0000000000..b8afcc8fa2 --- /dev/null +++ b/frontend/appflowy_web_app/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "allowSyntheticDefaultImports": true + }, + "include": [ + "vite.config.ts" + ] +} diff --git a/frontend/appflowy_web_app/tsconfig.web.json b/frontend/appflowy_web_app/tsconfig.web.json new file mode 100644 index 0000000000..f6c24c1512 --- /dev/null +++ b/frontend/appflowy_web_app/tsconfig.web.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "exclude": [ + "node_modules", + "src/application/services/tauri-services" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/frontend/appflowy_web_app/vite.config.ts b/frontend/appflowy_web_app/vite.config.ts new file mode 100644 index 0000000000..13971b0709 --- /dev/null +++ b/frontend/appflowy_web_app/vite.config.ts @@ -0,0 +1,85 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import svgr from 'vite-plugin-svgr'; +import wasm from 'vite-plugin-wasm'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react(), + wasm(), + svgr({ + svgrOptions: { + prettier: false, + plugins: ['@svgr/plugin-svgo', '@svgr/plugin-jsx'], + icon: true, + svgoConfig: { + multipass: true, + plugins: [ + { + name: 'preset-default', + params: { + overrides: { + removeViewBox: false, + }, + }, + }, + ], + }, + svgProps: { + role: 'img', + }, + replaceAttrValues: { + '#333': 'currentColor', + }, + }, + }), + ], + // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` + // prevent vite from obscuring rust errors + clearScreen: false, + // tauri expects a fixed port, fail if that port is not available + server: { + port: process.env.TAURI_MODE ? 5173 : process.env.PORT ? parseInt(process.env.PORT) : 3000, + strictPort: true, + watch: { + ignored: ['**/__tests__/**'], + }, + // proxy: { + // '/api': { + // target: 'https://test.appflowy.cloud', + // changeOrigin: true, + // secure: false, + // }, + // }, + }, + envPrefix: ['AF', 'TAURI_'], + build: process.env.TAURI_MODE + ? { + // Tauri supports es2021 + target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13', + // don't minify for debug builds + minify: !process.env.TAURI_DEBUG ? 'esbuild' : false, + // produce sourcemaps for debug builds + sourcemap: !!process.env.TAURI_DEBUG, + } + : { + target: `esnext`, + }, + resolve: { + alias: [ + { find: 'src/', replacement: `${__dirname}/src/` }, + { find: '@/', replacement: `${__dirname}/src/` }, + { + find: '$client-services', + replacement: process.env.TAURI_MODE + ? `${__dirname}/src/application/services/tauri-services` + : `${__dirname}/src/application/services/js-services`, + }, + ], + }, + + optimizeDeps: { + include: ['@mui/material/Tooltip'], + }, +}); diff --git a/frontend/resources/flowy_icons/16x/m_aa_indent.svg b/frontend/resources/flowy_icons/16x/m_aa_indent.svg index 42dbcd6051..43d1d43786 100644 --- a/frontend/resources/flowy_icons/16x/m_aa_indent.svg +++ b/frontend/resources/flowy_icons/16x/m_aa_indent.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_aa_outdent.svg b/frontend/resources/flowy_icons/16x/m_aa_outdent.svg index 43d1d43786..42dbcd6051 100644 --- a/frontend/resources/flowy_icons/16x/m_aa_outdent.svg +++ b/frontend/resources/flowy_icons/16x/m_aa_outdent.svg @@ -1,6 +1,6 @@ - - - - - + + + + + diff --git a/frontend/resources/flowy_icons/16x/m_bottom_sheet_close.svg b/frontend/resources/flowy_icons/16x/m_bottom_sheet_close.svg new file mode 100644 index 0000000000..9be5ee420e --- /dev/null +++ b/frontend/resources/flowy_icons/16x/m_bottom_sheet_close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_align_center.svg b/frontend/resources/flowy_icons/24x/m_aa_align_center.svg new file mode 100644 index 0000000000..1a287877df --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_align_center.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_align_left.svg b/frontend/resources/flowy_icons/24x/m_aa_align_left.svg new file mode 100644 index 0000000000..8b26e2bddf --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_align_left.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_align_right.svg b/frontend/resources/flowy_icons/24x/m_aa_align_right.svg new file mode 100644 index 0000000000..54f91608b6 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_align_right.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_code.svg b/frontend/resources/flowy_icons/24x/m_aa_code.svg new file mode 100644 index 0000000000..5e7ee42d4c --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_font_color.svg b/frontend/resources/flowy_icons/24x/m_aa_font_color.svg new file mode 100644 index 0000000000..919aa91c59 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_font_color.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_h1.svg b/frontend/resources/flowy_icons/24x/m_aa_h1.svg new file mode 100644 index 0000000000..478192490c --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_h1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_h2.svg b/frontend/resources/flowy_icons/24x/m_aa_h2.svg new file mode 100644 index 0000000000..49b99983f5 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_h2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_h3.svg b/frontend/resources/flowy_icons/24x/m_aa_h3.svg new file mode 100644 index 0000000000..0d1a57cd8f --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_h3.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_indent.svg b/frontend/resources/flowy_icons/24x/m_aa_indent.svg new file mode 100644 index 0000000000..6dd3e72a16 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_indent.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_math.svg b/frontend/resources/flowy_icons/24x/m_aa_math.svg new file mode 100644 index 0000000000..0590a34a9d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_math.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_outdent.svg b/frontend/resources/flowy_icons/24x/m_aa_outdent.svg new file mode 100644 index 0000000000..2194ecc259 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_outdent.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_paragraph.svg b/frontend/resources/flowy_icons/24x/m_aa_paragraph.svg new file mode 100644 index 0000000000..5b04dce7f1 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_paragraph.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_aa_quote.svg b/frontend/resources/flowy_icons/24x/m_aa_quote.svg new file mode 100644 index 0000000000..01a92b4f99 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_aa_quote.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_bottom_sheet_close.svg b/frontend/resources/flowy_icons/24x/m_bottom_sheet_close.svg new file mode 100644 index 0000000000..9be5ee420e --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_bottom_sheet_close.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_aa.svg b/frontend/resources/flowy_icons/24x/m_toolbar_aa.svg new file mode 100644 index 0000000000..6fb13d985a --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_aa.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_add.svg b/frontend/resources/flowy_icons/24x/m_toolbar_add.svg new file mode 100644 index 0000000000..651c3d1638 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_add.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_bold.svg b/frontend/resources/flowy_icons/24x/m_toolbar_bold.svg new file mode 100644 index 0000000000..a3302284a5 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_bold.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_bulleted_list.svg b/frontend/resources/flowy_icons/24x/m_toolbar_bulleted_list.svg new file mode 100644 index 0000000000..46dbd0f2fd --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_bulleted_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_calendar.svg b/frontend/resources/flowy_icons/24x/m_toolbar_calendar.svg new file mode 100644 index 0000000000..43a60cfe08 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_calendar.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_checkbox.svg b/frontend/resources/flowy_icons/24x/m_toolbar_checkbox.svg new file mode 100644 index 0000000000..a84bbc94b0 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_checkbox.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_image.svg b/frontend/resources/flowy_icons/24x/m_toolbar_image.svg new file mode 100644 index 0000000000..f3fc20769e --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_italic.svg b/frontend/resources/flowy_icons/24x/m_toolbar_italic.svg new file mode 100644 index 0000000000..7543a1eceb --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_italic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_keyboard.svg b/frontend/resources/flowy_icons/24x/m_toolbar_keyboard.svg new file mode 100644 index 0000000000..42c7a390b7 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_keyboard.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_link.svg b/frontend/resources/flowy_icons/24x/m_toolbar_link.svg new file mode 100644 index 0000000000..7ee84011b9 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_link.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_numbered_list.svg b/frontend/resources/flowy_icons/24x/m_toolbar_numbered_list.svg new file mode 100644 index 0000000000..787a05fa0d --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_numbered_list.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_redo.svg b/frontend/resources/flowy_icons/24x/m_toolbar_redo.svg new file mode 100644 index 0000000000..3b521ae091 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_redo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_strike.svg b/frontend/resources/flowy_icons/24x/m_toolbar_strike.svg new file mode 100644 index 0000000000..209ea728c7 --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_strike.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_underline.svg b/frontend/resources/flowy_icons/24x/m_toolbar_underline.svg new file mode 100644 index 0000000000..f96282ca4f --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_underline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/resources/flowy_icons/24x/m_toolbar_undo.svg b/frontend/resources/flowy_icons/24x/m_toolbar_undo.svg new file mode 100644 index 0000000000..617dac39fe --- /dev/null +++ b/frontend/resources/flowy_icons/24x/m_toolbar_undo.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/resources/translations/am-ET.json b/frontend/resources/translations/am-ET.json index f5f11d7495..b07649f0e4 100644 --- a/frontend/resources/translations/am-ET.json +++ b/frontend/resources/translations/am-ET.json @@ -422,13 +422,9 @@ "isComplete": "አይደለም", "isIncomplted": "ባዶ ነው" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "ነው", "isNot": "አይደለም", - "isEmpty": "ባዶ ነው", - "isNotEmpty": "ባዶ አይደለም" - }, - "multiSelectOptionFilter": { "contains": "ይይዛል", "doesNotContain": "አይይዝም", "isEmpty": "ባዶ ነው", diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index fae5ea6aa9..25021d704c 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -178,9 +178,7 @@ "dragRow": "اضغط مطولاً لإعادة ترتيب الصف", "viewDataBase": "عرض قاعدة البيانات", "referencePage": "تمت الإشارة إلى هذا {name}", - "addBlockBelow": "إضافة كتلة أدناه", - "urlLaunchAccessory": "فتح في المتصفح", - "urlCopyAccessory": "إنسخ الرابط" + "addBlockBelow": "إضافة كتلة أدناه" }, "sideBar": { "closeSidebar": "إغلاق الشريط الجانبي", @@ -233,6 +231,7 @@ "helpCenter": "مركز المساعدة", "add": "اضافة", "yes": "نعم", + "back": "خلف", "tryAGain": "حاول ثانية" }, "label": { @@ -488,13 +487,9 @@ "isComplete": "كاملة", "isIncomplted": "غير مكتمل" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "يكون", "isNot": "ليس", - "isEmpty": "فارغ", - "isNotEmpty": "ليس فارغا" - }, - "multiSelectOptionFilter": { "contains": "يتضمن", "doesNotContain": "لا يحتوي", "isEmpty": "فارغ", @@ -625,6 +620,10 @@ "hideComplete": "إخفاء المهام المكتملة", "showComplete": "إظهار كافة المهام" }, + "url": { + "launch": "فتح في المتصفح", + "copy": "إنسخ الرابط" + }, "menuName": "شبكة", "referencedGridPrefix": "نظرا ل" }, diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index ef312e3735..799e9d68c0 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -176,9 +176,7 @@ "dragRow": "Premeu llargament per reordenar la fila", "viewDataBase": "Veure base de dades", "referencePage": "Es fa referència a aquest {nom}", - "addBlockBelow": "Afegeix un bloc a continuació", - "urlLaunchAccessory": "Oberta al navegador", - "urlCopyAccessory": "Copia l'URL" + "addBlockBelow": "Afegeix un bloc a continuació" }, "sideBar": { "closeSidebar": "Close sidebar", @@ -461,13 +459,9 @@ "isComplete": "està completa", "isIncomplted": "és incompleta" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "És", "isNot": "No és", - "isEmpty": "Està buit", - "isNotEmpty": "No està buit" - }, - "multiSelectOptionFilter": { "contains": "Conté", "doesNotContain": "No conté", "isEmpty": "Està buit", @@ -573,6 +567,10 @@ "addNew": "Afegeix un element", "submitNewTask": "Crear" }, + "url": { + "launch": "Oberta al navegador", + "copy": "Copia l'URL" + }, "menuName": "Quadrícula", "referencedGridPrefix": "Vista de" }, diff --git a/frontend/resources/translations/ckb-KU.json b/frontend/resources/translations/ckb-KU.json index f5509b5ea1..f88a020209 100644 --- a/frontend/resources/translations/ckb-KU.json +++ b/frontend/resources/translations/ckb-KU.json @@ -2,7 +2,8 @@ "appName": "AppFlowy", "defaultUsername": "من", "welcomeText": "@:appName بەخێربێن بۆ", - "githubStarText": "وە گیتهابی ئێمە ئەستێرە بدەن", + "welcomeTo": "بەخێربێن بۆ", + "githubStarText": "بە گیتهابەکەمان ئەستێرە بدەن", "subscribeNewsletterText": "سەبسکرایبی هەواڵنامە بکە", "letsGoButtonText": "دەست پێ بکە", "title": "سه‌ردێڕ", @@ -17,57 +18,75 @@ "openMenuTooltip": "کلیک کردن بۆ کردنەوەی مینیوەکە" }, "signUp": { - "buttonText": "📄️ناو نووسین", + "buttonText": "ناو نووسین", "title": " ناو نووسین لە @:appName", "getStartedText": "دەست پێ بکە", "emptyPasswordError": "ناتوانرێت تێپه‌ڕه‌وشه بەتاڵ بێت", "repeatPasswordEmptyError": "تێپه‌ڕه‌وشەی دووبارەکراو ناتوانرێت بەتاڵ بێت", "unmatchedPasswordError": "دووبارەکراوەی تێپه‌ڕه‌وشه هەمان تێپه‌ڕه‌وشه نییە", "alreadyHaveAnAccount": "لە پێشتر ئەکاونتت هەیە؟", - "emailHint": "📧️ئیمەیڵ", - "passwordHint": "🔑️تێپه‌ڕه‌وشه", + "emailHint": "ئیمەیڵ", + "passwordHint": "تێپه‌ڕه‌وشه", "repeatPasswordHint": "دووبارە کردنی تێپه‌ڕه‌وشه", - "signUpWith": "ناونووسین وە:" + "signUpWith": "ناونووسین بە:" }, "signIn": { - "loginTitle": "چوونه‌ژووره‌وه‌ وە @:appName", + "loginTitle": "چوونه‌ژووره‌وه‌ بە @:appName", "loginButtonText": "چوونه‌ژووره‌وه‌", + "loginStartWithAnonymous": "بە دانیشتنێکی بێناو دەست پێ بکە", "continueAnonymousUser": "وەک بەکارهێنەری میوان بەردەوام بە", "buttonText": "چوونه‌ژووره‌وه‌", + "signingInText": "چوونە ناوەوە...", "forgotPassword": "تێپه‌ڕه‌وشەت لەبیر كردووە ؟", - "emailHint": "📧️ئیمەیڵ", - "passwordHint": "🔑️تێپه‌ڕه‌وشه", + "emailHint": "ئیمەیڵ", + "passwordHint": "تێپه‌ڕه‌وشه", "dontHaveAnAccount": "ئەکاونتت نییە؟", "repeatPasswordEmptyError": "ناتوانرێت تێپه‌ڕه‌وشه بەتاڵ بێت", "unmatchedPasswordError": "دووبارەکراوەی تێپه‌ڕه‌وشه هەمان تێپه‌ڕه‌وشه نییە", + "syncPromptMessage": "ڕەنگە هاوکاتکردنی داتاکان ماوەیەکی پێبچێت. تکایە ئەم پەیجە دامەخە", + "or": "یان", + "LogInWithGoogle": "چوونە ژوورەوە لە ڕێگەی گووگڵەوە", + "LogInWithGithub": "چوونە ژوورەوە لە ڕێگەی گیتهاب", + "LogInWithDiscord": "چوونە ژوورەوە لە ڕێگەی دیسکۆرد", "signInWith": "ناونووسین وە:", "loginAsGuestButtonText": "دەست پێ بکە" }, "workspace": { + "chooseWorkspace": "هەڵبژاردنی شوێنی کارەکەت", "create": "دروستکردنی شوێنی کارکردن", + "reset": "شوێنی کار ڕێست بکەرەوە", + "resetWorkspacePrompt": "ڕێستکردنی شوێنی کارەکە هەموو لاپەڕە و داتاکانی ناوی دەسڕێتەوە. ئایا دڵنیای کە دەتەوێت شوێنی کارەکە ڕێست بکەیتەوە؟ یان دەتوانیت پەیوەندی بە تیمی پشتگیرییەوە بکەیت بۆ گەڕاندنەوەی شوێنی کارەکە", "hint": "شوێنی کارکردن", - "notFoundError": "هیچ شوێنێکی کار نەدۆزراوە" + "notFoundError": "هیچ شوێنێکی کار نەدۆزراوە", + "failedToLoad": "هەندێ شت بە هەڵە ڕۆیشت! شکستی هێنا لە بارکردنی شوێنی کارکردن. هەوڵبدە هەر نموونەیەکی کراوەی AppFlowy دابخەیت و دووبارە هەوڵبدەرەوە.", + "errorActions": { + "reportIssue": "ڕاپۆرت کردنی کێشەیەک", + "reportIssueOnGithub": "ڕاپۆرت کردنی کێشەیەک لەسەر گیتهابەوە ", + "exportLogFiles": "هەناردەکردنی فایلەکانی گوزارش", + "reachOut": "پەیوەندی لەگەڵ دیسکۆرد" + } }, "shareAction": { "buttonText": "هاوبەشکردن", "workInProgress": "بەم زووانە", "markdown": "Markdown", + "csv": "CSV", "copyLink": "کۆپی کردنی لینک" }, "moreAction": { - "small": "بچووک", - "medium": "ناوەند", - "large": "گەورە", "fontSize": "قەبارەی قەڵەم", "import": "زیادکردن", - "moreOptions": "بژاردەی زیاتر" + "moreOptions": "بژاردەی زیاتر", + "small": "بچووک", + "medium": "ناوەند", + "large": "گەورە" }, "importPanel": { "textAndMarkdown": "Text & Markdown", "documentFromV010": "به‌ڵگه‌نامه لە وەشانی 0.1.0", "databaseFromV010": "داتابەیس لە وەشانی 0.1.0", "csv": "CSV", - "database": "🪪️داتابەیس-بنکەدراوە" + "database": "بنکەدراوە" }, "disclosureAction": { "rename": "گۆڕینی ناو", @@ -82,6 +101,10 @@ }, "blankPageTitle": "لاپەڕەی بەتاڵ", "newPageText": "لاپەڕەی نوێ", + "newDocumentText": "بەڵگەنامەی نوێ", + "newGridText": "تۆڕی نوێ", + "newCalendarText": "ڕۆژژمێری نوێ", + "newBoardText": "تەختەی نوێ", "trash": { "text": "زبڵدان", "restoreAll": "گەڕاندنەوەی هەموو", @@ -98,6 +121,13 @@ "confirmRestoreAll": { "title": "ئایا دەتەوێت هەموو لاپەڕەکانی ناو زبڵدانەکە بگەڕێنێتەوە؟", "caption": "ئەم کارە پێچەوانە نابێتەوە." + }, + "mobile": { + "actions": "کردەکانی زبڵدان", + "empty": "تەنەکەی زبڵدان بەتاڵە", + "emptyDescription": "هیچ فایلێکی سڕاوەت نییە", + "isDeleted": "دەسڕدرێتەوە", + "isRestored": "دەگەڕێتەوە" } }, "deletePagePrompt": { @@ -124,6 +154,7 @@ "defaultNewPageName": "بێ ناونیشان", "renameDialog": "گۆڕینی ناو" }, + "noPagesInside": "لە ناوەوە هیچ لاپەڕەیەک نییە", "toolbar": { "undo": "پاشەکشە", "redo": "Redo", @@ -133,25 +164,27 @@ "strike": "لەگەڵ هێڵ لە ناوەڕاستدا", "numList": "لیستی ژمارەدار", "bulletList": "بووڵت لیست", - "checkList": "لیستی پشکنین", + "checkList": "لیستی پیاچوونه‌وه‌", "inlineCode": "کۆدی ناو هێڵ", "quote": "ده‌ق", "header": "سه‌رپه‌ڕه‌", - "highlight": "بەرجەستەکردن⚡️", + "highlight": "بەرجەستەکردن", "color": "ڕەنگ", "addLink": "زیادکردنی لینک", - "link": "🔗️لینک" + "link": "لینک" }, "tooltip": { - "lightMode": "مۆدی کاڵ/لاییت", - "darkMode": "مۆدی تاریک", + "lightMode": "دۆخی ڕووناک", + "darkMode": "دۆخی تاریک", "openAsPage": "کردنەوە وەک لاپەڕە", "addNewRow": "زیادکردنی ڕیزێکی نوێ", "openMenu": "Menu کردنەوەی", "dragRow": "بۆ ڕێکخستنەوەی ڕیزەکە فشارێکی درێژ بکە", "viewDataBase": "بینینی بنکەدراوە", "referencePage": "ئەم {name} ڕەوانە کراوە", - "addBlockBelow": "لە خوارەوە بلۆکێک زیاد بکە" + "addBlockBelow": "لە خوارەوە بلۆکێک زیاد بکە", + "urlLaunchAccessory": "کردنەوە لە وێبگەڕ", + "urlCopyAccessory": "کۆپی کردنی URL" }, "sideBar": { "closeSidebar": "داخستنی سایدبار", @@ -160,7 +193,8 @@ "favorites": "دڵخوازەکان", "clickToHidePersonal": "بۆ شاردنەوەی بەشی کەسی کلیک بکە", "clickToHideFavorites": "بۆ شاردنەوەی بەشی دڵخوازەکان کلیک بکە", - "addAPage": "زیاد کردنی لاپەڕەیەک" + "addAPage": "زیاد کردنی لاپەڕەیەک", + "recent": "نوێ" }, "notifications": { "export": { @@ -175,7 +209,9 @@ "editContact": "دەستکاریکردنی پەیوەندی" }, "button": { + "ok": "باشە", "done": "ئەنجامدرا", + "cancel": "ڕەتکردنەوە", "signIn": "چوونە ژوورەوە", "signOut": "دەرچوون", "complete": "تەواوە", @@ -187,19 +223,29 @@ "discard": "ڕەتکردنەوە", "replace": "شوێن گرتنەوە", "insertBelow": "insert لە خوارەوە", + "insertAbove": "لە سەرەوە دابنێ", "upload": "بارکردن...", "edit": "بژارکردن", "delete": "سڕینەوە", "duplicate": "هاوشێوە کردن", "putback": "بیخەرەوە بۆ دواوە", - "cancel": "ڕەتکردن", - "ok": "ئۆکەی" + "update": "نوێکردنەوە", + "share": "هاوبەشکردن", + "removeFromFavorites": "سڕینەوە لە دڵخوازەکان", + "addToFavorites": "خستنە لیستی دڵخوازەکان", + "rename": "گۆڕینی ناو", + "helpCenter": "ناوەندی یارمەتی", + "add": "زیادکردن", + "yes": "بەڵێ", + "Done": "تەواوه", + "Cancel": "ڕەتکردن", + "OK": "ئۆکەی" }, "label": { "welcome": "بەخێربێن!", - "firstName": "ناوی یەکەم", + "firstName": "ناو", "middleName": "ناوی ناوەڕاست", - "lastName": "ناوی کۆتایی", + "lastName": "دوایین ناو", "stepX": "Step {X}" }, "oAuth": { @@ -222,14 +268,60 @@ "language": "زمانەکان", "user": "بەکارهێنەر", "files": "فایلەکان", + "notifications": "ئاگادارکردنەوەکان", "open": "کردنەوەی ڕێکخستنەکان", "logout": "دەرچوون", "logoutPrompt": "دڵنیای کە دەتەوێت بچیتە دەرەوە؟", + "selfEncryptionLogoutPrompt": "دڵنیای کە دەتەوێت بچیتە دەرەوە؟ تکایە دڵنیابە کە نهێنی کۆدکردنەکەت کۆپی کردووە", "syncSetting": "ڕێکخستنەکانی هاوکاتکردن", + "cloudSettings": "ڕێکخستنەکانی کڵاود", "enableSync": "چالاک کردنی هاوکاتکردن", + "enableEncrypt": "کۆدکردنی داتاکان", + "cloudURL": "بەستەری سەرەکی", + "invalidCloudURLScheme": "پلانی نادروست", + "cloudServerType": "ڕاژەکاری کڵاود", + "cloudServerTypeTip": "تکایە ئاگاداربە کە لەوانەیە دوای گۆڕینی ڕاژەکاری کڵاودکە لە ئەکاونتی ئێستات دەربچێت", + "cloudLocal": "خۆماڵی", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "url ی supabase ناتوانێت بەتاڵ بێت", + "cloudSupabaseAnonKey": "کلیلی شاراوەی Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "کلیلی anon ناتوانێت بەتاڵ بێت", + "cloudAppFlowy": "ئەپفلۆوی کلاود بێتا", + "cloudAppFlowySelfHost": "ئەپفلۆوی کلاود بە هۆستی خۆیی", + "appFlowyCloudUrlCanNotBeEmpty": "url ی هەور ناتوانێت بەتاڵ بێت", + "clickToCopy": "کرتە بۆ کۆپی کردن", + "selfHostStart": "ئەگەر ڕاژه‌كارت نییە، تکایە سەردانی بکە...", + "selfHostContent": "به‌ڵگه‌نامه", + "selfHostEnd": "بۆ ڕێنمایی لەسەر چۆنیەتی خۆهۆستکردنی ڕاژەکاری خۆت", + "cloudURLHint": "URL ی بنەڕەتی ڕاژەکارەکەت بنووسە", + "cloudWSURL": "URL ی وێبسۆکێت", + "cloudWSURLHint": "ناونیشانی وێبسۆکێتی ڕاژەکارەکەت دابنێ", + "restartApp": "دووبارە دەستپێکردنەوە", + "restartAppTip": "بەرنامەکە دووبارە دەستپێبکەرەوە بۆ ئەوەی گۆڕانکارییەکان کاریگەرییان هەبێت. تکایە ئاگاداربە کە ئەمە ڕەنگە ئەکاونتی ئێستات دەربچێت", + "changeServerTip": "دوای گۆڕینی ڕاژەکارەکە، پێویستە کلیک لەسەر دوگمەی دووبارە دەستپێکردنەوە بکەیت بۆ ئەوەی گۆڕانکارییەکان کاریگەرییان هەبێت", + "enableEncryptPrompt": "کۆدکردن چالاک بکە بۆ پاراستنی داتاکانت بەم نهێنییە. بە سەلامەتی هەڵیبگرە؛ کاتێک چالاک کرا، ناتوانرێت بکوژێنرێتەوە. ئەگەر لەدەستچوو، داتاکانت دەبنە شتێکی وەرنەگیراو. بۆ کۆپیکردن کلیک بکە", + "inputEncryptPrompt": "تکایە نهێنی کۆدکردنەکەت بنووسە بۆ...", + "clickToCopySecret": "بۆ کۆپیکردنی نهێنی کلیک بکە", + "configServerSetting": "ڕێکخستنەکانی ڕاژەکارەکەت ڕێکبخە", + "configServerGuide": "دوای هەڵبژاردنی `دەستپێکردنی خێرا`، بچۆ بۆ `ڕێکخستنەکان` و پاشان \"ڕێکخستنەکانی کڵاود\" بۆ ڕێکخستنی سێرڤەری خۆهۆستکراوەکەت.", + "inputTextFieldHint": "نهێنی تۆ", "historicalUserList": "مێژووی چوونەژوورەوەی بەکارهێنەر", "historicalUserListTooltip": "ئەم لیستە ئەکاونتە بێناوەکانت پیشان دەدات. دەتوانیت کلیک لەسەر ئەکاونتێک بکەیت بۆ بینینی وردەکارییەکانی. ئەکاونتی بێناو بە کلیک کردن لەسەر دوگمەی دەستپێکردن دروست دەکرێت", - "openHistoricalUser": "بۆ کردنەوەی ئەکاونتی بێناو کلیک بکە" + "openHistoricalUser": "بۆ کردنەوەی ئەکاونتی بێناو کلیک بکە", + "customPathPrompt": "هەڵگرتنی فۆڵدەری داتاکانی AppFlowy لە فۆڵدەرێکی هاوکاتی کڵاود وەک گووگڵ درایڤ دەتوانێت مەترسی دروست بکات. ئەگەر بنکەدراوەی ناو ئەم فۆڵدەرە لە یەک کاتدا لە چەندین شوێنەوە دەستی پێ بگات یان دەستکاری بکرێت، لەوانەیە ببێتە هۆی ناکۆکی هاوکاتکردن و ئەگەری تێکچوونی داتاکان", + "importAppFlowyData": "هێنانی داتا لە فۆڵدەری دەرەکی AppFlowy", + "importingAppFlowyDataTip": "هێنانی داتا لە قۆناغی جێبەجێکردندایە. تکایە ئەپەکە دامەخە", + "importAppFlowyDataDescription": "داتا لە فۆڵدەری داتای دەرەکی AppFlowy کۆپی بکە و هاوردە بکە بۆ ناو فۆڵدەری داتاکانی AppFlowy ی ئێستا", + "importSuccess": "بە سەرکەوتوویی فۆڵدەری داتاکانی AppFlowy هاوردە کرد", + "importFailed": "هاوردەکردنی فۆڵدەری داتاکانی AppFlowy شکستی هێنا", + "importGuide": "بۆ زانیاری زیاتر، تکایە بەڵگەنامەی ئاماژەپێکراو بپشکنە" + }, + "notifications": { + "enableNotifications": { + "label": "چالاک کردنی ئاگادارکردنەوەکان", + "hint": "کوژاندنەوە بۆ وەستاندنی دەرکەوتنی ئاگادارکردنەوە ناوخۆییەکان" + } }, "appearance": { "resetSetting": "ڕێکخستن لە سفرەوە", @@ -239,12 +331,39 @@ }, "themeMode": { "label": "مۆدی تێم", - "light": "مۆدی کاڵ/لاییت", + "light": "مۆدی ڕوناک", "dark": "مۆدی تاریک", - "system": "خۆگونجاندن لەگەڵ تێمی سیستەمدا" + "system": "خۆگونجاندن لەگەڵ سیستەمدا" + }, + "documentSettings": { + "cursorColor": "ڕەنگی جێنیشانده‌ری بەڵگەنامە", + "selectionColor": "ڕەنگی \"دیاریكراو\" بەڵگەنامە", + "hexEmptyError": "ڕەنگی هێکس ناتوانێت بەتاڵ بێت", + "hexLengthError": "بەهای هێکس دەبێت درێژییەکەی ٦ ژمارە بێت", + "hexInvalidError": "بەهای هێکسی نادروست", + "opacityEmptyError": "لێڵی ناتوانێت بەتاڵ بێت", + "opacityRangeError": "لێڵی دەبێت لە نێوان 1 بۆ 100 بێت", + "app": "App", + "flowy": "Flowy", + "apply": "به‌کاربردن" + }, + "layoutDirection": { + "label": "ئاراستەی داڕشتن", + "hint": "کۆنتڕۆڵی ڕۆیشتنی ناوەڕۆک لەسەر شاشەکەت بکە، لە چەپەوە بۆ ڕاست یان ڕاست بۆ چەپ.", + "ltr": " چەپ بۆ ڕاست", + "rtl": "ڕاست بۆ چەپ" + }, + "textDirection": { + "label": "ئاراستەی دەقی پێشوەختە", + "hint": "دیاری بکە کە ئایا دەق دەبێت لە چەپەوە دەستپێبکات یان ڕاست وەکو پێشوەختە.", + "ltr": " چەپ بۆ ڕاست", + "rtl": "ڕاست بۆ چەپ", + "auto": "خۆکار", + "fallback": "هەمان شێوەی ئاراستەی نەخشە" }, "themeUpload": { "button": "بارکردن", + "uploadTheme": "بارکردنی تێم", "description": "بە بەکارهێنانی دوگمەی خوارەوە تێمی AppFlowy ـەکەت باربکە.", "loading": "تکایە چاوەڕوان بن تا ئێمە تێمی قاڵبەکەت پشتڕاست دەکەینەوە و بار دەکەین...", "uploadSuccess": "تێمی قاڵبەکەت بە سەرکەوتوویی بارکرا", @@ -253,9 +372,23 @@ "urlUploadFailure": "ناتوانرێت URL بکرێتەوە: {}", "failure": "تێمی قاڵبی بارکراو نادروستە." }, - "theme": "تێم و دەرکەوتن", + "theme": "تێم و ڕووکار", "builtInsLabel": "قاڵبی پێش دروستکراو", - "pluginsLabel": "پێوەکراوەکان" + "pluginsLabel": "پێوەکراوەکان", + "dateFormat": { + "label": "فۆرماتی بەروار", + "local": "ناوخۆیی", + "us": "US", + "iso": "ISO", + "friendly": "بەکارهێنانی ئاسانە", + "dmy": "ڕ/م/س" + }, + "timeFormat": { + "label": "فۆرماتی کات", + "twelveHour": "دوانزە کاتژمێر", + "twentyFourHour": "بیست و چوار کاتژمێر" + }, + "showNamingDialogWhenCreatingPage": "پیشاندانی دیالۆگی ناونان لە کاتی دروستکردنی لاپەڕەیەکدا" }, "files": { "copy": "کۆپی", @@ -265,7 +398,7 @@ "restoreLocation": "گەڕاندنەوە بۆ ڕێڕەوی پێشوەختەی AppFlowy", "customizeLocation": "فۆڵدەرێکی دیکە بکەرەوە", "restartApp": "تکایە ئەپەکە دابخە و بیکەرەوە بۆ ئەوەی گۆڕانکارییەکان جێبەجێ بکرێن.", - "exportDatabase": "بنکەدراوە هەناردە بکە", + "exportDatabase": "هەناردە کردنی بنکەدراوە", "selectFiles": "پەڕگەکان هەڵبژێرە بۆ هەناردە کردن", "selectAll": "هەڵبژاردنی هەموویان", "deselectAll": "هەڵبژاردەی هەموو هەڵبگرە", @@ -295,8 +428,11 @@ }, "user": { "name": "ناو", + "email": "ئیمەیڵ", + "tooltipSelectIcon": "هەڵبژاەدنی وێنۆچكه‌", "selectAnIcon": "هەڵبژاردنی وێنۆچكه‌", - "pleaseInputYourOpenAIKey": "🔑️تکایە کلیلی OpenAI ـەکەت بنووسە", + "pleaseInputYourOpenAIKey": "تکایە کلیلی OpenAI ـەکەت بنووسە", + "pleaseInputYourStabilityAIKey": "تکایە جێگیری کلیلی AI ـەکەت بنووسە", "clickToLogout": "بۆ دەرچوون لە بەکارهێنەری ئێستا کلیک بکە" }, "shortcuts": { @@ -309,29 +445,58 @@ "resetToDefault": "گەڕاندنەوە بۆ کلیلەکانی بنه‌ڕه‌ت", "couldNotLoadErrorMsg": "کورتە ڕێگاکان نەتوانرا باربکرێن، تکایە دووبارە هەوڵبدەرەوە", "couldNotSaveErrorMsg": "کورتە ڕێگاکان نەتوانرا پاشەکەوت بکرێن، تکایە دووبارە هەوڵبدەرەوە" + }, + "mobile": { + "personalInfo": "زانیاری کەسی", + "username": "ناوی بەکارهێنەر", + "usernameEmptyError": "ناوی بەکارهێنەر ناتوانێت بەتاڵ بێت", + "about": "لەربارەی", + "pushNotifications": "ئاگادارکردنەوەکانی خستنه‌سه‌ر", + "support": "پشتیوانی", + "joinDiscord": "لە دیسکۆرد لەگەڵمان بن", + "privacyPolicy": "سیاسەتی پاراستنی نهێنی", + "userAgreement": "ڕێککەوتنی بەکارهێنەر", + "termsAndConditions": "بار و دۆخ و مەرجەکان", + "userprofileError": "شکستی هێنا لە بارکردنی پڕۆفایلی بەکارهێنەر", + "userprofileErrorDescription": "تکایە هەوڵبدە بچیتە دەرەوە و بچۆرەوە ژوورەوە بۆ ئەوەی بزانیت ئایا کێشەکە هێشتا بەردەوامە یان نا.", + "selectLayout": "نەخشە هەڵبژێرە", + "selectStartingDay": "ڕۆژی دەستپێکردنەکەت هەڵبژێرە", + "version": "وەشان" } }, "grid": { "deleteView": "ئایا دڵنیای کە دەتەوێت ئەم دیمەنە بسڕیتەوە؟", "createView": "نوێ", + "title": { + "placeholder": "بێ ناونیشان" + }, "settings": { "filter": "فیلتێر", "sort": "پۆلێن کردن", "sortBy": "ڕیزکردن بەپێی", - "properties": "خەسیەتەکان", + "properties": "تایبەتمەندیەکان", "reorderPropertiesTooltip": "بۆ ڕێکخستنەوەی تایبەتمەندییەکان ڕابکێشە", "group": "ده‌سته‌", "addFilter": "زیادکردنی فیلتێر", "deleteFilter": "سڕینەوەی فیلتێر", "filterBy": "فیلتێر بەپێی...", "typeAValue": "بەهایەک بنووسە...", - "layout": "طرح‌بندی", - "databaseLayout": "گه‌ڵاڵه‌به‌ندی" + "layout": "گه‌ڵاڵه‌به‌ندی", + "databaseLayout": "گه‌ڵاڵه‌به‌ندی", + "viewList": { + "zero": "0 بینین", + "one": "{count} بینین", + "other": "{count} بینینەکان" + }, + "editView": "دەستکاری دیمەن", + "boardSettings": "ڕێکخستنەکانی تەختە", + "calendarSettings": "ڕێکخستنەکانی ساڵنامە", + "numberOfVisibleFields": "{} نیشان دراوە" }, "textFilter": { "contains": "لەخۆ دەگرێت", "doesNotContain": "لەخۆناگرێت", - "endsWith": "کۆتایی دێت بە🔚️", + "endsWith": "کۆتایی دێت بە", "startWith": "دەسپێکردن بە", "is": "هەیە", "isNot": "نییە", @@ -368,8 +533,17 @@ "isEmpty": "به‌تاڵه‌", "isNotEmpty": "بەتاڵ نییە" }, + "dateFilter": { + "is": "هەیە", + "before": "پێشترە", + "after": "دوای ئەبێت", + "between": "لە نێواندایە", + "empty": "به‌تاڵه‌", + "notEmpty": "بەتاڵ نییە" + }, "field": { "hide": "شاردنەوە", + "show": "نیشاندان", "insertLeft": "جێگیرکردن لە چەپ", "insertRight": "جێگیرکردن لە ڕاست", "duplicate": "دووبارەکردنەوە", @@ -387,6 +561,7 @@ "numberFormat": "فۆرمات ژمارە", "dateFormat": "فۆرمات ڕێکەوت", "includeTime": "کات لەخۆ بگرێت", + "isRange": "ڕۆژی کۆتایی", "dateFormatFriendly": "Mang Roj, Sall", "dateFormatISO": "Sall-Mang-Roj", "dateFormatLocal": "Mang/Roj/Sall", @@ -397,13 +572,28 @@ "timeFormatTwelveHour": "دوانزە کاتژمێر", "timeFormatTwentyFourHour": "بیست و چوار کاتژمێر", "clearDate": "سڕینەوەی ڕێکەوت", + "dateTime": "کاتی بەروار", + "startDateTime": "کاتی بەرواری دەستپێک", + "endDateTime": "کاتی بەرواری کۆتایی", + "failedToLoadDate": "شکستی هێنا لە بارکردنی بەهای بەروار", + "selectTime": "کات هەڵبژێرە", + "selectDate": "بەروار هەڵبژێرە", + "visibility": "پلەی بینین", + "propertyType": "جۆری تایبه‌تمه‌ندی", "addSelectOption": "زیادکردنی بژاردەیەک", + "typeANewOption": "بژاردەیەکی نوێ بنووسە", "optionTitle": "بژاردەکان", "addOption": "زیادکردنی بژاردە", "editProperty": "دەستکاریکردنی تایبەتمەندی", "newProperty": "تایبەتمەندی نوێ", "deleteFieldPromptMessage": "ئایا دڵنیایت لە سڕدنەوەی ئەم تایبەتمەندییە؟", - "newColumn": "ستوونی نوێ" + "newColumn": "ستوونی نوێ", + "format": "فۆرمات", + "reminderOnDateTooltip": "ئەم خانەیە بیرخستنەوەیەکی بەرنامە بۆ داڕێژراوی هەیە" + }, + "rowPage": { + "newField": "خانەیێکی نوێ زیاد بکە", + "fieldDragElementTooltip": "بۆ کردنەوەی مێنۆ کرتە بکە" }, "sort": { "ascending": "هەڵکشاو", @@ -414,11 +604,14 @@ "row": { "duplicate": "دووبارە کردنەوە", "delete": "سڕینەوە", + "titlePlaceholder": "بێ ناونیشان", "textPlaceholder": "بەتاڵ", "copyProperty": "تایبەتمەندی کۆپی کرا بۆ کلیپبۆرد", "count": "سەرژمێرکردن", "newRow": "ڕیزی نوێ", - "action": "کردەوە" + "action": "کردەوە", + "drag": "ڕاکێشان بۆ جوڵە", + "dragAndClick": "بۆ جوڵاندن ڕابکێشە، کلیک بکە بۆ کردنەوەی مێنۆ" }, "selectOption": { "create": "دروستکردن", @@ -434,13 +627,31 @@ "deleteTag": "سڕینەوە تاگ", "colorPanelTitle": "ڕەنگەکان", "panelTitle": "بژاردەیەک زیاد یان دروستی بکە.", - "searchOption": "گەڕان بەدوای بژاردەیەکدا" + "searchOption": "گەڕان بەدوای بژاردەیەکدا", + "searchOrCreateOption": "گەڕان یان دروستکردنی بژاردەیەک...", + "createNew": "دروستکردنی نوێ", + "orSelectOne": "یان بژاردەیەک هەڵبژێرە", + "typeANewOption": "بژاردەیەکی نوێ بنووسە", + "tagName": "ناوی تاگ" }, "checklist": { - "addNew": "شتێک زیاد بکە" + "taskHint": "وەسفکردنی ئەرک", + "addNew": "شتێک زیاد بکە", + "submitNewTask": "ئافراندن", + "hideComplete": "شاردنەوەی ئەرکە تەواوکراوەکان", + "showComplete": "هەموو ئەرکەکان پیشان بدە" }, "menuName": "تۆڕ", - "referencedGridPrefix": "نواندن" + "referencedGridPrefix": "نواندن", + "calculate": "حیساب بکە", + "calculationTypeLabel": { + "none": "هیچ", + "average": "تێکڕا و ڕێژە", + "max": "زۆر", + "median": "ناوەند", + "min": "کەم", + "sum": "کۆ" + } }, "document": { "menuName": "بەڵگەنامە", @@ -460,15 +671,20 @@ "calendar": { "selectACalendarToLinkTo": "ساڵنامەیەک هەڵبژێرە بۆ ئەوەی لینکی بۆ بکەیت.", "createANewCalendar": "ساڵنامەیەکی نوێ دروست بکە" + }, + "document": { + "selectADocumentToLinkTo": "بەڵگەنامەیەک هەڵبژێرە کە بەستەرەکەی بۆ دابنێیت" } }, "selectionMenu": { - "outline": "گەڵاڵە" + "outline": "گەڵاڵە", + "codeBlock": "بلۆکی کۆد" }, "plugins": { "referencedBoard": "بۆردی چاوگ", - "referencedGrid": "تۆڕی چاوگ", - "referencedCalendar": "ساڵنامەی چاوگ", + "referencedGrid": "تۆڕی ئاماژەپێکراو", + "referencedCalendar": "ساڵنامەی ئاماژەپێکراو", + "referencedDocument": "بەڵگەنامەی ئاماژەپێکراو", "autoGeneratorMenuItemName": "OpenAI نووسەری", "autoGeneratorTitleName": "داوا لە AI بکە هەر شتێک بنووسێت...", "autoGeneratorLearnMore": "زیاتر زانین", @@ -488,7 +704,10 @@ "smartEditDisabled": "لە ڕێکخستنەکاندا پەیوەندی بە OpenAI بکە", "discardResponse": "ئایا دەتەوێت وەڵامەکانی AI بسڕیتەوە؟", "createInlineMathEquation": "درووست کردنی هاوکێشە", - "toggleList": "toggle لیست", + "fonts": "فۆنتەکان", + "toggleList": "Toggle لیستی", + "quoteList": "لیستی وەرگرتە", + "numberedList": "لیستی ژمارەدار", "cover": { "changeCover": "گۆڕینی بەرگ", "colors": "ڕەنگەکان", diff --git a/frontend/resources/translations/cs-CZ.json b/frontend/resources/translations/cs-CZ.json index 42990ad0fb..6ceb6f50ef 100644 --- a/frontend/resources/translations/cs-CZ.json +++ b/frontend/resources/translations/cs-CZ.json @@ -262,10 +262,10 @@ "logoutPrompt": "Opravdu se chcete odhlásit?", "selfEncryptionLogoutPrompt": "Opravdu se chcete odhlásit? Ujistěte se prosím, že jste si zkopírovali šifrovací klíč", "syncSetting": "Synchronizovat nastavení", - "cloudSetting": "Nastavení cloudu", - "cloudURL": "URL adresa serveru", "enableSync": "Zapnout synchronizaci", "enableEncrypt": "Šifrovat data", + "cloudURL": "URL adresa serveru", + "cloudAppFlowy": "AppFlowy Cloud Beta", "enableEncryptPrompt": "Zapněte šifrování a zabezpečte svá ", "inputEncryptPrompt": "Vložte prosím Váš šifrovací klíč k", "clickToCopySecret": "Kliknutím zkopírujete šifrovací klíč", @@ -273,7 +273,8 @@ "historicalUserList": "Historie přihlášení uživatele", "historicalUserListTooltip": "V tomto seznamu vidíte anonymní účty. Kliknutím na účet zobrazíte jeho detaily. Anonymní účty vznikají kliknutím na tlačítko \"Začínáme\"", "openHistoricalUser": "Kliknutím založíte anonymní účet", - "customPathPrompt": "Uložením složky s daty AppFlowy ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v složce navštíví nebo změní " + "customPathPrompt": "Uložením složky s daty AppFlowy ve složce synchronizovanéí jako např. Google Drive může nést rizika. Pokud se databáze v složce navštíví nebo změní ", + "cloudSetting": "Nastavení cloudu" }, "notifications": { "enableNotifications": { @@ -311,12 +312,12 @@ "button": "Nahrát", "uploadTheme": "Nahrát motiv vzhledu", "description": "Nahrajte vlastní motiv vzhledu pro AppFlowy stisknutím tlačítka níže.", - "failure": "Nahrané téma vzhledu má neplatný formát.", "loading": "Prosím počkejte dokud nedokončíme kontrolu a nahrávání vašeho motivu vzhledu...", "uploadSuccess": "Váš motiv vzhledu byl úspěšně nahrán", "deletionFailure": "Nepodařilo se smazat motiv vzhledu. Zkuste ho smazat ručně.", "filePickerDialogTitle": "Vyberte soubor typu .flowy_plugin", - "urlUploadFailure": "Nepodařilo se otevřít URL adresu: {}" + "urlUploadFailure": "Nepodařilo se otevřít URL adresu: {}", + "failure": "Nahrané téma vzhledu má neplatný formát." }, "theme": "Motiv vzhledu", "builtInsLabel": "Vestavěné motivy vzhledu", @@ -454,13 +455,9 @@ "isComplete": "je hotový", "isIncomplted": "není hotový" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Je", "isNot": "Není", - "isEmpty": "Je prázdné", - "isNotEmpty": "Není prázdné" - }, - "multiSelectOptionFilter": { "contains": "Obsahuje", "doesNotContain": "Neobsahuje", "isEmpty": "Je prázdné", @@ -1094,4 +1091,4 @@ "font": "Písmo", "actions": "Příkazy" } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index 0e9305c744..dda65118cf 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -2,31 +2,33 @@ "appName": "AppFlowy", "defaultUsername": "Ich", "welcomeText": "Willkommen bei @:appName", + "welcomeTo": "Willkommen zu", "githubStarText": "Mit einem Stern auf GitHub markieren", "subscribeNewsletterText": "Abonniere den Newsletter", "letsGoButtonText": "Los geht's", "title": "Titel", "youCanAlso": "Du kannst auch", "and": "und", + "failedToOpenUrl": "URL konnte nicht geöffnet werden: {}", "blockActions": { - "addBelowTooltip": "Klicken, um etwas unten hinzuzufügen", + "addBelowTooltip": "Unten klicken um etwas hinzuzufügen", "addAboveCmd": "Alt+Klick", "addAboveMacCmd": "Option+Klick", "addAboveTooltip": "oben hinzufügen", - "dragTooltip": "Verschieben durch ziehen", + "dragTooltip": "Drag to Drop", "openMenuTooltip": "Klicken, um das Menü zu öffnen" }, "signUp": { "buttonText": "Registrieren", "title": "Registriere dich bei @:appName", "getStartedText": "Erste Schritte", - "emptyPasswordError": "Das Passwort darf nicht leer sein", - "repeatPasswordEmptyError": "Die Passwortwiederholung darf nicht leer sein", - "unmatchedPasswordError": "Die Passwörter stimmen nicht überein", - "alreadyHaveAnAccount": "Bereits registriert?", + "emptyPasswordError": "Passwort darf nicht leer sein", + "repeatPasswordEmptyError": "Passwortwiederholung darf nicht leer sein", + "unmatchedPasswordError": "Passwörter stimmen nicht überein", + "alreadyHaveAnAccount": "Hast du schon ein Account?", "emailHint": "E-Mail", "passwordHint": "Passwort", - "repeatPasswordHint": "Wiederhole Passwort", + "repeatPasswordHint": "Passwort wiederholen", "signUpWith": "Anmelden mit:" }, "signIn": { @@ -35,13 +37,14 @@ "loginStartWithAnonymous": "Anonyme Sitzung starten", "continueAnonymousUser": "in anonymer Sitzung fortfahren", "buttonText": "Anmelden", + "signingInText": "Anmelden...", "forgotPassword": "Passwort vergessen?", "emailHint": "E-Mail", "passwordHint": "Passwort", "dontHaveAnAccount": "Noch kein Konto?", "repeatPasswordEmptyError": "Passwortwiederholung darf nicht leer sein", "unmatchedPasswordError": "Passwörter stimmen nicht überein", - "syncPromptMessage": "Die Synchronisation kann ein paar Minuten dauern. Bitte diese Seite nicht schließen", + "syncPromptMessage": "Synchronisation kann ein paar Minuten dauern. Diese Seite bitte nicht schließen", "or": "ODER", "LogInWithGoogle": "Mit Google-Account anmelden", "LogInWithGithub": "Mit GitHub-Account anmelden", @@ -49,17 +52,34 @@ "signInWith": "Anmeldeoptionen:" }, "workspace": { - "chooseWorkspace": "Arbeitsbereich wählen", - "create": "Arbeitsbereich erstellen", - "reset": "Arbeitsbereich zurücksetzen", - "resetWorkspacePrompt": "Das zurücksetzen des Arbeitsbereiches löscht alle enthaltenen Seiten und Daten. Sind sie sicher dass sie den Arbeitsbereich zurücksetzen wollen? ", - "hint": "Arbeitsbereich", - "notFoundError": "Arbeitsbereich nicht gefunden", - "failedToLoad": "Etwas ist schief gelaufen! Der Arbeitsbereich konnte nicht geladen werden. Versuchen Sie, alle AppFlowy Instanzen zu schließen, und versuchen Sie es erneut.", + "chooseWorkspace": "Workspace wählen", + "create": "Workspace erstellen", + "reset": "Workspace zurücksetzen", + "resetWorkspacePrompt": "Das Zurücksetzen des Workspace löscht alle enthaltenen Seiten und Daten. Bist du sicher, dass du den Arbeitsbereich zurücksetzen möchstest? ", + "hint": "Workspace", + "notFoundError": "Workspace nicht gefunden", + "failedToLoad": "Etwas ist schief gelaufen! Der Workspace konnte nicht geladen werden. Versuche, alle AppFlowy-Instanzen zu schließen & versuche es erneut.", "errorActions": { "reportIssue": "Problem melden", - "reachOut": "Kontaktieren Sie uns auf Discord" - } + "reportIssueOnGithub": "Melde ein Problem auf Github", + "exportLogFiles": "Exportiere Log-Dateien", + "reachOut": "Kontaktiere uns auf Discord" + }, + "menuTitle": "Arbeitsbereiche", + "deleteWorkspaceHintText": "Sicher, dass du dein Workspace löschen möchtest?\nDies kann nicht mehr Rückgängig gemacht werden.", + "createSuccess": "Workspace erfolgreich erstellt", + "createFailed": "Der Workspace konnte nicht erstellt werden", + "createLimitExceeded": "Du hast die für dein Benutzerkonto maximal zulässige Anzahl an Arbeitsbereichen erreicht. Benötigst du zum fortsetzen deiner Arbeit noch weitere Arbeitsbereiche, erstelle auf Github bitte eine entsprechende Anfrage.", + "deleteSuccess": "Workspace erfolgreich gelöscht", + "deleteFailed": "Der Workspace konnte nicht gelöscht werden", + "openSuccess": "Workspace erfolgreich geöffnet", + "openFailed": "Der Workspace konnte nicht geöffnet werden", + "renameSuccess": "Workspace erfolgreich umbenannt", + "renameFailed": "Der Workspace konnte nicht umbenannt werden", + "updateIconSuccess": "Workspace erfolgreich zurückgesetzt", + "updateIconFailed": "Der Workspace konnte nicht zurückgesetzt werden", + "cannotDeleteTheOnlyWorkspace": "Der einzig vorhandene Arbeitsbereich kann nicht gelöscht werden", + "fetchWorkspacesFailed": "Arbeitsbereiche konnten nicht abgerufen werden!" }, "shareAction": { "buttonText": "Teilen", @@ -74,7 +94,12 @@ "large": "groß", "fontSize": "Schriftgröße", "import": "Importieren", - "moreOptions": "Mehr Optionen" + "moreOptions": "Weitere Optionen", + "wordCount": "Wortanzahl: {}", + "charCount": "Zeichenanzahl: {}", + "createdAt": "Erstellt am: {}", + "deleteView": "Löschen", + "duplicateView": "Duplizieren" }, "importPanel": { "textAndMarkdown": "Text & Markdown", @@ -110,23 +135,24 @@ "created": "Erstellt" }, "confirmDeleteAll": { - "title": "Sicher, dass alle Seiten im Papierkorb gelöscht werden?", + "title": "Bist du dir sicher? Das löscht alle Seiten in den Papierkorb.", "caption": "Diese Aktion kann nicht rückgängig gemacht werden." }, "confirmRestoreAll": { - "title": "Sicher, dass alle Seiten im Papierkorb wiederhergestellt werden?", + "title": "Möchtest du wirklich alle Seiten aus dem Papierkorb wiederherstellen?", "caption": "Diese Aktion kann nicht rückgängig gemacht werden." }, "mobile": { - "actions": "Papierkorbaktionen", - "empty": "Der Papierkorb ist leer", - "emptyDescription": "Es sind keine gelöschten Dateien vorhanden", - "isDeleted": "ist gelöscht", - "isRestored": "ist wiederhergestellt" - } + "actions": "Papierkorb-Einstellungen", + "empty": "Der Papierkorb ist leer.", + "emptyDescription": "Es sind keine gelöschten Dateien vorhanden.", + "isDeleted": "wurde gelöscht", + "isRestored": "wurde wiederhergestellt" + }, + "confirmDeleteTitle": "Bist du dir sicher, dass du diese Seite unwiderruflich löschen möchtest?" }, "deletePagePrompt": { - "text": "Diese Seite ist im Papierkorb", + "text": "Diese Seite befindet sich im Papierkorb", "restore": "Seite wiederherstellen", "deletePermanent": "Dauerhaft löschen" }, @@ -139,13 +165,13 @@ "debug": { "name": "Debug-Informationen", "success": "Debug-Informationen in die Zwischenablage kopiert!", - "fail": "Debug-Informationen können nicht in die Zwischenablage kopiert werden" + "fail": "Debug-Informationen konnten nicht in die Zwischenablage kopiert werden" }, "feedback": "Feedback" }, "menuAppHeader": { "moreButtonToolTip": "Entfernen, umbenennen und mehr...", - "addPageTooltip": "Hier eine Seite direkt hinzufügen", + "addPageTooltip": "Schnell eine Seite hineinfügen", "defaultNewPageName": "Unbenannt", "renameDialog": "Umbenennen" }, @@ -165,8 +191,8 @@ "header": "Überschrift", "highlight": "Hervorhebung", "color": "Farbe", - "addLink": "Verknüpfung hinzufügen", - "link": "Verknüpfung" + "addLink": "Link hinzufügen", + "link": "Link" }, "tooltip": { "lightMode": "In den hellen Modus wechseln", @@ -177,7 +203,7 @@ "dragRow": "Gedrückt halten, um die Zeile neu anzuordnen", "viewDataBase": "Datenbank ansehen", "referencePage": "Auf diesen {Name} wird verwiesen", - "addBlockBelow": "Einen Block unten hinzufügen", + "addBlockBelow": "Einen Block hinzufügen", "urlLaunchAccessory": "Im Browser öffnen", "urlCopyAccessory": "Webadresse kopieren." }, @@ -185,11 +211,17 @@ "closeSidebar": "Seitenleiste schließen", "openSidebar": "Seitenleiste öffnen", "personal": "Persönlich", + "private": "Privat", + "public": "Öffentlich", "favorites": "Favoriten", + "clickToHidePrivate": "Hier klicken, um den privaten Bereich auszublenden.\nVon dir hier erstellte Seiten sind nur für dich sichtbar.", + "clickToHidePublic": "Hier klicken, um den öffentlichen Raum auszublenden.\nVon dir hier erstellte Seiten sind für jedes Mitglied sichtbar.", "clickToHidePersonal": "Klicken, um den persönlichen Abschnitt zu verbergen", "clickToHideFavorites": "Klicken, um Favoriten zu verbergen", "addAPage": "Seite hinzufügen", - "recent": "Zueletzt" + "addAPageToPrivate": "Eine Seite zum privaten Bereich hinzufügen.", + "addAPageToPublic": "Eine Seite zum öffentlichen Bereich hinzufügen.", + "recent": "Zuletzt" }, "notifications": { "export": { @@ -200,12 +232,12 @@ "contactsPage": { "title": "Kontakte", "whatsHappening": "Was passiert diese Woche?", - "addContact": "Kontakt hinzufügen", - "editContact": "Kontakt bearbeiten" + "addContact": "Kontakte hinzufügen", + "editContact": "Kontakte bearbeiten" }, "button": { "ok": "OK", - "done": "Erledigt", + "done": "Erledigt!", "cancel": "Abbrechen", "signIn": "Anmelden", "signOut": "Abmelden", @@ -213,7 +245,7 @@ "save": "Speichern", "generate": "Erstellen", "esc": "ESC", - "keep": "Halten", + "keep": "behalten", "tryAgain": "Nochmal versuchen", "discard": "Verwerfen", "replace": "Ersetzen", @@ -223,15 +255,27 @@ "edit": "Bearbeiten", "delete": "Löschen", "duplicate": "Duplikat", - "putback": "Zurück geben", + "putback": "wieder zurückgeben", "update": "Update", "share": "Teilen", - "removeFromFavorites": "Von den Favoriten entfernen", + "removeFromFavorites": "Aus den Favoriten entfernen", "addToFavorites": "Zu den Favoriten hinzufügen", "rename": "Umbenennen", - "helpCenter": "Hilfe", + "helpCenter": "Hilfe Center", "add": "Hinzufügen", - "yes": "Ja" + "yes": "Ja", + "clear": "Leeren", + "remove": "Entfernen", + "dontRemove": "Nicht entfernen", + "copyLink": "Link kopieren", + "align": "zentrieren", + "login": "Anmelden", + "logout": "Abmelden", + "deleteAccount": "Benutzerkonto löschen", + "back": "Zurück", + "signInGoogle": "Mit einem Google Benutzerkonto anmelden", + "signInGithub": "Mit einem Github Benutzerkonto anmelden", + "signInDiscord": "Mit einem Discord Benutzerkonto anmelden" }, "label": { "welcome": "Willkommen!", @@ -242,15 +286,15 @@ }, "oAuth": { "err": { - "failedTitle": "Keine Verbindung zu Ihrem Konto möglich.", - "failedMsg": "Bitte prüfen, ob der Anmeldevorgang im Browser abgeschlossen wurde." + "failedTitle": "Keine Verbindung zum Konto möglich.", + "failedMsg": "Prüfe, ob der Anmeldevorgang im Browser abgeschlossen wurde." }, "google": { - "title": "GOOGLE ANMELDUNG", - "instruction1": "Um Ihre Google-Kontakte zu importieren, müssen Sie diese Anwendung über Ihren Webbrowser autorisieren.", - "instruction2": "Kopieren Sie diesen Code in Ihre Zwischenablage, indem Sie auf das Symbol klicken oder den Text auswählen:", - "instruction3": "Rufen Sie den folgenden Link in Ihrem Webbrowser auf, und geben Sie den obigen Code ein:", - "instruction4": "Klicken Sie unten auf die Schaltfläche, wenn Sie die Anmeldung abgeschlossen haben:" + "title": "Google Sign-In", + "instruction1": "Um die Google-Kontakte zu importieren, muss die Anwendung über den Webbrowser autorisiert werden.", + "instruction2": "Kopiere den Code in die Zwischenablage, über das Symbol oder indem du den Text auswählst:", + "instruction3": "Rufe den folgenden Link im Webbrowser auf und gebe den Code ein:", + "instruction4": "Klicke unten auf die Schaltfläche, wenn die Anmeldung abgeschlossen ist:" } }, "settings": { @@ -264,7 +308,7 @@ "open": "Einstellungen öffnen", "logout": "Abmelden", "logoutPrompt": "Wollen sie sich wirklich Abmelden?", - "selfEncryptionLogoutPrompt": "Wollen sie sich wirklich Abmelden? Bitte sicherstellen, dass der Encryption Secret Code kopiert wurde.", + "selfEncryptionLogoutPrompt": "Willst du dich wirklich Abmelden? Bitte stelle sicher, dass der Encryption Secret Code kopiert wurde.", "syncSetting": "Sync Einstellung", "cloudSettings": "Cloud Einstellungen", "enableSync": "Sync aktivieren", @@ -272,42 +316,47 @@ "cloudURL": "Basis URL", "invalidCloudURLScheme": "Ungültiges Format", "cloudServerType": "Cloud Server", - "cloudServerTypeTip": "Bitte beachten, dass Sie vom aktuellen Account ausgeloggt werden, nach dem Wechsel zum Cloud Server", + "cloudServerTypeTip": "Bitte beachte, dass der aktuelle Benutzer ausgeloggt wird beim wechsel des Cloud-Servers", "cloudLocal": "Lokal", "cloudSupabase": "Supabase", "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "Die Supabase-URL darf nicht leer sein", "cloudSupabaseAnonKey": "Supabase anonymer Schlüssel", - "cloudSupabaseAnonKeyCanNotBeEmpty": "Der anonyme Schlüssel darf nicht leer sein, wenn die Supabase URL gesetzt ist.", - "cloudAppFlowy": "AppFlowy Cloud", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Der anonyme Schlüssel darf nicht leer sein", + "cloudAppFlowy": "AppFlowy Cloud [BETA]", + "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", + "appFlowyCloudUrlCanNotBeEmpty": "Die Cloud-URL darf nicht leer sein", "clickToCopy": "Klicken, um zu kopieren", - "selfHostStart": "Falls Sie keinen Server haben, verweisen Sie bitte auf", + "selfHostStart": "Falls du keinen Server hast, nehme lieber folgende", "selfHostContent": "Dokument", - "selfHostEnd": "für Hilfe, um einen einen Server aufzusetzen", + "selfHostEnd": "für Hilfe, um einen einen eigenen Server aufzusetzen", "cloudURLHint": "Eingabe der Basis- URL Ihres Servers", "cloudWSURL": "Websocket URL", "cloudWSURLHint": "Eingbe der Websocket Adresse Ihres Servers", "restartApp": "Neustart", - "restartAppTip": "Programm neustarten, um die Änderungen zu übernehmen. Bitte bachten, dass Sie vom aktuellen Account eventuell ausgeloggt werden.", - "enableEncryptPrompt": "Verschlüsselung aktivieren, um Ihre Daten mit dem Secret Key zu verschlüsseln. Verwahren Sie den Schlüssel sicher. Einmal aktiviert kann es nicht mehr rückgängig gemacht werden. Falls der Schlüssel verloren geht sind Ihre Daten unwiderbringlich verloren. Klicken, um zu kopieren.", + "restartAppTip": "Programm neustarten, um die Änderungen zu übernehmen. Bitte bachten, dass der aktuelle Account eventuell ausgeloggt wird.", + "changeServerTip": "Nach dem Wechsel des Servers muss auf die Schaltfläche „Neustart“ geklickt werden, damit die Änderungen wirksam werden", + "enableEncryptPrompt": "Verschlüsselung aktivieren, um deine Daten mit dem Secret Key zu verschlüsseln. Verwahre den Schlüssel sicher! \nEinmal aktiviert kann es nicht mehr rückgängig gemacht werden.\nFalls der Schlüssel verloren geht sind die Daten unwiderbringlich verloren.\nKlicken, um zu kopieren.", "inputEncryptPrompt": "Bitte den Encryption Secret Code eingeben", "clickToCopySecret": "Klicken, um den Secret Code zu kopieren", - "configServerSetting": "Ihre Servereinstellungen anpassen", - "configServerGuide": "Zu erst `Schnellstart/Quick Start` auswählen, dann zu den `Einstellungen/Settings` wechseln und dann die Cloud-Einstellungen \"Cloud Settings\" auswählen, um Ihren Server zu konfigurieren.", - "inputTextFieldHint": "Ihr Secret Code", - "historicalUserList": "Nutzer-Anmelde-Historie", - "historicalUserListTooltip": "Diese Liste zeigt Ihre anonymen Accounts. Sie können einen Account anklicken, um die Detailinformationen zu sehen. Anonyme Accounts werden über den 'Erste Schritte' Button erstellt", - "openHistoricalUser": "Klicken, um den anonymen Account zu öffnen", + "configServerSetting": "Deine Servereinstellungen anpassen", + "configServerGuide": "`Schnellstart/Quick Start` auswählen, dann zu den `Einstellungen/Settings` wechseln und dann die Cloud-Einstellungen \"Cloud Settings\" auswählen, um deinen Server zu konfigurieren.", + "inputTextFieldHint": "Dein Secret-Code", + "historicalUserList": "Anmeldeverlauf", + "historicalUserListTooltip": "Diese Liste zeigt deine anonymen Accounts. Du kannst einen Account anklicken, um mehr Informationen zu sehen.\nAnonyme Accounts werden über den 'Erste Schritte' Button erstellt.", + "openHistoricalUser": "Klicken, um einen anonymen Account zu öffnen", "customPathPrompt": "Den AppFlowy Daten-Ordner in einem mit der Cloud synchronisierten Ordner (z.B. Google Drive) zu speichern, könnte Risiken bergen. Falls die Datenbank innerhalb dieses Ordners gleichzeitig von mehreren Orten zugegriffen oder verändert wird könnte es zu Synchronisationskonflikten und potentiellen Daten-Beschädigung führen", "importAppFlowyData": "Daten von einem externen AppFlowy Ordner importieren.", + "importingAppFlowyDataTip": "Der Datenimport läuft. Bitte die App nicht schließen oder in den Hintergrund setzten", "importAppFlowyDataDescription": "Daten von einem externen AppFlowy Ordner kopieren und in den aktuellen AppFlowy Datenordner importieren.", - "importSuccess": "Der AppFlowy Datenordner wurde erfolgreich importiert", - "importFailed": "Der AppFlowy Datenordner-Import ist fehlgeschlagen", + "importSuccess": "Der AppFlowy Dateienordner wurde erfolgreich importiert", + "importFailed": "Der AppFlowy Dateienordner-Import ist fehlgeschlagen", "importGuide": "Für weitere Details, bitte das verlinkte Dokument prüfen" }, "notifications": { "enableNotifications": { "label": "Benachrichtigungen aktivieren", - "hint": "Ausschalten, damit die lokalen Benachrichtigungen nicht mehr angezeigt werden" + "hint": "Wenn diese Funktion ausgeschaltet ist, werden keine lokalen Benachrichtigungen mehr angezeigt." } }, "appearance": { @@ -317,11 +366,12 @@ "search": "Suchen" }, "themeMode": { - "label": "Designmodus", + "label": "Design", "light": "Helles Design", "dark": "Dunkles Design", - "system": "Automatisch, wie Betriebssystem" + "system": "Wie Betriebssystem" }, + "fontScaleFactor": "Schriftgröße", "documentSettings": { "cursorColor": "Dokument Cursor-Farbe", "selectionColor": "Dokument Auswahl-Farbe", @@ -332,35 +382,35 @@ "opacityRangeError": "Transparenz ist ein Wert zwischen 1 und 100", "app": "App", "flowy": "Flowy", - "apply": "Apply" + "apply": "Verwenden" }, "layoutDirection": { "label": "Layoutrichtung", "hint": "Steuere den Umlauf der Inhalte auf deinem Bildschirm: Von Links nach Rechts oder von Rechts nach Links.", - "ltr": "LNR", - "rtl": "RNL" + "ltr": "Links nach Rechts", + "rtl": "Rechts nach Links" }, "textDirection": { "label": "Textrichtung", "hint": "Wie soll der Text laufen: von Links nach Rechts oder von Rechts nach Links?", - "ltr": "LNR", - "rtl": "RNL", + "ltr": "Links nach Rechts", + "rtl": "Rechts nach Links", "auto": "AUTO", - "fallback": "Wie die Layoutrichtung" + "fallback": "Wie Layoutrichtung" }, "themeUpload": { "button": "Hochladen", "uploadTheme": "Theme hochladen", - "description": "Laden Sie Ihr eigenes AppFlowy-Theme über die Schaltfläche unten hoch.", - "loading": "Bitte warten Sie, während wir Ihr Theme validieren und hochladen ...", - "uploadSuccess": "Ihr Theme wurde erfolgreich hochgeladen", - "deletionFailure": "Das Thema konnte nicht gelöscht werden. Versuchen Sie, es manuell zu löschen.", - "filePickerDialogTitle": "Wählen Sie eine .flowy_plugin-Datei", + "description": "Lade eigenes AppFlowy-Theme über die Schaltfläche unten hoch.", + "loading": "Bitte warte einen Moment . . .\nWir validieren gerade dein Theme und laden es hoch.", + "uploadSuccess": "Das Theme wurde erfolgreich hochgeladen", + "deletionFailure": "Das Theme konnte nicht gelöscht werden. Versuche, es manuell zu löschen.", + "filePickerDialogTitle": "Wähle eine .flowy_plugin-Datei", "urlUploadFailure": "URL konnte nicht geöffnet werden: {}", "failure": "Das hochgeladene Theme hatte ein ungültiges Format." }, "theme": "Theme", - "builtInsLabel": "Integrierte Themes", + "builtInsLabel": "Integrierte Theme", "pluginsLabel": "Plugins", "dateFormat": { "label": "Datumsformat", @@ -368,14 +418,43 @@ "us": "US", "iso": "ISO", "friendly": "Freundlich", - "dmy": "T/M/J" + "dmy": "TT/MM/JJJJ" }, "timeFormat": { "label": "Zeitformat", "twelveHour": "12 Stunden", "twentyFourHour": "24 Stunden" }, - "showNamingDialogWhenCreatingPage": "Zeige Bennenungsfenster wenn eine neue Seite erstellt wird" + "showNamingDialogWhenCreatingPage": "Zeige Bennenungsfenster, wenn eine neue Seite erstellt wird", + "enableRTLToolbarItems": "Aktivieren Sie RTL-Symbolleiste", + "members": { + "title": "Mitglieder-Einstellungen", + "inviteMembers": "Mitglieder einladen", + "sendInvite": "Einladung senden", + "copyInviteLink": "Kopiere Einladungslink", + "label": "Mitglieder", + "user": "Nutzer", + "role": "Rolle", + "removeFromWorkspace": "Vom Workspace entfernen", + "owner": "Besitzer", + "guest": "Gast", + "member": "Mitglied", + "memberHintText": "Ein Mitglied kann Seiten lesen, kommentieren und bearbeiten, sowie Einladungen an Mitglieder & Gäste versenden.", + "guestHintText": "Ein Gast kann mit Erlaubnis bestimmte Seiten lesen, reagieren, kommentieren und bearbeiten.", + "emailInvalidError": "Ungültige E-Mail. Bitte prüfe die E-Mail und versuche es erneut.", + "emailSent": "E-Mail gesendet. Prüfe den Posteingang.", + "members": "Mitglieder", + "membersCount": { + "zero": "{} Mitglieder", + "one": "{} Mitglied", + "other": "{} Mitglieder" + }, + "memberLimitExceeded": "Du hast die maximal zulässige Mitgliederzahl für dein Benutzerkonto erreicht. Benötigst du weitere Mitglieder, um deine Arbeit fortsetzen zu können, erstelle auf Github bitte eine entsprechende Anfrage.", + "failedToAddMember": "Mitglied konnte nicht hinzugefügt werden!", + "addMemberSuccess": "Mitglied erfolgreich hinzugefügt", + "removeMember": "Mitglied entfernen", + "areYouSureToRemoveMember": "Möchten Sie dieses Mitglied wirklich entfernen?" + } }, "files": { "copy": "Kopieren", @@ -411,15 +490,19 @@ "recoverLocationTooltips": "Zurücksetzen auf das Standarddatenverzeichnis von AppFlowy", "exportFileSuccess": "Datei erfolgreich exportiert!", "exportFileFail": "Datei-Export fehlgeschlagen!", - "export": "Export" + "export": "Export", + "clearCache": "Cache leeren", + "clearCacheDesc": "Wenn Probleme auftreten, dass Bilder nicht geladen werden oder Schriftarten nicht richtig angezeigt werden, versuche, den Cache zu leeren. Durch diese Aktion werden die Benutzerdaten nicht entfernt.", + "areYouSureToClearCache": "Möchtest du den Cache wirklich leeren?", + "clearCacheSuccess": "Cache erfolgreich geleert!" }, "user": { "name": "Name", "email": "E-Mail", "tooltipSelectIcon": "Symbol auswählen", "selectAnIcon": "Ein Symbol auswählen", - "pleaseInputYourOpenAIKey": "Bitte geben Sie Ihren OpenAI-Schlüssel ein", - "pleaseInputYourStabilityAIKey": "Bitte geben Sie Ihren Stability AI Schlüssel ein", + "pleaseInputYourOpenAIKey": "Bitte gebe den OpenAI-Schlüssel ein", + "pleaseInputYourStabilityAIKey": "Bitte gebe den Stability AI Schlüssel ein", "clickToLogout": "Klicken, um den aktuellen Nutzer auszulogen" }, "shortcuts": { @@ -440,13 +523,15 @@ "about": "Über", "pushNotifications": "Push Benachrichtigungen", "support": "Support", - "joinDiscord": "Komm zu uns bei Discord", + "joinDiscord": "Komm zu uns auf Discord", "privacyPolicy": "Datenschutz", "userAgreement": "Nutzungsbedingungen", + "termsAndConditions": "Geschäftsbedingungen", "userprofileError": "Das Nutzerprofil konnte nicht geladen werden", "userprofileErrorDescription": "Bitte abmelden und wieder anmelden, um zu prüfen ob das Problem weiterhin bestehen bleibt.", "selectLayout": "Layout auswählen", - "selectStartingDay": "Ersten Tag auswählen" + "selectStartingDay": "Ersten Tag auswählen", + "version": "Version" } }, "grid": { @@ -468,14 +553,14 @@ "typeAValue": "Einen Wert eingeben...", "layout": "Layout", "databaseLayout": "Layout", + "viewList": "Datenbank-Ansichten", "editView": "Ansicht editieren", "boardSettings": "Board-Einstellungen", "calendarSettings": "Kalender-Einstellungen", "createView": "New Ansicht", "duplicateView": "Ansicht duplizieren", "deleteView": "Anslicht löschen", - "numberOfVisibleFields": "{} zeigen", - "viewList": "Datenbank-Ansichten" + "numberOfVisibleFields": "{} angezeigt" }, "textFilter": { "contains": "Enthält", @@ -505,13 +590,9 @@ "isComplete": "ist komplett", "isIncomplted": "ist unvollständig" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Ist", "isNot": "Ist nicht", - "isEmpty": "Ist leer", - "isNotEmpty": "Ist nicht leer" - }, - "multiSelectOptionFilter": { "contains": "Enthält", "doesNotContain": "Beinhaltet nicht", "isEmpty": "Ist leer", @@ -521,11 +602,29 @@ "is": "Ist", "before": "Ist bevor", "after": "Ist nach", - "onOrBefore": "Ist am oder bevor", + "onOrBefore": "Ist am oder vor", "onOrAfter": "Ist am oder nach", "between": "Ist zwischen", "empty": "Ist leer", - "notEmpty": "Ist nicht leer" + "notEmpty": "Ist nicht leer", + "choicechipPrefix": { + "before": "Vorher", + "after": "Danach", + "onOrBefore": "Am oder davor", + "onOrAfter": "Während oder danach", + "isEmpty": "leer", + "isNotEmpty": "nicht leer" + } + }, + "numberFilter": { + "equal": "gleich", + "notEqual": "ungleich", + "lessThan": "weniger als", + "greaterThan": "größer als", + "lessThanOrEqualTo": "weniger als oder gleich wie", + "greaterThanOrEqualTo": "größer als oder gleich wie", + "isEmpty": "leer", + "isNotEmpty": "nicht leer" }, "field": { "hide": "Verstecken", @@ -534,6 +633,7 @@ "insertRight": "Rechts einfügen", "duplicate": "Duplikat", "delete": "Löschen", + "clear": " Zelleninhalte löschen", "textFieldName": "Text", "checkboxFieldName": "Kontrollkästchen", "dateFieldName": "Datum", @@ -544,6 +644,7 @@ "multiSelectFieldName": "Mehrfachauswahl", "urlFieldName": "URL", "checklistFieldName": "Checkliste", + "relationFieldName": "Beziehung", "numberFormat": "Zahlenformat", "dateFormat": "Datumsformat", "includeTime": "Zeitangabe", @@ -566,15 +667,18 @@ "selectDate": "Auswahl Datum", "visibility": "Sichtbarkeit", "propertyType": "Eigenschaftstyp", - "addSelectOption": "Fügen Sie eine Option hinzu", + "addSelectOption": "Füge Option hinzu", "typeANewOption": "Eine neue Option eingeben", "optionTitle": "Optionen", "addOption": "Option hinzufügen", "editProperty": "Eigenschaft bearbeiten", "newProperty": "Neue Eigenschaft", "deleteFieldPromptMessage": "Sicher? Diese Eigenschaft wird gelöscht", + "clearFieldPromptMessage": "Bist du dir sicher? Alle Zelleninhalte in dieser Spalte werden gelöscht!", "newColumn": "Neue Spalte", - "format": "Format" + "format": "Format", + "reminderOnDateTooltip": "Diese Zeile hat eine terminierte Erinnerung", + "optionAlreadyExist": "Einstellung existiert bereits" }, "rowPage": { "newField": "Ein neues Feld hinzufügen", @@ -593,8 +697,13 @@ "sort": { "ascending": "Aufsteigend", "descending": "Absteigend", + "by": "von", + "empty": "Keine Sortierung", + "cannotFindCreatableField": "Es konnte kein geeignetes Feld zum Sortieren gefunden werden", "deleteAllSorts": "Alle Sortierungen entfernen", - "addSort": "Sortierung hinzufügen" + "addSort": "Sortierung hinzufügen", + "removeSorting": "Möchten Sie die Sortierung entfernen?", + "fieldInUse": "Sie sortieren bereits nach diesem Feld" }, "row": { "duplicate": "Duplikat", @@ -639,8 +748,36 @@ "hideComplete": "Blende abgeschlossene Aufgaben aus", "showComplete": "Zeige alle Aufgaben" }, + "url": { + "launch": "Im Browser öffnen", + "copy": "Webadresse kopieren", + "textFieldHint": "Geben Sie eine URL ein", + "copiedNotification": "In die Zwischenablage kopiert!" + }, + "relation": { + "relatedDatabasePlaceLabel": "Verwandte Datenbank", + "relatedDatabasePlaceholder": "Nichts", + "inRelatedDatabase": "in", + "rowSearchTextFieldPlaceholder": "Suchen", + "noDatabaseSelected": "Keine Datenbank ausgewählt! Bitte wähle zuerst eine Datenbank aus der nachfolgenden Liste aus:", + "emptySearchResult": "Nichts gefunden" + }, "menuName": "Raster", - "referencedGridPrefix": "Sicht von" + "referencedGridPrefix": "Sicht von", + "calculate": "berechnet", + "calculationTypeLabel": { + "none": "nichts", + "average": "Durchschnitt", + "max": "Max", + "median": "Mittelwert", + "min": "Min", + "sum": "Ergebnis", + "count": "Zahl", + "countEmpty": "Zahl leer", + "countEmptyShort": "leer", + "countNonEmpty": "Zahl nicht leer", + "countNonEmptyShort": "nicht leer" + } }, "document": { "menuName": "Dokument", @@ -743,17 +880,21 @@ "left": "Links", "center": "Zentriert", "right": "Rechts", - "defaultColor": "Standard" + "defaultColor": "Standard", + "depth": "Tiefe" }, "image": { "copiedToPasteBoard": "Der Bildlink wurde in die Zwischenablage kopiert", - "addAnImage": "Ein Bild hinzufügen" + "addAnImage": "Ein Bild hinzufügen", + "imageUploadFailed": "Bild hochladen gescheitert" }, "urlPreview": { - "copiedToPasteBoard": "Der Link wurde in die Zwischenablage kopiert" + "copiedToPasteBoard": "Der Link wurde in die Zwischenablage kopiert", + "convertToLink": "Konvertieren zum eingebetteten Link" }, "outline": { - "addHeadingToCreateOutline": "Fügen Sie Überschriften hinzu, um ein Inhaltsverzeichnis zu erstellen." + "addHeadingToCreateOutline": "Fügen Sie Überschriften hinzu, um ein Inhaltsverzeichnis zu erstellen.", + "noMatchHeadings": "Keine passenden Überschriften gefunden." }, "table": { "addAfter": "Danach einfügen", @@ -776,7 +917,12 @@ "toContinue": "fortfahren", "newDatabase": "Neue Datenbank", "linkToDatabase": "Verknüpfung zur Datenbank" - } + }, + "date": "Datum", + "emoji": "Emoji" + }, + "outlineBlock": { + "placeholder": "Inhaltsverzeichnis" }, "textBlock": { "placeholder": "Geben Sie „/“ für Inhaltsblöcke ein" @@ -807,7 +953,8 @@ "invalidImage": "Ungültiges Bild", "invalidImageSize": "Die Bildgröße muss kleiner als 5 MB sein", "invalidImageFormat": "Das Bildformat wird nicht unterstützt. Unterstützte Formate: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "Ungültige Bild-URL" + "invalidImageUrl": "Ungültige Bild-URL", + "noImage": "Keine Datei oder Verzeichnis" }, "embedLink": { "label": "Eingebetteter Link", @@ -822,7 +969,10 @@ "saveImageToGallery": "Bild speichern", "failedToAddImageToGallery": "Das Bild konnte nicht zur Galerie hinzugefügt werden", "successToAddImageToGallery": "Das Bild wurde zur Galerie hinzugefügt werden", - "unableToLoadImage": "Das Bild konnte nicht geladen werden" + "unableToLoadImage": "Das Bild konnte nicht geladen werden", + "maximumImageSize": "Die maximal unterstützte Upload-Bildgröße beträgt 10 MB", + "uploadImageErrorImageSizeTooBig": "Die Bildgröße muss weniger als 10 MB betragen", + "imageIsUploading": "Bild wird hochgeladen" }, "codeBlock": { "language": { @@ -849,7 +999,9 @@ "page": { "label": "Link zur Seite", "tooltip": "Klicken, um die Seite zu öffnen" - } + }, + "deleted": "gelöscht", + "deletedContent": "Dieser Inhalt existiert nicht oder wurde gelöscht" }, "toolbar": { "resetToDefaultFont": "Auf den Standard zurücksetzen" @@ -870,7 +1022,7 @@ "hideColumn": "Verstecken", "newGroup": "Neue Gruppe", "deleteColumn": "Löschen", - "deleteColumnConfirmation": "Das wird diese Gruppe und alle enthaltenen Karten löschen.\nSicher, dass Sie fortsetzen möchte?", + "deleteColumnConfirmation": "Das wird diese Gruppe und alle enthaltenen Karten löschen.\nSicher, dass du fortsetzen möchtest?", "groupActions": "Gruppenaktion" }, "hiddenGroupSection": { @@ -897,7 +1049,7 @@ "editURL": "Bearbeite URL", "unhideGroup": "Zeige die Gruppe", "unhideGroupContent": "Sicher, dass diese Gruppe auf dem Board angezeigt werden soll?", - "faildToLoad": "Board Sicht konnte nicht geladen werden" + "faildToLoad": "Boardansicht konnte nicht geladen werden" } }, "calendar": { @@ -910,6 +1062,10 @@ "previousMonth": "Vorheriger Monat", "nextMonth": "Nächster Monat" }, + "mobileEventScreen": { + "emptyTitle": "Noch keine Events", + "emptyBody": "Drücke die Plus-Taste, um für heute ein Ereignis zu erstellen." + }, "settings": { "showWeekNumbers": "Wochennummern anzeigen", "showWeekends": "Wochenenden anzeigen", @@ -922,16 +1078,17 @@ "one": "{count} Ereignisse ohne Datum", "other": "{count} Ereignisse ohne Datum" }, - "unscheduledEventsTitle": "Unscheduled events", + "unscheduledEventsTitle": "Ungeplante Events", "clickToAdd": "Klicken Sie, um es zum Kalender hinzuzufügen", "name": "Kalendereinstellungen" }, "referencedCalendarPrefix": "Sicht von", - "quickJumpYear": "Spring zu" + "quickJumpYear": "Spring zu", + "duplicateEvent": "Doppeltes Ereignis" }, "errorDialog": { "title": "AppFlowy-Fehler", - "howToFixFallback": "Wir entschuldigen uns für die Unannehmlichkeiten! Reichen Sie auf unserer GitHub-Seite ein Problem ein, das Ihren Fehler beschreibt.", + "howToFixFallback": "Wir entschuldigen uns für die Unannehmlichkeiten! Reiche auf unserer GitHub-Seite ein Problem ein, das Ihren Fehler beschreibt.", "github": "Auf GitHub ansehen" }, "search": { @@ -998,6 +1155,10 @@ "inlineActions": { "noResults": "Keine Ergebnisse", "pageReference": "Seitenreferenz", + "docReference": "Dokumentverweis", + "boardReference": "Board-Referenz", + "calReference": "Kalenderreferenz", + "gridReference": "Gitter Referenz", "date": "Datum", "reminder": { "groupTitle": "Erinnerung", @@ -1010,7 +1171,24 @@ "includeTime": "Inkl. Zeit", "isRange": "Enddatum", "timeFormat": "Zeitformat", - "clearDate": "Datum löschen" + "clearDate": "Datum löschen", + "reminderLabel": "Erinnerung", + "selectReminder": "Erinnerung auswählen", + "reminderOptions": { + "none": "nichts", + "atTimeOfEvent": "Uhrzeit des Events", + "fiveMinsBefore": "5Min. vorher", + "tenMinsBefore": "10Min. vorher", + "fifteenMinsBefore": "15Min. vorher", + "thirtyMinsBefore": "30Min. vorher", + "oneHourBefore": "1Std. vorher", + "twoHoursBefore": "2Std. vorher", + "onDayOfEvent": "Am Tag des Events", + "oneDayBefore": "1Tag vorher", + "twoDaysBefore": "2Tage vorher", + "oneWeekBefore": "1Woche vorher", + "custom": "Benutzerdefiniert" + } }, "relativeDates": { "yesterday": "Gestern", @@ -1024,7 +1202,7 @@ "title": "Neuigkeiten" }, "emptyTitle": "Leer", - "emptyBody": "Keine offenen Benachrichtigungen oder Aktionen. Genießen Sie die Ruhe.", + "emptyBody": "Keine offenen Benachrichtigungen oder Aktionen. Genieße die Ruhe.", "tabs": { "inbox": "Eingang", "upcoming": "Demnächst" @@ -1044,7 +1222,7 @@ }, "reminderNotification": { "title": "Erinnerung", - "message": "Bitte denken Sie daran das hier zu prüfen bevor Sie es vergessen!", + "message": "Bitte denke daran, dass hier zu prüfen bevor du es vergisst.", "tooltipDelete": "Löschen", "tooltipMarkRead": "Als gelesen markieren", "tooltipMarkUnread": "Als ungelesen markieren" @@ -1057,15 +1235,17 @@ "replace": "Ersetzen", "replaceAll": "Alle ersetzen", "noResult": "Keine Ergebnisse", - "caseSensitive": "Groß-/Kleinschreibung beachten" + "caseSensitive": "Groß-/Kleinschreibung beachten", + "searchMore": "Suche für mehr Ergebnisse" }, "error": { "weAreSorry": "Das tut uns leid", - "loadingViewError": "Wir haben Schwierigkeiten diese Ansicht zu laden. Bitte prüfen Sie Ihre Internetverbindung, laden die App neu und zögern Sie nicht, das Team zu kontaktieren, falls das Problem weiterhin bestehen sollte." + "loadingViewError": "Wir haben Schwierigkeiten diese Ansicht zu laden. Bitte prüfe die Internetverbindung, lade die App neu und zögere Sie nicht, dass Team zu kontaktieren, falls das Problem weiterhin besteht." }, "editor": { "bold": "Fett", "bulletedList": "Stichpunktliste", + "bulletedListShortForm": "Mit Aufzählungszeichen", "checkbox": "Checkbox", "embedCode": "Eingebetteter Code", "heading1": "Überschrift 1", @@ -1074,9 +1254,11 @@ "highlight": "Hervorhebung", "color": "Farbe", "image": "Bild", + "date": "Datum", "italic": "Kursiv", "link": "Link", "numberedList": "Nummerierte Liste", + "numberedListShortForm": "Nummeriert", "quote": "Zitat", "strikethrough": "Durgestrichen", "text": "Text", @@ -1101,6 +1283,8 @@ "backgroundColorPurple": "Lila Hintergrund", "backgroundColorPink": "Pinker Hintergrund", "backgroundColorRed": "Roter Hintergrund", + "backgroundColorLime": "Lime-Hintergrund", + "backgroundColorAqua": "Aqua-Hintergrund", "done": "Erledigt", "cancel": "Abbrechen", "tint1": "Farbton 1", @@ -1148,6 +1332,8 @@ "copy": "Kopieren", "paste": "Einfügen", "find": "Finden", + "select": "Auswählen", + "selectAll": "Alle auswählen", "previousMatch": "Vorheriger Treffer", "nextMatch": "Nächster Treffer", "closeFind": "Schließen", @@ -1175,17 +1361,21 @@ "colClear": "Inhalt löschen", "rowClear": "Inhalt löschen", "slashPlaceHolder": "'/'-Taste, um einen Block einzufügen oder Text eingeben", - "typeSomething": "Etwas eingeben..." + "typeSomething": "Etwas eingeben...", + "toggleListShortForm": "Umschalten", + "quoteListShortForm": "Zitat", + "mathEquationShortForm": "Formel", + "codeBlockShortForm": "Code" }, "favorite": { - "noFavorite": "Keine Favoritenseite", + "noFavorite": "Leere Favoritenseite", "noFavoriteHintText": "Nach links wischen, um es den Favoriten hinzuzufügen" }, "cardDetails": { "notesPlaceholder": "'/'-Taste, um einen Block einzufügen oder Text eingeben" }, "blockPlaceholders": { - "todoList": "To-do", + "todoList": "To-Do", "bulletList": "Liste", "numberList": "Liste", "quote": "Zitat", @@ -1199,5 +1389,32 @@ "date": "Datum", "addField": "Ein Feld hinzufügen", "userIcon": "Nutzerbild" + }, + "noLogFiles": "Hier gibt es kein Log-File", + "newSettings": { + "myAccount": { + "title": "Mein Benutzerkonto", + "subtitle": "Passe dein Profil an, verwalte Einstellungen zur Sicherheit deines Benutzerkontos, öffne AI-Schlüssel oder melde dich bei deinem Konto an.", + "profileLabel": "Kontoname und Profilbild", + "profileNamePlaceholder": "Gib deinen Namen ein", + "accountSecurity": "Konto Sicherheit", + "2FA": "Authentifizierung in zwei Schritten", + "aiKeys": "AI-Schlüssel", + "accountLogin": "Benutzerkonto Login", + "updateNameError": "Namensaktualisierung fehlgeschlagen!", + "updateIconError": "Symbol konnte nicht aktualisiert werden!", + "deleteAccount": { + "title": "Benutzerkonto löschen", + "subtitle": "Benutzerkonto inkl. deiner persönlicher Daten unwiderruflich löschen.", + "deleteMyAccount": "Mein Benutzerkonto löschen", + "dialogTitle": "Benutzerkonto löschen", + "dialogContent1": "Bist du sicher, dass du dein Benutzerkonto unwiderruflich löschen möchtest?", + "dialogContent2": "Diese Aktion kann nicht rückgängig gemacht werden und führt dazu, dass der Zugriff auf alle Teambereiche aufgehoben wird, Ihr gesamtes Benutzerkonto, einschließlich privater Arbeitsbereiche, gelöscht wird und Sie aus allen freigegebenen Arbeitsbereichen entfernt werden." + } + }, + "workplace": { + "name": "Arbeitsbereich", + "title": "Arbeitsbereichseinstellungen" + } } } diff --git a/frontend/resources/translations/el-GR.json b/frontend/resources/translations/el-GR.json new file mode 100644 index 0000000000..3c57a4db1e --- /dev/null +++ b/frontend/resources/translations/el-GR.json @@ -0,0 +1,1411 @@ +{ + "appName": "AppFlowy", + "defaultUsername": "Me", + "welcomeText": "Καλωσορίσατε στο @:appName", + "welcomeTo": "Καλωσορίσατε στο", + "githubStarText": "Star on GitHub", + "subscribeNewsletterText": "Εγγραφείτε στο Newsletter", + "letsGoButtonText": "Γρήγορη Εκκίνηση", + "title": "Τίτλος", + "youCanAlso": "Μπορείτε επίσης", + "and": "και", + "failedToOpenUrl": "Αποτυχία ανοίγματος διεύθυνσης url: {}", + "blockActions": { + "addBelowTooltip": "Κάντε κλικ για να προσθέσετε παρακάτω", + "addAboveCmd": "Alt+click", + "addAboveMacCmd": "Option+click", + "addAboveTooltip": "για να προσθέσετε παραπάνω", + "dragTooltip": "Σύρετε για μετακίνηση", + "openMenuTooltip": "Κάντε κλικ για άνοιγμα μενού" + }, + "signUp": { + "buttonText": "Εγγραφή", + "title": "Εγγραφείτε στο @:appName", + "getStartedText": "Ξεκινήστε", + "emptyPasswordError": "Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός", + "repeatPasswordEmptyError": "Η επανάληψη κωδικού πρόσβασης δεν μπορεί να είναι κενή", + "unmatchedPasswordError": "Η επανάληψη κωδικού πρόσβασης δεν είναι ίδια με τον κωδικό πρόσβασης", + "alreadyHaveAnAccount": "Έχετε ήδη λογαριασμό;", + "emailHint": "Email", + "passwordHint": "Κωδικός", + "repeatPasswordHint": "Επαναλάβετε τον κωδικό πρόσβασης", + "signUpWith": "Εγγραφή με:" + }, + "signIn": { + "loginTitle": "Συνδεθείτε στο @:appName", + "loginButtonText": "Σύνδεση", + "loginStartWithAnonymous": "Έναρξη με ανώνυμη συνεδρία", + "continueAnonymousUser": "Συνέχεια με ανώνυμη συνεδρία", + "buttonText": "Είσοδος", + "signingInText": "Πραγματοποιείται σύνδεση...", + "forgotPassword": "Ξεχάσατε το κωδικό;", + "emailHint": "Email", + "passwordHint": "Κωδικός", + "dontHaveAnAccount": "Δεν έχετε λογαριασμό;", + "repeatPasswordEmptyError": "Η επανάληψη κωδικού πρόσβασης δεν μπορεί να είναι κενή", + "unmatchedPasswordError": "Η επανάληψη κωδικού πρόσβασης δεν είναι ίδια με τον κωδικό πρόσβασης", + "syncPromptMessage": "Ο συγχρονισμός των δεδομένων μπορεί να διαρκέσει λίγο. Παρακαλώ μην κλείσετε αυτήν τη σελίδα", + "or": "- Ή -", + "LogInWithGoogle": "Σύνδεση μέσω Google", + "LogInWithGithub": "Σύνδεση μέσω Github", + "LogInWithDiscord": "Σύνδεση μέσω Discord", + "signInWith": "Συνδεθείτε με:" + }, + "workspace": { + "chooseWorkspace": "Επιλέξτε το χώρο εργασίας σας", + "create": "Δημιουργία χώρου εργασίας", + "reset": "Επαναφορά χώρου εργασίας", + "resetWorkspacePrompt": "Η επαναφορά του χώρου εργασίας θα διαγράψει όλες τις σελίδες και τα δεδομένα μέσα σε αυτό. Είστε βέβαιοι ότι θέλετε να επαναφέρετε το χώρο εργασίας? Εναλλακτικά, μπορείτε να επικοινωνήσετε με την ομάδα υποστήριξης για να επαναφέρετε το χώρο εργασίας", + "hint": "workspace", + "notFoundError": "Workspace not found", + "failedToLoad": "Something went wrong! Failed to load the workspace. Try to close any open instance of AppFlowy and try again.", + "errorActions": { + "reportIssue": "Report an issue", + "reportIssueOnGithub": "Report an issue on Github", + "exportLogFiles": "Export log files", + "reachOut": "Reach out on Discord" + }, + "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.", + "createSuccess": "Workspace created successfully", + "createFailed": "Failed to create workspace", + "deleteSuccess": "Workspace deleted successfully", + "deleteFailed": "Failed to delete workspace", + "openSuccess": "Open workspace successfully", + "openFailed": "Failed to open workspace", + "renameSuccess": "Workspace renamed successfully", + "renameFailed": "Failed to rename workspace", + "updateIconSuccess": "Updated workspace icon successfully", + "updateIconFailed": "Updated workspace icon failed" + }, + "shareAction": { + "buttonText": "Share", + "workInProgress": "Coming soon", + "markdown": "Markdown", + "csv": "CSV", + "copyLink": "Copy Link" + }, + "moreAction": { + "small": "small", + "medium": "medium", + "large": "large", + "fontSize": "Font size", + "import": "Import", + "moreOptions": "More options", + "wordCount": "Word count: {}", + "charCount": "Character count: {}", + "createdAt": "Created: {}", + "deleteView": "Delete", + "duplicateView": "Duplicate" + }, + "importPanel": { + "textAndMarkdown": "Text & Markdown", + "documentFromV010": "Document from v0.1.0", + "databaseFromV010": "Database from v0.1.0", + "csv": "CSV", + "database": "Database" + }, + "disclosureAction": { + "rename": "Rename", + "delete": "Delete", + "duplicate": "Duplicate", + "unfavorite": "Remove from favorites", + "favorite": "Προσθήκη στα αγαπημένα", + "openNewTab": "Άνοιγμα σε νέα καρτέλα", + "moveTo": "Μετακίνηση στο", + "addToFavorites": "Προσθήκη στα Αγαπημένα", + "copyLink": "Αντιγραφή Συνδέσμου" + }, + "blankPageTitle": "Κενή σελίδα", + "newPageText": "Νέα σελίδα", + "newDocumentText": "Νέο έγγραφο", + "newGridText": "Νέο πλέγμα", + "newCalendarText": "Νέο ημερολόγιο", + "newBoardText": "Νέος πίνακας", + "trash": { + "text": "Κάδος απορριμμάτων", + "restoreAll": "Επαναφορά Όλων", + "deleteAll": "Διαγραφή Όλων", + "pageHeader": { + "fileName": "Όνομα αρχείου", + "lastModified": "Τελευταία Τροποποίηση", + "created": "Δημιουργήθηκε" + }, + "confirmDeleteAll": { + "title": "Είστε βέβαιοι οτι θέλετε να διαγράψετε όλες τις σελίδες στον κάδο απορριμμάτων;", + "caption": "Αυτή η ενέργεια δεν μπορεί να ανεραιθεί." + }, + "confirmRestoreAll": { + "title": "Είστε βέβαιοι οτι θέλετε να επαναφέρετε όλες τις σελίδες στον κάδο απορριμμάτων;", + "caption": "Αυτή η ενέργεια δεν μπορεί να ανεραιθεί." + }, + "mobile": { + "actions": "Ενέργειες Απορριμμάτων", + "empty": "Ο κάδος απορριμμάτων είναι άδειος", + "emptyDescription": "Δεν έχετε διαγράψει κανένα αρχείο", + "isDeleted": "έχει διαγραφεί", + "isRestored": "έγινε επαναφορά" + } + }, + "deletePagePrompt": { + "text": "Αυτή η σελίδα βρίσκεται στον κάδο απορριμμάτων", + "restore": "Επαναφορά σελίδας", + "deletePermanent": "Οριστική διαγραφή" + }, + "dialogCreatePageNameHint": "Όνομα σελίδας", + "questionBubble": { + "shortcuts": "Συντομεύσεις", + "whatsNew": "Τι νέο υπάρχει;", + "help": "Βοήθεια & Υποστήριξη", + "markdown": "Markdown", + "debug": { + "name": "Debug Info", + "success": "Copied debug info to clipboard!", + "fail": "Unable to copy debug info to clipboard" + }, + "feedback": "Σχόλια" + }, + "menuAppHeader": { + "moreButtonToolTip": "Αφαίρεση, μετονομασία και άλλα...", + "addPageTooltip": "Γρήγορη προσθήκη σελίδας", + "defaultNewPageName": "Χωρίς τίτλο", + "renameDialog": "Μετονομασία" + }, + "noPagesInside": "Δεν υπάρχουν σελίδες", + "toolbar": { + "undo": "Αναίρεση", + "redo": "Επαναφορά", + "bold": "Έντονo", + "italic": "Πλάγια", + "underline": "Υπογράμμιση", + "strike": "Διακριτή διαγραφή", + "numList": "Αριθμημένη λίστα", + "bulletList": "Bulleted List", + "checkList": "Check List", + "inlineCode": "Inline Code", + "quote": "Quote Block", + "header": "Header", + "highlight": "Highlight", + "color": "Color", + "addLink": "Add Link", + "link": "Link" + }, + "tooltip": { + "lightMode": "Switch to Light mode", + "darkMode": "Switch to Dark mode", + "openAsPage": "Open as a Page", + "addNewRow": "Add a new row", + "openMenu": "Click to open menu", + "dragRow": "Long press to reorder the row", + "viewDataBase": "View database", + "referencePage": "This {name} is referenced", + "addBlockBelow": "Add a block below" + }, + "sideBar": { + "closeSidebar": "Close side bar", + "openSidebar": "Open side bar", + "personal": "Personal", + "favorites": "Favorites", + "clickToHidePersonal": "Click to hide personal section", + "clickToHideFavorites": "Click to hide favorite section", + "addAPage": "Add a page", + "recent": "Recent" + }, + "notifications": { + "export": { + "markdown": "Exported Note To Markdown", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "Contacts", + "whatsHappening": "What's happening this week?", + "addContact": "Add Contact", + "editContact": "Edit Contact" + }, + "button": { + "ok": "OK", + "done": "Done", + "cancel": "Cancel", + "signIn": "Sign In", + "signOut": "Sign Out", + "complete": "Complete", + "save": "Save", + "generate": "Generate", + "esc": "ESC", + "keep": "Keep", + "tryAgain": "Try again", + "discard": "Discard", + "replace": "Replace", + "insertBelow": "Insert below", + "insertAbove": "Εισαγωγή από επάνω", + "upload": "Μεταφόρτωση", + "edit": "Επεξεργασία", + "delete": "Διαγραφή", + "duplicate": "Δημιουργία διπλότυπου", + "putback": "Βάλτε Πίσω", + "update": "Ενημέρωση", + "share": "Κοινοποίηση", + "removeFromFavorites": "Κατάργηση από τα αγαπημένα", + "addToFavorites": "Προσθήκη στα αγαπημένα", + "rename": "Μετονομασία", + "helpCenter": "Κέντρο Βοήθειας", + "add": "Προσθήκη", + "yes": "Ναι", + "clear": "Καθαρισμός", + "remove": "Αφαίρεση", + "dontRemove": "Να μην αφαιρεθεί", + "copyLink": "Αντιγραφή Συνδέσμου", + "align": "Στοίχιση", + "login": "Σύνδεση", + "logout": "Αποσύνδεση", + "deleteAccount": "Διαγραφή λογαριασμού", + "back": "Πίσω", + "signInGoogle": "Συνδεθείτε μέσω λογαριασμού Google", + "signInGithub": "Συνδεθείτε μέσω λογαριασμού Github", + "signInDiscord": "Συνδεθείτε μέσω λογαριασμού Discord" + }, + "label": { + "welcome": "Καλώς ήρθατε!", + "firstName": "Όνομα", + "middleName": "Μεσαίο όνομα", + "lastName": "Επώνυμο", + "stepX": "Step {X}" + }, + "oAuth": { + "err": { + "failedTitle": "Αδυναμία σύνδεσης στο λογαριασμό σας.", + "failedMsg": "Παρακαλούμε βεβαιωθείτε ότι έχετε ολοκληρώσει τη διαδικασία εισόδου στο πρόγραμμα περιήγησης." + }, + "google": { + "title": "GOOGLE SIGN-IN", + "instruction1": "Για να εισαγάγετε τις Επαφές Google σας, θα πρέπει να εξουσιοδοτήσετε αυτήν την εφαρμογή χρησιμοποιώντας το πρόγραμμα περιήγησής σας.", + "instruction2": "Αντιγράψτε αυτόν τον κώδικα στο πρόχειρο κάνοντας κλικ στο εικονίδιο ή επιλέγοντας το κείμενο:", + "instruction3": "Μεταβείτε στον ακόλουθο σύνδεσμο στο πρόγραμμα περιήγησής σας και πληκτρολογήστε τον παραπάνω κωδικό:", + "instruction4": "Πατήστε το κουμπί παρακάτω όταν ολοκληρώσετε την εγγραφή:" + } + }, + "settings": { + "title": "Ρυθμίσεις", + "menu": { + "appearance": "Εμφάνιση", + "language": "Γλώσσα", + "user": "Χρήστης", + "files": "Αρχεία", + "notifications": "Ειδοποιήσεις", + "open": "Άνοιγμα Ρυθμίσεων", + "logout": "Αποσυνδέση", + "logoutPrompt": "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε;", + "selfEncryptionLogoutPrompt": "Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε; Παρακαλούμε βεβαιωθείτε ότι έχετε αντιγράψει το κρυπτογραφημένο μυστικό", + "syncSetting": "Ρυθμίσεις συγχρονισμού", + "cloudSettings": "Ρυθμίσεις Cloud", + "enableSync": "Enable sync", + "enableEncrypt": "Encrypt data", + "cloudURL": "Base URL", + "invalidCloudURLScheme": "Invalid Scheme", + "cloudServerType": "Cloud server", + "cloudServerTypeTip": "Please note that it might log out your current account after switching the cloud server", + "cloudLocal": "Local", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "The supabase url can't be empty", + "cloudSupabaseAnonKey": "Supabase anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "The anon key can't be empty", + "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowySelfHost": "AppFlowy Cloud Self-hosted", + "appFlowyCloudUrlCanNotBeEmpty": "The cloud url can't be empty", + "clickToCopy": "Click to copy", + "selfHostStart": "If you don't have a server, please refer to the", + "selfHostContent": "document", + "selfHostEnd": "for guidance on how to self-host your own server", + "cloudURLHint": "Input the base URL of your server", + "cloudWSURL": "Websocket URL", + "cloudWSURLHint": "Input the websocket address of your server", + "restartApp": "Restart", + "restartAppTip": "Restart the application for the changes to take effect. Please note that this might log out your current account", + "changeServerTip": "After changing the server, you must click the restart button for the changes to take effect", + "enableEncryptPrompt": "Activate encryption to secure your data with this secret. Store it safely; once enabled, it can't be turned off. If lost, your data becomes irretrievable. Click to copy", + "inputEncryptPrompt": "Please enter your encryption secret for", + "clickToCopySecret": "Click to copy secret", + "configServerSetting": "Configurate your server settings", + "configServerGuide": "After selecting `Quick Start`, navigate to `Settings` and then \"Cloud Settings\" to configure your self-hosted server.", + "inputTextFieldHint": "Your secret", + "historicalUserList": "User login history", + "historicalUserListTooltip": "This list displays your anonymous accounts. You can click on an account to view its details. Anonymous accounts are created by clicking the 'Get Started' button", + "openHistoricalUser": "Click to open the anonymous account", + "customPathPrompt": "Storing the AppFlowy data folder in a cloud-synced folder such as Google Drive can pose risks. If the database within this folder is accessed or modified from multiple locations at the same time, it may result in synchronization conflicts and potential data corruption", + "importAppFlowyData": "Import Data from External AppFlowy Folder", + "importingAppFlowyDataTip": "Data import is in progress. Please do not close the app", + "importAppFlowyDataDescription": "Copy data from an external AppFlowy data folder and import it into the current AppFlowy data folder", + "importSuccess": "Successfully imported the AppFlowy data folder", + "importFailed": "Importing the AppFlowy data folder failed", + "importGuide": "For further details, please check the referenced document" + }, + "notifications": { + "enableNotifications": { + "label": "Enable notifications", + "hint": "Turn off to stop local notifications from appearing." + } + }, + "appearance": { + "resetSetting": "Reset", + "fontFamily": { + "label": "Font Family", + "search": "Search" + }, + "themeMode": { + "label": "Theme Mode", + "light": "Light Mode", + "dark": "Σκοτεινό Θέμα", + "system": "Προσαρμογή στο σύστημα" + }, + "fontScaleFactor": "Font Scale Factor", + "documentSettings": { + "cursorColor": "Χρώμα κέρσορα εγγράφου", + "selectionColor": "Χρώμα επιλογής κειμένου", + "hexEmptyError": "Το χρώμα σε δεκαεξαδική μορφή δεν μπορεί να είναι κενό", + "hexLengthError": "Η τιμή δεκαεξαδικού πρέπει να είναι 6 ψηφία", + "hexInvalidError": "Μη έγκυρη τιμή δεκαεξαδικού", + "opacityEmptyError": "Η διαφάνεια δεν μπορεί να είναι κενή", + "opacityRangeError": "Η διαφάνεια πρέπει να είναι μεταξύ 1 και 100", + "app": "Εφαρμογή", + "flowy": "Flowy", + "apply": "Apply" + }, + "layoutDirection": { + "label": "Κατεύθυνση Διάταξης", + "hint": "Ελέγξτε τη ροή του περιεχομένου στην οθόνη σας, από αριστερά προς τα δεξιά ή δεξιά προς τα αριστερά.", + "ltr": "LTR", + "rtl": "RTL" + }, + "textDirection": { + "label": "Προεπιλεγμένη κατεύθυνση κειμένου", + "hint": "Καθορίστε αν το κείμενο θα ξεκινά από αριστερά ή δεξιά ως προεπιλογή.", + "ltr": "LTR", + "rtl": "RTL", + "auto": "AUTO", + "fallback": "Ίδια με την κατεύθυνση διάταξης" + }, + "themeUpload": { + "button": "Μεταφόρτωση", + "uploadTheme": "Μεταφόρτωση θέματος", + "description": "Ανεβάστε το δικό σας θέμα για το AppFlowy χρησιμοποιώντας το παρακάτω κουμπί.", + "loading": "Παρακαλώ περιμένετε ενώ επικυρώνουμε και ανεβάζουμε το θέμα σας...", + "uploadSuccess": "Το θέμα σας μεταφορτώθηκε με επιτυχία", + "deletionFailure": "Αποτυχία διαγραφής του θέματος. Προσπαθήστε να το διαγράψετε χειροκίνητα.", + "filePickerDialogTitle": "Επιλέξτε ένα αρχείο .flowy_plugin", + "urlUploadFailure": "Αποτυχία ανοίγματος url: {}" + }, + "theme": "Θέμα", + "builtInsLabel": "Ενσωματωμένα Θέματα", + "pluginsLabel": "Πρόσθετα", + "dateFormat": { + "label": "Μορφή ημερομηνίας", + "local": "Τοπική", + "us": "US", + "iso": "ISO", + "friendly": "Friendly", + "dmy": "D/M/Y" + }, + "timeFormat": { + "label": "Μορφή ώρας", + "twelveHour": "12 ώρες", + "twentyFourHour": "24 ώρες" + }, + "showNamingDialogWhenCreatingPage": "Εμφάνιση διαλόγου ονομασίας κατά τη δημιουργία μιας σελίδας", + "enableRTLToolbarItems": "Enable RTL toolbar items", + "members": { + "title": "Members Settings", + "inviteMembers": "Πρόσκληση Μέλους", + "sendInvite": "Αποστολή Πρόσκλησης", + "copyInviteLink": "Αντιγραφή Συνδέσμου Πρόσκλησης", + "label": "Μέλη", + "user": "User", + "role": "Role", + "removeFromWorkspace": "Remove from Workspace", + "owner": "Owner", + "guest": "Guest", + "member": "Member", + "memberHintText": "A member can read, comment, and edit pages. Invite members and guests.", + "guestHintText": "A Guest can read, react, comment, and can edit certain pages with permission.", + "emailInvalidError": "Invalid email, please check and try again", + "emailSent": "Email sent, please check the inbox", + "members": "members" + } + }, + "files": { + "copy": "Copy", + "defaultLocation": "Read files and data storage location", + "exportData": "Export your data", + "doubleTapToCopy": "Double tap to copy the path", + "restoreLocation": "Restore to AppFlowy default path", + "customizeLocation": "Open another folder", + "restartApp": "Please restart app for the changes to take effect.", + "exportDatabase": "Export database", + "selectFiles": "Select the files that need to be export", + "selectAll": "Select all", + "deselectAll": "Deselect all", + "createNewFolder": "Create a new folder", + "createNewFolderDesc": "Tell us where you want to store your data", + "defineWhereYourDataIsStored": "Define where your data is stored", + "open": "Open", + "openFolder": "Open an existing folder", + "openFolderDesc": "Read and write it to your existing AppFlowy folder", + "folderHintText": "folder name", + "location": "Creating a new folder", + "locationDesc": "Pick a name for your AppFlowy data folder", + "browser": "Browse", + "create": "Create", + "set": "Set", + "folderPath": "Path to store your folder", + "locationCannotBeEmpty": "Path cannot be empty", + "pathCopiedSnackbar": "File storage path copied to clipboard!", + "changeLocationTooltips": "Change the data directory", + "change": "Αλλαγή", + "openLocationTooltips": "Open another data directory", + "openCurrentDataFolder": "Άνοιγμα του τρέχοντος φακέλου", + "recoverLocationTooltips": "Reset to AppFlowy's default data directory", + "exportFileSuccess": "Επιτυχής εξαγωγή αρχείου!", + "exportFileFail": "Η εξαγωγή αρχείου απέτυχε!", + "export": "Εξαγωγή", + "clearCache": "Εκκαθάριση προσωρινής μνήμης", + "clearCacheDesc": "Αν αντιμετωπίζετε προβλήματα με εικόνες που δεν φορτώνουν ή γραμματοσειρές που δεν εμφανίζονται σωστά, δοκιμάστε να καθαρίσετε την προσωρινή μνήμη. Αυτή η ενέργεια δεν θα διαγράψει τα δεδομένα χρήστη σας.", + "areYouSureToClearCache": "Σίγουρα θέλετε να καθαρίσετε την προσωρινή μνήμη;", + "clearCacheSuccess": "Επιτυχής εκκαθάριση προσωρινής μνήμης!" + }, + "user": { + "name": "Όνομα", + "email": "Email", + "tooltipSelectIcon": "Select icon", + "selectAnIcon": "Select an icon", + "pleaseInputYourOpenAIKey": "παρακαλώ εισάγετε το OpenAI κλειδί σας", + "pleaseInputYourStabilityAIKey": "παρακαλώ εισάγετε το Stability AI κλειδί σας", + "clickToLogout": "Κάντε κλικ για αποσύνδεση του τρέχοντος χρήστη" + }, + "shortcuts": { + "shortcutsLabel": "Συντομεύσεις", + "command": "Command", + "keyBinding": "Keybinding", + "addNewCommand": "Προσθήκη Νέας Εντολής", + "updateShortcutStep": "Πατήστε τον επιθυμητό συνδυασμό πλήκτρων και πατήστε ENTER", + "shortcutIsAlreadyUsed": "Αυτή η συντόμευση χρησιμοποιείται ήδη για: {conflict}", + "resetToDefault": "Επαναφορά προεπιλεγμένων συντομεύσεων πληκτρολογίου", + "couldNotLoadErrorMsg": "Αδυναμία φόρτωσης συντομεύσεων, Προσπαθήστε ξανά", + "couldNotSaveErrorMsg": "Δεν ήταν δυνατή η αποθήκευση συντομεύσεων, Προσπαθήστε ξανά" + }, + "mobile": { + "personalInfo": "Προσωπικά Στοιχεία", + "username": "Όνομα Χρήστη", + "usernameEmptyError": "Το όνομα χρήστη δεν μπορεί να είναι κενό", + "about": "Σχετικά", + "pushNotifications": "Ειδοποιήσεις Push", + "support": "Υποστήριξη", + "joinDiscord": "Ελάτε μαζί μας στο Discord", + "privacyPolicy": "Πολιτική Απορρήτου", + "userAgreement": "Όροι Χρήσης", + "termsAndConditions": "Όροι και Προϋποθέσεις", + "userprofileError": "Αποτυχία φόρτωσης προφίλ χρήστη", + "userprofileErrorDescription": "Παρακαλώ προσπαθήστε να αποσυνδεθείτε και να συνδεθείτε ξανά για να ελέγξετε αν το πρόβλημα εξακολουθεί να υπάρχει.", + "selectLayout": "Επιλέξτε διάταξη", + "selectStartingDay": "Επιλέξτε ημέρα έναρξης", + "version": "Έκδοση" + } + }, + "grid": { + "deleteView": "Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτήν τη προβολή;", + "createView": "Νέο", + "title": { + "placeholder": "Χωρίς τίτλο" + }, + "settings": { + "filter": "Φίλτρο", + "sort": "Ταξινόμηση", + "sortBy": "Ταξινόμηση κατά", + "properties": "Properties", + "reorderPropertiesTooltip": "Drag to reorder properties", + "group": "Group", + "addFilter": "Add Filter", + "deleteFilter": "Delete filter", + "filterBy": "Filter by...", + "typeAValue": "Type a value...", + "layout": "Layout", + "databaseLayout": "Layout", + "viewList": { + "zero": "0 views", + "one": "{count} view", + "other": "{count} views" + }, + "editView": "Edit View", + "boardSettings": "Board settings", + "calendarSettings": "Calendar settings", + "createView": "New view", + "duplicateView": "Duplicate view", + "deleteView": "Delete view", + "numberOfVisibleFields": "{} shown" + }, + "textFilter": { + "contains": "Contains", + "doesNotContain": "Does not contain", + "endsWith": "Ends with", + "startWith": "Starts with", + "is": "Is", + "isNot": "Is not", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty", + "choicechipPrefix": { + "isNot": "Not", + "startWith": "Starts with", + "endWith": "Ends with", + "isEmpty": "is empty", + "isNotEmpty": "is not empty" + } + }, + "checkboxFilter": { + "isChecked": "Checked", + "isUnchecked": "Unchecked", + "choicechipPrefix": { + "is": "is" + } + }, + "checklistFilter": { + "isComplete": "is complete", + "isIncomplted": "is incomplete" + }, + "selectOptionFilter": { + "is": "Is", + "isNot": "Is not", + "contains": "Contains", + "doesNotContain": "Does not contain", + "isEmpty": "Είναι κενό", + "isNotEmpty": "Δεν είναι κενό" + }, + "dateFilter": { + "is": "Is", + "before": "Is before", + "after": "Is after", + "onOrBefore": "Is on or before", + "onOrAfter": "Is on or after", + "between": "Is between", + "empty": "Είναι κενό", + "notEmpty": "Δεν είναι κενό", + "choicechipPrefix": { + "before": "Before", + "after": "After", + "onOrBefore": "On or before", + "onOrAfter": "On or after", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty" + } + }, + "numberFilter": { + "equal": "Equals", + "notEqual": "Δεν ισούται με", + "lessThan": "Είναι μικρότερο από", + "greaterThan": "Είναι μεγαλύτερο από", + "lessThanOrEqualTo": "Είναι μικρότερο από ή ίσο με", + "greaterThanOrEqualTo": "Είναι μεγαλύτερο από ή ίσο με", + "isEmpty": "Είναι κενό", + "isNotEmpty": "Δεν είναι κενό" + }, + "field": { + "hide": "Απόκρυψη", + "show": "Εμφάνιση", + "insertLeft": "Εισαγωγή από αριστερά", + "insertRight": "Εισαγωγή από δεξιά", + "duplicate": "Διπλότυπο", + "delete": "Διαγραφή", + "textFieldName": "Κείμενο", + "checkboxFieldName": "Checkbox", + "dateFieldName": "Date", + "updatedAtFieldName": "Τελευταία τροποποίηση", + "createdAtFieldName": "Δημιουργήθηκε στις", + "numberFieldName": "Numbers", + "singleSelectFieldName": "Επιλογή", + "multiSelectFieldName": "Multiselect", + "urlFieldName": "URL", + "checklistFieldName": "Checklist", + "relationFieldName": "Relation", + "numberFormat": "Μορφή αριθμού", + "dateFormat": "Μορφή ημερομηνίας", + "includeTime": "Περιλαμβάνει χρόνο", + "isRange": "End date", + "dateFormatFriendly": "Μήνας Ημέρα, Έτος", + "dateFormatISO": "Έτος-Μήνας-Ημέρα", + "dateFormatLocal": "Μήνας/Ημέρα/Έτος", + "dateFormatUS": "Έτος/Μήνας/Ημέρα", + "dateFormatDayMonthYear": "Ημέρα/Μήνας/Έτος", + "timeFormat": "Time format", + "invalidTimeFormat": "Invalid format", + "timeFormatTwelveHour": "12 hour", + "timeFormatTwentyFourHour": "24 hour", + "clearDate": "Clear date", + "dateTime": "Date time", + "startDateTime": "Start date time", + "endDateTime": "End date time", + "failedToLoadDate": "Failed to load date value", + "selectTime": "Select time", + "selectDate": "Select date", + "visibility": "Visibility", + "propertyType": "Property type", + "addSelectOption": "Add an option", + "typeANewOption": "Type a new option", + "optionTitle": "Options", + "addOption": "Add option", + "editProperty": "Edit property", + "newProperty": "New property", + "deleteFieldPromptMessage": "Are you sure? This property will be deleted", + "newColumn": "New Column", + "format": "Format", + "reminderOnDateTooltip": "This cell has a scheduled reminder", + "optionAlreadyExist": "Option already exists" + }, + "rowPage": { + "newField": "Add a new field", + "fieldDragElementTooltip": "Click to open menu", + "showHiddenFields": { + "one": "Show {count} hidden field", + "many": "Show {count} hidden fields", + "other": "Show {count} hidden fields" + }, + "hideHiddenFields": { + "one": "Απόκρυψη {count} κρυφού πεδίου", + "many": "Απόκρυψη {count} κρυφών πεδίων", + "other": "Απόκρυψη {count} κρυφών πεδίων" + } + }, + "sort": { + "ascending": "Αύξουσα", + "descending": "Φθίνουσα", + "by": "By", + "empty": "No active sorts", + "cannotFindCreatableField": "Αδυναμία εύρεσης κατάλληλου πεδίου για ταξινόμηση", + "deleteAllSorts": "Delete all sorts", + "addSort": "Add new sort", + "removeSorting": "Θα θέλατε να αφαιρέσετε τη ταξινόμηση;", + "fieldInUse": "You are already sorting by this field" + }, + "row": { + "duplicate": "Duplicate", + "delete": "Διαγραφή στήλης", + "titlePlaceholder": "Χωρίς τίτλο", + "textPlaceholder": "Άδειο", + "copyProperty": "Copied property to clipboard", + "count": "Count", + "newRow": "Νέα γραμμή", + "action": "Action", + "add": "Click add to below", + "drag": "Σύρετε για μετακίνηση", + "dragAndClick": "Drag to move, click to open menu", + "insertRecordAbove": "Εισαγωγή εγγραφής επάνω", + "insertRecordBelow": "Εισαγωγή εγγραφής κάτω" + }, + "selectOption": { + "create": "Δημιουργία", + "purpleColor": "Μωβ", + "pinkColor": "Ροζ", + "lightPinkColor": "Απαλό ροζ", + "orangeColor": "Πορτοκαλί", + "yellowColor": "Κίτρινο", + "limeColor": "Λάιμ", + "greenColor": "Πράσινο", + "aquaColor": "Θαλασσί", + "blueColor": "Μπλέ", + "deleteTag": "Διαγραφή ετικέτας", + "colorPanelTitle": "Χρώμα", + "panelTitle": "Select an option or create one", + "searchOption": "Search for an option", + "searchOrCreateOption": "Search or create an option...", + "createNew": "Δημιουργία νέας", + "orSelectOne": "Or select an option", + "typeANewOption": "Type a new option", + "tagName": "Όνομα ετικέτας" + }, + "checklist": { + "taskHint": "Περιγραφή εργασίας", + "addNew": "Προσθήκη νέας εργασίας", + "submitNewTask": "Δημιουργία", + "hideComplete": "Απόκρυψη ολοκληρωμένων εργασιών", + "showComplete": "Εμφάνιση όλων των εργασιών" + }, + "url": { + "launch": "Άνοιγμα συνδέσμου στο πρόγραμμα περιήγησης", + "copy": "Copy link to clipboard", + "textFieldHint": "Enter a URL", + "copiedNotification": "Copied to clipboard!" + }, + "relation": { + "relatedDatabasePlaceLabel": "Related Database", + "relatedDatabasePlaceholder": "None", + "inRelatedDatabase": "In", + "rowSearchTextFieldPlaceholder": "Search", + "noDatabaseSelected": "No database selected, please select one first from the list below:", + "emptySearchResult": "No records found" + }, + "menuName": "Grid", + "referencedGridPrefix": "View of", + "calculate": "Calculate", + "calculationTypeLabel": { + "none": "None", + "average": "Average", + "max": "Max", + "median": "Median", + "min": "Min", + "sum": "Sum", + "count": "Count", + "countEmpty": "Count empty", + "countEmptyShort": "EMPTY", + "countNonEmpty": "Count not empty", + "countNonEmptyShort": "FILLED" + } + }, + "document": { + "menuName": "Document", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "Select a Board to link to", + "createANewBoard": "Create a new Board" + }, + "grid": { + "selectAGridToLinkTo": "Select a Grid to link to", + "createANewGrid": "Create a new Grid" + }, + "calendar": { + "selectACalendarToLinkTo": "Select a Calendar to link to", + "createANewCalendar": "Create a new Calendar" + }, + "document": { + "selectADocumentToLinkTo": "Select a Document to link to" + } + }, + "selectionMenu": { + "outline": "Outline", + "codeBlock": "Code Block" + }, + "plugins": { + "referencedBoard": "Referenced Board", + "referencedGrid": "Referenced Grid", + "referencedCalendar": "Referenced Calendar", + "referencedDocument": "Referenced Document", + "autoGeneratorMenuItemName": "OpenAI Writer", + "autoGeneratorTitleName": "OpenAI: Ask AI to write anything...", + "autoGeneratorLearnMore": "Μάθετε περισσότερα", + "autoGeneratorGenerate": "Generate", + "autoGeneratorHintText": "Ρωτήστε Το OpenAI ...", + "autoGeneratorCantGetOpenAIKey": "Αδυναμία λήψης κλειδιού OpenAI", + "autoGeneratorRewrite": "Rewrite", + "smartEdit": "AI Assistants", + "openAI": "OpenAI", + "smartEditFixSpelling": "Διόρθωση ορθογραφίας", + "warning": "⚠️ Οι απαντήσεις AI μπορεί να είναι ανακριβείς ή παραπλανητικές.", + "smartEditSummarize": "Summarize", + "smartEditImproveWriting": "Improve writing", + "smartEditMakeLonger": "Make longer", + "smartEditCouldNotFetchResult": "Could not fetch result from OpenAI", + "smartEditCouldNotFetchKey": "Could not fetch OpenAI key", + "smartEditDisabled": "Connect OpenAI in Settings", + "discardResponse": "Do you want to discard the AI responses?", + "createInlineMathEquation": "Create equation", + "fonts": "Γραμματοσειρές", + "toggleList": "Toggle list", + "quoteList": "Quote list", + "numberedList": "Αριθμημένη λίστα", + "bulletedList": "Bulleted list", + "todoList": "Todo List", + "callout": "Callout", + "cover": { + "changeCover": "Change Cover", + "colors": "Χρώματα", + "images": "Εικόνες", + "clearAll": "Εκκαθάριση όλων", + "abstract": "Abstract", + "addCover": "Προσθέστε ένα εξώφυλλο", + "addLocalImage": "Add local image", + "invalidImageUrl": "Μη έγκυρο URL εικόνας", + "failedToAddImageToGallery": "Failed to add image to gallery", + "enterImageUrl": "Enter image URL", + "add": "Add", + "back": "Back", + "saveToGallery": "Save to gallery", + "removeIcon": "Remove icon", + "pasteImageUrl": "Paste image URL", + "or": "OR", + "pickFromFiles": "Pick from files", + "couldNotFetchImage": "Could not fetch image", + "imageSavingFailed": "Image Saving Failed", + "addIcon": "Add icon", + "changeIcon": "Change icon", + "coverRemoveAlert": "It will be removed from cover after it is deleted.", + "alertDialogConfirmation": "Are you sure, you want to continue?" + }, + "mathEquation": { + "name": "Math Equation", + "addMathEquation": "Add a TeX equation", + "editMathEquation": "Edit Math Equation" + }, + "optionAction": { + "click": "Click", + "toOpenMenu": " to open menu", + "delete": "Delete", + "duplicate": "Duplicate", + "turnInto": "Turn into", + "moveUp": "Move up", + "moveDown": "Move down", + "color": "Color", + "align": "Align", + "left": "Left", + "center": "Center", + "right": "Right", + "defaultColor": "Default", + "depth": "Depth" + }, + "image": { + "copiedToPasteBoard": "The image link has been copied to the clipboard", + "addAnImage": "Add an image", + "imageUploadFailed": "Image upload failed" + }, + "urlPreview": { + "copiedToPasteBoard": "The link has been copied to the clipboard", + "convertToLink": "Convert to embed link" + }, + "outline": { + "addHeadingToCreateOutline": "Add headings to create a table of contents.", + "noMatchHeadings": "No matching headings found." + }, + "table": { + "addAfter": "Add after", + "addBefore": "Add before", + "delete": "Delete", + "clear": "Clear content", + "duplicate": "Duplicate", + "bgColor": "Background color" + }, + "contextMenu": { + "copy": "Copy", + "cut": "Cut", + "paste": "Paste" + }, + "action": "Actions", + "database": { + "selectDataSource": "Select data source", + "noDataSource": "No data source", + "selectADataSource": "Select a data source", + "toContinue": "to continue", + "newDatabase": "New Database", + "linkToDatabase": "Link to Database" + }, + "date": "Date", + "emoji": "Emoji" + }, + "outlineBlock": { + "placeholder": "Table of Contents" + }, + "textBlock": { + "placeholder": "Type '/' for commands" + }, + "title": { + "placeholder": "Untitled" + }, + "imageBlock": { + "placeholder": "Click to add image", + "upload": { + "label": "Upload", + "placeholder": "Click to upload image" + }, + "url": { + "label": "Image URL", + "placeholder": "Enter image URL" + }, + "ai": { + "label": "Generate image from OpenAI", + "placeholder": "Please input the prompt for OpenAI to generate image" + }, + "stability_ai": { + "label": "Generate image from Stability AI", + "placeholder": "Please input the prompt for Stability AI to generate image" + }, + "support": "Image size limit is 5MB. Supported formats: JPEG, PNG, GIF, SVG", + "error": { + "invalidImage": "Invalid image", + "invalidImageSize": "Image size must be less than 5MB", + "invalidImageFormat": "Image format is not supported. Supported formats: JPEG, PNG, JPG, GIF, SVG, WEBP", + "invalidImageUrl": "Invalid image URL", + "noImage": "No such file or directory" + }, + "embedLink": { + "label": "Embed link", + "placeholder": "Paste or type an image link" + }, + "unsplash": { + "label": "Unsplash" + }, + "searchForAnImage": "Search for an image", + "pleaseInputYourOpenAIKey": "please input your OpenAI key in Settings page", + "pleaseInputYourStabilityAIKey": "please input your Stability AI key in Settings page", + "saveImageToGallery": "Save image", + "failedToAddImageToGallery": "Failed to add image to gallery", + "successToAddImageToGallery": "Image added to gallery successfully", + "unableToLoadImage": "Unable to load image", + "maximumImageSize": "Maximum supported upload image size is 10MB", + "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB", + "imageIsUploading": "Image is uploading" + }, + "codeBlock": { + "language": { + "label": "Language", + "placeholder": "Select language" + } + }, + "inlineLink": { + "placeholder": "Paste or type a link", + "openInNewTab": "Open in new tab", + "copyLink": "Copy link", + "removeLink": "Remove link", + "url": { + "label": "Link URL", + "placeholder": "Enter link URL" + }, + "title": { + "label": "Link Title", + "placeholder": "Enter link title" + } + }, + "mention": { + "placeholder": "Mention a person or a page or date...", + "page": { + "label": "Link to page", + "tooltip": "Click to open page" + }, + "deleted": "Deleted", + "deletedContent": "This content does not exist or has been deleted" + }, + "toolbar": { + "resetToDefaultFont": "Reset to default" + }, + "errorBlock": { + "theBlockIsNotSupported": "The current version does not support this block.", + "blockContentHasBeenCopied": "The block content has been copied." + } + }, + "board": { + "column": { + "createNewCard": "New", + "renameGroupTooltip": "Press to rename group", + "createNewColumn": "Add a new group", + "addToColumnTopTooltip": "Add a new card at the top", + "addToColumnBottomTooltip": "Add a new card at the bottom", + "renameColumn": "Rename", + "hideColumn": "Hide", + "newGroup": "New Group", + "deleteColumn": "Delete", + "deleteColumnConfirmation": "This will delete this group and all the cards in it.\nAre you sure you want to continue?" + }, + "hiddenGroupSection": { + "sectionTitle": "Hidden Groups", + "collapseTooltip": "Hide the hidden groups", + "expandTooltip": "View the hidden groups" + }, + "cardDetail": "Card Detail", + "cardActions": "Card Actions", + "cardDuplicated": "Card has been duplicated", + "cardDeleted": "Card has been deleted", + "showOnCard": "Show on card detail", + "setting": "Setting", + "propertyName": "Property name", + "menuName": "Board", + "showUngrouped": "Show ungrouped items", + "ungroupedButtonText": "Ungrouped", + "ungroupedButtonTooltip": "Contains cards that don't belong in any group", + "ungroupedItemsTitle": "Click to add to the board", + "groupBy": "Group by", + "referencedBoardPrefix": "View of", + "notesTooltip": "Notes inside", + "mobile": { + "editURL": "Edit URL", + "unhideGroup": "Unhide group", + "unhideGroupContent": "Are you sure you want to show this group on the board?", + "faildToLoad": "Failed to load board view" + } + }, + "calendar": { + "menuName": "Calendar", + "defaultNewCalendarTitle": "Untitled", + "newEventButtonTooltip": "Add a new event", + "navigation": { + "today": "Today", + "jumpToday": "Jump to Today", + "previousMonth": "Previous Month", + "nextMonth": "Next Month" + }, + "mobileEventScreen": { + "emptyTitle": "No events yet", + "emptyBody": "Press the plus button to create an event on this day." + }, + "settings": { + "showWeekNumbers": "Show week numbers", + "showWeekends": "Show weekends", + "firstDayOfWeek": "Start week on", + "layoutDateField": "Layout calendar by", + "changeLayoutDateField": "Change layout field", + "noDateTitle": "No Date", + "noDateHint": { + "zero": "Unscheduled events will show up here", + "one": "{count} unscheduled event", + "other": "{count} unscheduled events" + }, + "unscheduledEventsTitle": "Unscheduled events", + "clickToAdd": "Click to add to the calendar", + "name": "Calendar settings" + }, + "referencedCalendarPrefix": "View of", + "quickJumpYear": "Jump to", + "duplicateEvent": "Duplicate event" + }, + "errorDialog": { + "title": "AppFlowy Error", + "howToFixFallback": "We're sorry for the inconvenience! Submit an issue on our GitHub page that describes your error.", + "github": "View on GitHub" + }, + "search": { + "label": "Search", + "placeholder": { + "actions": "Search actions..." + } + }, + "message": { + "copy": { + "success": "Copied!", + "fail": "Unable to copy" + } + }, + "unSupportBlock": "The current version does not support this Block.", + "views": { + "deleteContentTitle": "Are you sure want to delete the {pageType}?", + "deleteContentCaption": "if you delete this {pageType}, you can restore it from the trash." + }, + "colors": { + "custom": "Custom", + "default": "Default", + "red": "Red", + "orange": "Orange", + "yellow": "Yellow", + "green": "Green", + "blue": "Blue", + "purple": "Purple", + "pink": "Pink", + "brown": "Brown", + "gray": "Gray" + }, + "emoji": { + "emojiTab": "Emoji", + "search": "Search emoji", + "noRecent": "No recent emoji", + "noEmojiFound": "No emoji found", + "filter": "Filter", + "random": "Random", + "selectSkinTone": "Select skin tone", + "remove": "Remove emoji", + "categories": { + "smileys": "Smileys & Emotion", + "people": "People & Body", + "animals": "Animals & Nature", + "food": "Food & Drink", + "activities": "Activities", + "places": "Travel & Places", + "objects": "Objects", + "symbols": "Symbols", + "flags": "Flags", + "nature": "Nature", + "frequentlyUsed": "Frequently Used" + }, + "skinTone": { + "default": "Default", + "light": "Light", + "mediumLight": "Medium-Light", + "medium": "Medium", + "mediumDark": "Medium-Dark", + "dark": "Dark" + } + }, + "inlineActions": { + "noResults": "No results", + "pageReference": "Page reference", + "docReference": "Document reference", + "boardReference": "Board reference", + "calReference": "Calendar reference", + "gridReference": "Grid reference", + "date": "Date", + "reminder": { + "groupTitle": "Reminder", + "shortKeyword": "remind" + } + }, + "datePicker": { + "dateTimeFormatTooltip": "Change the date and time format in settings", + "dateFormat": "Date format", + "includeTime": "Include time", + "isRange": "End date", + "timeFormat": "Time format", + "clearDate": "Clear date", + "reminderLabel": "Reminder", + "selectReminder": "Select reminder", + "reminderOptions": { + "none": "None", + "atTimeOfEvent": "Time of event", + "fiveMinsBefore": "5 mins before", + "tenMinsBefore": "10 mins before", + "fifteenMinsBefore": "15 mins before", + "thirtyMinsBefore": "30 mins before", + "oneHourBefore": "1 hour before", + "twoHoursBefore": "2 hours before", + "onDayOfEvent": "On day of event", + "oneDayBefore": "1 day before", + "twoDaysBefore": "2 days before", + "oneWeekBefore": "1 week before", + "custom": "Custom" + } + }, + "relativeDates": { + "yesterday": "Yesterday", + "today": "Today", + "tomorrow": "Tomorrow", + "oneWeek": "1 week" + }, + "notificationHub": { + "title": "Notifications", + "mobile": { + "title": "Updates" + }, + "emptyTitle": "All caught up!", + "emptyBody": "No pending notifications or actions. Enjoy the calm.", + "tabs": { + "inbox": "Inbox", + "upcoming": "Upcoming" + }, + "actions": { + "markAllRead": "Mark all as read", + "showAll": "All", + "showUnreads": "Unread" + }, + "filters": { + "ascending": "Ascending", + "descending": "Descending", + "groupByDate": "Group by date", + "showUnreadsOnly": "Show unreads only", + "resetToDefault": "Reset to default" + } + }, + "reminderNotification": { + "title": "Reminder", + "message": "Remember to check this before you forget!", + "tooltipDelete": "Delete", + "tooltipMarkRead": "Mark as read", + "tooltipMarkUnread": "Mark as unread" + }, + "findAndReplace": { + "find": "Find", + "previousMatch": "Previous match", + "nextMatch": "Next match", + "close": "Close", + "replace": "Replace", + "replaceAll": "Replace all", + "noResult": "No results", + "caseSensitive": "Case sensitive", + "searchMore": "Search to find more results" + }, + "error": { + "weAreSorry": "We're sorry", + "loadingViewError": "We're having trouble loading this view. Please check your internet connection, refresh the app, and do not hesitate to reach out to the team if the issue continues." + }, + "editor": { + "bold": "Bold", + "bulletedList": "Bulleted List", + "bulletedListShortForm": "Bulleted", + "checkbox": "Checkbox", + "embedCode": "Embed Code", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "Highlight", + "color": "Color", + "image": "Image", + "date": "Date", + "italic": "Italic", + "link": "Link", + "numberedList": "Numbered List", + "numberedListShortForm": "Numbered", + "quote": "Quote", + "strikethrough": "Strikethrough", + "text": "Text", + "underline": "Underline", + "fontColorDefault": "Default", + "fontColorGray": "Gray", + "fontColorBrown": "Brown", + "fontColorOrange": "Orange", + "fontColorYellow": "Yellow", + "fontColorGreen": "Green", + "fontColorBlue": "Blue", + "fontColorPurple": "Purple", + "fontColorPink": "Pink", + "fontColorRed": "Red", + "backgroundColorDefault": "Default background", + "backgroundColorGray": "Gray background", + "backgroundColorBrown": "Brown background", + "backgroundColorOrange": "Orange background", + "backgroundColorYellow": "Yellow background", + "backgroundColorGreen": "Green background", + "backgroundColorBlue": "Blue background", + "backgroundColorPurple": "Purple background", + "backgroundColorPink": "Pink background", + "backgroundColorRed": "Red background", + "backgroundColorLime": "Lime background", + "backgroundColorAqua": "Aqua background", + "done": "Done", + "cancel": "Cancel", + "tint1": "Tint 1", + "tint2": "Tint 2", + "tint3": "Tint 3", + "tint4": "Tint 4", + "tint5": "Tint 5", + "tint6": "Tint 6", + "tint7": "Tint 7", + "tint8": "Tint 8", + "tint9": "Tint 9", + "lightLightTint1": "Purple", + "lightLightTint2": "Pink", + "lightLightTint3": "Light Pink", + "lightLightTint4": "Orange", + "lightLightTint5": "Yellow", + "lightLightTint6": "Lime", + "lightLightTint7": "Green", + "lightLightTint8": "Aqua", + "lightLightTint9": "Blue", + "urlHint": "URL", + "mobileHeading1": "Heading 1", + "mobileHeading2": "Heading 2", + "mobileHeading3": "Heading 3", + "textColor": "Text Color", + "backgroundColor": "Background Color", + "addYourLink": "Add your link", + "openLink": "Open link", + "copyLink": "Copy link", + "removeLink": "Remove link", + "editLink": "Edit link", + "linkText": "Text", + "linkTextHint": "Please enter text", + "linkAddressHint": "Please enter URL", + "highlightColor": "Highlight color", + "clearHighlightColor": "Clear highlight color", + "customColor": "Custom color", + "hexValue": "Hex value", + "opacity": "Opacity", + "resetToDefaultColor": "Reset to default color", + "ltr": "LTR", + "rtl": "RTL", + "auto": "Auto", + "cut": "Cut", + "copy": "Copy", + "paste": "Paste", + "find": "Find", + "previousMatch": "Previous match", + "nextMatch": "Next match", + "closeFind": "Close", + "replace": "Replace", + "replaceAll": "Replace all", + "regex": "Regex", + "caseSensitive": "Case sensitive", + "uploadImage": "Upload Image", + "urlImage": "URL Image", + "incorrectLink": "Incorrect Link", + "upload": "Upload", + "chooseImage": "Choose an image", + "loading": "Loading", + "imageLoadFailed": "Could not load the image", + "divider": "Divider", + "table": "Table", + "colAddBefore": "Add before", + "rowAddBefore": "Add before", + "colAddAfter": "Add after", + "rowAddAfter": "Add after", + "colRemove": "Remove", + "rowRemove": "Remove", + "colDuplicate": "Duplicate", + "rowDuplicate": "Duplicate", + "colClear": "Clear Content", + "rowClear": "Clear Content", + "slashPlaceHolder": "Type '/' to insert a block, or start typing", + "typeSomething": "Type something...", + "toggleListShortForm": "Toggle", + "quoteListShortForm": "Quote", + "mathEquationShortForm": "Formula", + "codeBlockShortForm": "Code" + }, + "favorite": { + "noFavorite": "No favorite page", + "noFavoriteHintText": "Swipe the page to the left to add it to your favorites" + }, + "cardDetails": { + "notesPlaceholder": "Enter a / to insert a block, or start typing" + }, + "blockPlaceholders": { + "todoList": "To-do", + "bulletList": "List", + "numberList": "List", + "quote": "Quote", + "heading": "Heading {}" + }, + "titleBar": { + "pageIcon": "Page icon", + "language": "Language", + "font": "Font", + "actions": "Actions", + "date": "Date", + "addField": "Add field", + "userIcon": "User icon" + }, + "noLogFiles": "There're no log files", + "newSettings": { + "myAccount": { + "title": "My account", + "subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.", + "profileLabel": "Account name & Profile image", + "profileNamePlaceholder": "Enter your name", + "accountSecurity": "Account security", + "2FA": "2-Step Authentication", + "aiKeys": "AI keys", + "accountLogin": "Account Login", + "updateNameError": "Failed to update name", + "updateIconError": "Failed to update icon", + "deleteAccount": { + "title": "Delete Account", + "subtitle": "Permanently delete your account and all of your data.", + "deleteMyAccount": "Delete my account", + "dialogTitle": "Delete account", + "dialogContent1": "Are you sure you want to permanently delete your account?", + "dialogContent2": "This action cannot be undone, and will remove access from all teamspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces." + } + }, + "workplace": { + "name": "Workplace", + "title": "Workplace Settings", + "subtitle": "Customize your workspace appearance, theme, font, text layout, date, time, and language.", + "workplaceName": "Workplace name", + "workplaceNamePlaceholder": "Enter workplace name", + "workplaceIcon": "Workplace icon", + "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications", + "renameError": "Failed to rename workplace", + "updateIconError": "Failed to update icon", + "appearance": { + "name": "Appearance", + "themeMode": { + "auto": "Auto", + "light": "Light", + "dark": "Dark" + }, + "language": "Language" + } + } + } +} \ No newline at end of file diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index fb50bf0998..fee91c5410 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -49,7 +49,8 @@ "LogInWithGoogle": "Log in with Google", "LogInWithGithub": "Log in with Github", "LogInWithDiscord": "Log in with Discord", - "signInWith": "Sign in with:" + "signInWith": "Sign in with:", + "signInWithEmail": "Sign in with Email" }, "workspace": { "chooseWorkspace": "Choose your workspace", @@ -65,17 +66,23 @@ "exportLogFiles": "Export log files", "reachOut": "Reach out on Discord" }, + "menuTitle": "Workspaces", "deleteWorkspaceHintText": "Are you sure you want to delete the workspace? This action cannot be undone.", "createSuccess": "Workspace created successfully", "createFailed": "Failed to create workspace", + "createLimitExceeded": "You've reached the maximum workspace limit allowed for your account. If you need additional workspaces to continue your work, please request on Github", "deleteSuccess": "Workspace deleted successfully", "deleteFailed": "Failed to delete workspace", "openSuccess": "Open workspace successfully", "openFailed": "Failed to open workspace", "renameSuccess": "Workspace renamed successfully", "renameFailed": "Failed to rename workspace", - "updateIconSuccess": "Workspace reset successfully", - "updateIconFailed": "Failed to reset workspace" + "updateIconSuccess": "Updated workspace icon successfully", + "updateIconFailed": "Updated workspace icon failed", + "cannotDeleteTheOnlyWorkspace": "Cannot delete the only workspace", + "fetchWorkspacesFailed": "Failed to fetch workspaces", + "leaveCurrentWorkspace": "Leave workspace", + "leaveCurrentWorkspacePrompt": "Are you sure you want to leave the current workspace?" }, "shareAction": { "buttonText": "Share", @@ -144,7 +151,8 @@ "emptyDescription": "You don't have any deleted file", "isDeleted": "is deleted", "isRestored": "is restored" - } + }, + "confirmDeleteTitle": "Are you sure you want to delete this page permanently?" }, "deletePagePrompt": { "text": "This page is in Trash", @@ -198,18 +206,22 @@ "dragRow": "Long press to reorder the row", "viewDataBase": "View database", "referencePage": "This {name} is referenced", - "addBlockBelow": "Add a block below", - "urlLaunchAccessory": "Open in browser", - "urlCopyAccessory": "Copy URL" + "addBlockBelow": "Add a block below" }, "sideBar": { "closeSidebar": "Close side bar", "openSidebar": "Open side bar", "personal": "Personal", + "private": "Private", + "public": "Public", "favorites": "Favorites", - "clickToHidePersonal": "Click to hide personal section", - "clickToHideFavorites": "Click to hide favorite section", + "clickToHidePrivate": "Click to hide private space\nPages you created here are only visible to you", + "clickToHidePublic": "Click to hide public space\nPages you created here are visible to every member", + "clickToHidePersonal": "Click to hide personal space", + "clickToHideFavorites": "Click to hide favorite space", "addAPage": "Add a page", + "addAPageToPrivate": "Add a page to private space", + "addAPageToPublic": "Add a page to public space", "recent": "Recent" }, "notifications": { @@ -257,7 +269,14 @@ "remove": "Remove", "dontRemove": "Don't remove", "copyLink": "Copy Link", - "align": "Align" + "align": "Align", + "login": "Login", + "logout": "Log out", + "deleteAccount": "Delete account", + "back": "Back", + "signInGoogle": "Sign in with Google", + "signInGithub": "Sign in with Github", + "signInDiscord": "Sign in with Discord" }, "label": { "welcome": "Welcome!", @@ -424,7 +443,17 @@ "guestHintText": "A Guest can read, react, comment, and can edit certain pages with permission.", "emailInvalidError": "Invalid email, please check and try again", "emailSent": "Email sent, please check the inbox", - "members": "members" + "members": "members", + "membersCount": { + "zero": "{} members", + "one": "{} member", + "other": "{} members" + }, + "memberLimitExceeded": "You've reached the maximum member limit allowed for your account. If you want to add more additional members to continue your work, please request on Github", + "failedToAddMember": "Failed to add member", + "addMemberSuccess": "Member added successfully", + "removeMember": "Remove Member", + "areYouSureToRemoveMember": "Are you sure you want to remove this member?" } }, "files": { @@ -565,13 +594,9 @@ "isComplete": "is complete", "isIncomplted": "is incomplete" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Is", "isNot": "Is not", - "isEmpty": "Is empty", - "isNotEmpty": "Is not empty" - }, - "multiSelectOptionFilter": { "contains": "Contains", "doesNotContain": "Does not contain", "isEmpty": "Is empty", @@ -612,6 +637,7 @@ "insertRight": "Insert Right", "duplicate": "Duplicate", "delete": "Delete", + "clear": "Clear cells", "textFieldName": "Text", "checkboxFieldName": "Checkbox", "dateFieldName": "Date", @@ -652,6 +678,7 @@ "editProperty": "Edit property", "newProperty": "New property", "deleteFieldPromptMessage": "Are you sure? This property will be deleted", + "clearFieldPromptMessage": "Are you sure? All cells in this column will be emptied", "newColumn": "New Column", "format": "Format", "reminderOnDateTooltip": "This cell has a scheduled reminder", @@ -712,7 +739,7 @@ "colorPanelTitle": "Color", "panelTitle": "Select an option or create one", "searchOption": "Search for an option", - "searchOrCreateOption": "Search or create an option...", + "searchOrCreateOption": "Search for an option or create one", "createNew": "Create a new", "orSelectOne": "Or select an option", "typeANewOption": "Type a new option", @@ -725,10 +752,18 @@ "hideComplete": "Hide completed tasks", "showComplete": "Show all tasks" }, + "url": { + "launch": "Open link in browser", + "copy": "Copy link to clipboard", + "textFieldHint": "Enter a URL", + "copiedNotification": "Copied to clipboard!" + }, "relation": { "relatedDatabasePlaceLabel": "Related Database", "relatedDatabasePlaceholder": "None", "inRelatedDatabase": "In", + "rowSearchTextFieldPlaceholder": "Search", + "noDatabaseSelected": "No database selected, please select one first from the list below:", "emptySearchResult": "No records found" }, "menuName": "Grid", @@ -1300,6 +1335,8 @@ "copy": "Copy", "paste": "Paste", "find": "Find", + "select": "Select", + "selectAll": "Select all", "previousMatch": "Previous match", "nextMatch": "Next match", "closeFind": "Close", @@ -1356,5 +1393,52 @@ "addField": "Add field", "userIcon": "User icon" }, - "noLogFiles": "There're no log files" + "noLogFiles": "There're no log files", + "newSettings": { + "myAccount": { + "title": "My account", + "subtitle": "Customize your profile, manage account security, open AI keys, or login into your account.", + "profileLabel": "Account name & Profile image", + "profileNamePlaceholder": "Enter your name", + "accountSecurity": "Account security", + "2FA": "2-Step Authentication", + "aiKeys": "AI keys", + "accountLogin": "Account Login", + "updateNameError": "Failed to update name", + "updateIconError": "Failed to update icon", + "deleteAccount": { + "title": "Delete Account", + "subtitle": "Permanently delete your account and all of your data.", + "deleteMyAccount": "Delete my account", + "dialogTitle": "Delete account", + "dialogContent1": "Are you sure you want to permanently delete your account?", + "dialogContent2": "This action cannot be undone, and will remove access from all teamspaces, erasing your entire account, including private workspaces, and removing you from all shared workspaces." + } + }, + "workplace": { + "name": "Workplace", + "title": "Workplace Settings", + "subtitle": "Customize your workspace appearance, theme, font, text layout, date, time, and language.", + "workplaceName": "Workplace name", + "workplaceNamePlaceholder": "Enter workplace name", + "workplaceIcon": "Workplace icon", + "workplaceIconSubtitle": "Upload an image or use an emoji for your workspace. Icon will show in your sidebar and notifications", + "renameError": "Failed to rename workplace", + "updateIconError": "Failed to update icon", + "appearance": { + "name": "Appearance", + "themeMode": { + "auto": "Auto", + "light": "Light", + "dark": "Dark" + }, + "language": "Language" + } + }, + "syncState": { + "syncing": "Syncing", + "synced": "Synced", + "noNetworkConnected": "No network connected" + } + } } \ No newline at end of file diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index e4f79f4a7d..991a712421 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -9,6 +9,7 @@ "title": "Título", "youCanAlso": "Tú también puedes", "and": "y", + "failedToOpenUrl": "No se pudo abrir la URL: {}", "blockActions": { "addBelowTooltip": "Haga clic para agregar a continuación", "addAboveCmd": "Alt+clic", @@ -61,8 +62,18 @@ "failedToLoad": "¡Algo salió mal! No se pudo cargar el espacio de trabajo. Intente cerrar cualquier instancia abierta de AppFlowy y vuelva a intentarlo.", "errorActions": { "reportIssue": "Reportar un problema", + "reportIssueOnGithub": "Informar un problema en Github", + "exportLogFiles": "Exportar archivos de registro (logs)", "reachOut": "Comuníquese con Discord" - } + }, + "deleteWorkspaceHintText": "¿Está seguro de que desea eliminar el espacio de trabajo? Esta acción no se puede deshacer.", + "createSuccess": "Espacio de trabajo creado exitosamente", + "createFailed": "No se pudo crear el espacio de trabajo", + "deleteSuccess": "Espacio de trabajo eliminado correctamente", + "deleteFailed": "No se pudo eliminar el espacio de trabajo", + "openFailed": "No se pudo abrir el espacio de trabajo", + "renameSuccess": "Espacio de trabajo renombrado exitosamente", + "renameFailed": "No se pudo cambiar el nombre del espacio de trabajo" }, "shareAction": { "buttonText": "Compartir", @@ -77,7 +88,11 @@ "large": "grande", "fontSize": "Tamaño de fuente", "import": "Importar", - "moreOptions": "Mas opciones" + "moreOptions": "Mas opciones", + "wordCount": "El recuento de palabras: {}", + "charCount": "Número de caracteres : {}", + "deleteView": "Borrar", + "duplicateView": "Duplicar" }, "importPanel": { "textAndMarkdown": "Texto y descuento", @@ -180,9 +195,7 @@ "dragRow": "Pulsación larga para reordenar la fila", "viewDataBase": "Ver base de datos", "referencePage": "Se hace referencia a este {nombre}", - "addBlockBelow": "Añadir un bloque a continuación", - "urlLaunchAccessory": "Abrir en el navegador", - "urlCopyAccessory": "Copiar URL" + "addBlockBelow": "Añadir un bloque a continuación" }, "sideBar": { "closeSidebar": "Cerrar panel lateral", @@ -234,7 +247,18 @@ "rename": "Renombrar", "helpCenter": "Centro de ayuda", "add": "Añadir", - "yes": "Si" + "yes": "Si", + "remove": "Eliminar", + "dontRemove": "no quitar", + "copyLink": "Copiar enlace", + "align": "Alinear", + "login": "Inciar sessión", + "logout": "Cerrar sesión", + "deleteAccount": "Borrar cuenta", + "back": "Atrás", + "signInGoogle": "Inicia sesión con Google", + "signInGithub": "Iniciar sesión con Github", + "signInDiscord": "Iniciar sesión con discordia" }, "label": { "welcome": "¡Bienvenido!", @@ -502,13 +526,9 @@ "isComplete": "Esta completo", "isIncomplted": "esta incompleto" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Es", "isNot": "No es", - "isEmpty": "Esta vacio", - "isNotEmpty": "No está vacío" - }, - "multiSelectOptionFilter": { "contains": "Contiene", "doesNotContain": "No contiene", "isEmpty": "Esta vacio", @@ -627,6 +647,10 @@ "hideComplete": "Ocultar tareas completadas", "showComplete": "Mostrar todas las tareas" }, + "url": { + "launch": "Abrir en el navegador", + "copy": "Copiar URL" + }, "menuName": "Cuadrícula", "referencedGridPrefix": "Vista de" }, diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index 9c1dbacd5b..5aa3f3cee7 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -323,13 +323,9 @@ "isComplete": "osatu da", "isIncomplted": "osatu gabe dago" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "da", "isNot": "Ez da", - "isEmpty": "Hutsa dago", - "isNotEmpty": "Ez dago hutsik" - }, - "multiSelectOptionFilter": { "contains": "Duen", "doesNotContain": "Ez dauka", "isEmpty": "Hutsa dago", diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 5776dcd885..b4ea31ecf7 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -356,13 +356,9 @@ "isComplete": "کامل است", "isIncomplted": "کامل نیست" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "است", "isNot": "نیست", - "isEmpty": "خالی است", - "isNotEmpty": "خالی نیست" - }, - "multiSelectOptionFilter": { "contains": "شامل", "doesNotContain": "شامل نیست", "isEmpty": "خالی است", diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index ffd284cad7..d7448af5fd 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -182,9 +182,7 @@ "dragRow": "Appuyez longuement pour réorganiser la ligne", "viewDataBase": "Voir la base de données", "referencePage": "Ce {nom} est référencé", - "addBlockBelow": "Ajouter un bloc ci-dessous", - "urlLaunchAccessory": "Ouvrir dans le navigateur", - "urlCopyAccessory": "Copier l'URL" + "addBlockBelow": "Ajouter un bloc ci-dessous" }, "sideBar": { "closeSidebar": "Fermer le menu latéral", @@ -523,13 +521,9 @@ "isComplete": "fait", "isIncomplted": "pas fait" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Est", "isNot": "N'est pas", - "isEmpty": "Est vide", - "isNotEmpty": "N'est pas vide" - }, - "multiSelectOptionFilter": { "contains": "Contient", "doesNotContain": "Ne contient pas", "isEmpty": "Est vide", @@ -659,6 +653,10 @@ "hideComplete": "Cacher les tâches terminées", "showComplete": "Afficher toutes les tâches" }, + "url": { + "launch": "Ouvrir dans le navigateur", + "copy": "Copier l'URL" + }, "menuName": "Grille", "referencedGridPrefix": "Vue", "calculate": "Calculer", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 8d5e9111cd..2d80a67a4f 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -55,10 +55,10 @@ "chooseWorkspace": "Choisissez votre espace de travail", "create": "Créer un espace de travail", "reset": "Réinitialiser l'espace de travail", - "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", + "resetWorkspacePrompt": "La réinitialisation de l'espace de travail supprimera toutes les pages et données qu'elles contiennent. Êtes-vous sûr de vouloir réinitialiser l'espace de travail ? Alternativement, vous pouvez contacter l'équipe d'assistance pour restaurer l'espace de travail", "hint": "Espace de travail", "notFoundError": "Espace de travail introuvable", - "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'AppFlowy et réessayez.", + "failedToLoad": "Quelque chose s'est mal passé ! Échec du chargement de l'espace de travail. Essayez de fermer toute instance ouverte d'AppFlowy et réessayez.", "errorActions": { "reportIssue": "Signaler un problème", "reportIssueOnGithub": "Signaler un bug sur Github", @@ -79,7 +79,12 @@ "large": "grand", "fontSize": "Taille de police", "import": "Importer", - "moreOptions": "Plus d'options" + "moreOptions": "Plus d'options", + "wordCount": "Compteur de mot: {}", + "charCount": "Compteur de caractère: {}", + "createdAt": "Créé à: {}", + "deleteView": "Supprimer", + "duplicateView": "Dupliquer" }, "importPanel": { "textAndMarkdown": "Texte et Markdown", @@ -115,11 +120,11 @@ "created": "Créé" }, "confirmDeleteAll": { - "title": "Voulez-vous vraiment supprimer toutes les pages de la corbeille ?", + "title": "Voulez-vous vraiment supprimer toutes les pages de la corbeille ?", "caption": "Cette action ne peut pas être annulée." }, "confirmRestoreAll": { - "title": "Êtes-vous sûr de vouloir restaurer toutes les pages dans la corbeille ?", + "title": "Êtes-vous sûr de vouloir restaurer toutes les pages dans la corbeille ?", "caption": "Cette action ne peut pas être annulée." }, "mobile": { @@ -182,9 +187,7 @@ "dragRow": "Appuyez longuement pour réorganiser la ligne", "viewDataBase": "Voir la base de données", "referencePage": "Ce {nom} est référencé", - "addBlockBelow": "Ajouter un bloc ci-dessous", - "urlLaunchAccessory": "Ouvrir dans le navigateur", - "urlCopyAccessory": "Copier l'URL" + "addBlockBelow": "Ajouter un bloc ci-dessous" }, "sideBar": { "closeSidebar": "Fermer le menu latéral", @@ -237,6 +240,9 @@ "helpCenter": "Centre d'aide", "add": "Ajouter", "yes": "Oui", + "remove": "Retirer", + "dontRemove": "Ne pas retirer", + "align": "Aligner", "tryAGain": "Réessayer" }, "label": { @@ -269,7 +275,7 @@ "notifications": "Notifications", "open": "Ouvrir les paramètres", "logout": "Se déconnecter", - "logoutPrompt": "Êtes-vous sûr de vouloir vous déconnecter ?", + "logoutPrompt": "Êtes-vous sûr de vouloir vous déconnecter ?", "selfEncryptionLogoutPrompt": "Êtes-vous sûr de vouloir vous déconnecter ? Veuillez vous assurer d'avoir copié la clé de chiffrement.", "syncSetting": "Paramètres de synchronisation", "cloudSettings": "Paramètres cloud", @@ -298,14 +304,14 @@ "restartApp": "Redémarer", "restartAppTip": "Redémarrez l'application pour que les modifications prennent effet. Veuillez noter que cela pourrait déconnecter votre compte actuel.", "changeServerTip": "Après avoir changé de serveur, vous devez cliquer sur le bouton de redémarrer pour que les modifications prennent effet", - "enableEncryptPrompt": "Activez le chiffrement pour sécuriser vos données avec cette clé. Rangez-la en toute sécurité ; une fois activé, il ne peut pas être désactivé. En cas de perte, vos données deviennent irrécupérables. Cliquez pour copier", + "enableEncryptPrompt": "Activez le chiffrement pour sécuriser vos données avec cette clé. Rangez-la en toute sécurité ; une fois activé, il ne peut pas être désactivé. En cas de perte, vos données deviennent irrécupérables. Cliquez pour copier", "inputEncryptPrompt": "Veuillez saisir votre mot ou phrase de passe pour", "clickToCopySecret": "Cliquez pour copier le mot ou la phrase de passe", "configServerSetting": "Configurez les paramètres de votre serveur", "configServerGuide": "Après avoir sélectionné « Démarrage rapide », accédez à « Paramètres » puis « Paramètres Cloud » pour configurer votre serveur auto-hébergé.", "inputTextFieldHint": "Votre mot ou phrase de passe", "historicalUserList": "Historique de connexion d'utilisateurs", - "historicalUserListTooltip": "Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »", + "historicalUserListTooltip": "Cette liste affiche vos comptes anonymes. Vous pouvez cliquer sur un compte pour afficher ses détails. Les comptes anonymes sont créés en cliquant sur le bouton « Commencer »", "openHistoricalUser": "Cliquez pour ouvrir le compte anonyme", "customPathPrompt": "Le stockage du dossier de données AppFlowy dans un dossier synchronisé avec le cloud tel que Google Drive peut présenter des risques. Si la base de données de ce dossier est consultée ou modifiée à partir de plusieurs emplacements en même temps, cela peut entraîner des conflits de synchronisation et une corruption potentielle des données.", "importAppFlowyData": "Importer des données à partir du dossier AppFlowy externe", @@ -338,7 +344,7 @@ "cursorColor": "Couleur du curseur du document", "selectionColor": "Couleur de sélection du document", "hexEmptyError": "La couleur hexadécimale ne peut pas être vide", - "hexLengthError": "La valeur hexadécimale doit comporter 6 chiffres", + "hexLengthError": "La valeur hexadécimale doit comporter 6 chiffres", "hexInvalidError": "Valeur hexadécimale invalide", "opacityEmptyError": "L'opacité ne peut pas être vide", "opacityRangeError": "L'opacité doit être comprise entre 1 et 100", @@ -368,7 +374,7 @@ "uploadSuccess": "Votre thème a été téléversé avec succès", "deletionFailure": "Échec de la suppression du thème. Essayez de le supprimer manuellement.", "filePickerDialogTitle": "Choisissez un fichier .flowy_plugin", - "urlUploadFailure": "Échec de l'ouverture de l'URL : {}", + "urlUploadFailure": "Échec de l'ouverture de l'URL : {}", "failure": "Le thème qui a été téléchargé avait un format non valide." }, "theme": "Thème", @@ -387,7 +393,23 @@ "twelveHour": "Douze heures", "twentyFourHour": "Vingt-quatre heures" }, - "showNamingDialogWhenCreatingPage": "Afficher la boîte de dialogue de nommage lors de la création d'une page" + "showNamingDialogWhenCreatingPage": "Afficher la boîte de dialogue de nommage lors de la création d'une page", + "members": { + "inviteMembers": "Inviter des membres", + "sendInvite": "Envoyer une invitation", + "copyInviteLink": "Copier le lien d'invitation", + "label": "Membres", + "user": "Utilisateur", + "role": "Rôle", + "removeFromWorkspace": "Retirer de l'espace de travail", + "owner": "Propriétaire", + "guest": "Invité", + "member": "Membre", + "memberHintText": "Un membre peut lire, commenter, et éditer des pages. Inviter des membres et des invités.", + "guestHintText": "Un invité peut lire, réagir, commenter, et peut éditer certaines pages avec une permission", + "emailInvalidError": "Email invalide, veuillez le vérifier et recommencer", + "emailSent": "Email envoyé, veuillez vérifier dans votre boîte mail." + } }, "files": { "copy": "Copie", @@ -415,14 +437,14 @@ "set": "Définir", "folderPath": "Chemin pour stocker votre dossier", "locationCannotBeEmpty": "Le chemin ne peut pas être vide", - "pathCopiedSnackbar": "Chemin de stockage des fichiers copié dans le presse-papier !", + "pathCopiedSnackbar": "Chemin de stockage des fichiers copié dans le presse-papier !", "changeLocationTooltips": "Changer le répertoire de données", "change": "Changer", "openLocationTooltips": "Ouvrir un autre répertoire de données", "openCurrentDataFolder": "Ouvrir le répertoire de données actuel", "recoverLocationTooltips": "Réinitialiser au répertoire de données par défaut d'AppFlowy", - "exportFileSuccess": "Exporter le fichier avec succès !", - "exportFileFail": "Échec de l'export du fichier !", + "exportFileSuccess": "Exporter le fichier avec succès !", + "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter" }, "user": { @@ -440,7 +462,7 @@ "keyBinding": "Racourcis clavier", "addNewCommand": "Ajouter une Nouvelle Commande", "updateShortcutStep": "Appuyez sur la combinaison de touches souhaitée et appuyez sur ENTER", - "shortcutIsAlreadyUsed": "Ce raccourci est déjà utilisé pour : {conflict}", + "shortcutIsAlreadyUsed": "Ce raccourci est déjà utilisé pour : {conflict}", "resetToDefault": "Réinitialiser les raccourcis clavier par défaut", "couldNotLoadErrorMsg": "Impossible de charger les raccourcis, réessayez", "couldNotSaveErrorMsg": "Impossible d'enregistrer les raccourcis. Réessayez" @@ -464,7 +486,7 @@ } }, "grid": { - "deleteView": "Voulez-vous vraiment supprimer cette vue ?", + "deleteView": "Voulez-vous vraiment supprimer cette vue ?", "createView": "Nouveau", "title": { "placeholder": "Sans titre" @@ -520,13 +542,9 @@ "isComplete": "fait", "isIncomplted": "pas fait" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Est", "isNot": "N'est pas", - "isEmpty": "Est vide", - "isNotEmpty": "N'est pas vide" - }, - "multiSelectOptionFilter": { "contains": "Contient", "doesNotContain": "Ne contient pas", "isEmpty": "Est vide", @@ -540,7 +558,25 @@ "onOrAfter": "Est le ou après", "between": "Est entre", "empty": "Est vide", - "notEmpty": "N'est pas vide" + "notEmpty": "N'est pas vide", + "choicechipPrefix": { + "before": "Avant", + "after": "Après", + "onOrBefore": "Pendant ou avant", + "onOrAfter": "Pendant ou après", + "isEmpty": "Est vide", + "isNotEmpty": "N'est pas vide" + } + }, + "numberFilter": { + "equal": "Égal", + "notEqual": "N'est pas égal", + "lessThan": "Est moins que", + "greaterThan": "Est plus que", + "lessThanOrEqualTo": "Est inférieur ou égal à", + "greaterThanOrEqualTo": "Est supérieur ou égal à ", + "isEmpty": "Est vide", + "isNotEmpty": "N'est pas vide" }, "field": { "hide": "Cacher", @@ -559,6 +595,7 @@ "multiSelectFieldName": "Sélection multiple", "urlFieldName": "URL", "checklistFieldName": "Check-list", + "relationFieldName": "Relation", "numberFormat": "Format du nombre", "dateFormat": "Format de la date", "includeTime": "Inclure l'heure", @@ -590,27 +627,31 @@ "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?", "newColumn": "Nouvelle colonne", "format": "Format", - "reminderOnDateTooltip": "Cette cellule a un rappel programmé" + "reminderOnDateTooltip": "Cette cellule a un rappel programmé", + "optionAlreadyExist": "L'option existe déjà" }, "rowPage": { "newField": "Ajouter un nouveau champ", "fieldDragElementTooltip": "Cliquez pour ouvrir le menu", "showHiddenFields": { - "one": "Afficher {count} champ masqué", - "many": "Afficher {count} champs masqués", - "other": "Afficher {count} champs masqués" + "one": "Afficher {count} champ masqué", + "many": "Afficher {count} champs masqués", + "other": "Afficher {count} champs masqués" }, "hideHiddenFields": { - "one": "Cacher {count} champ caché", - "many": "Cacher {count} champs masqués", - "other": "Cacher {count} champs masqués" + "one": "Cacher {count} champ caché", + "many": "Cacher {count} champs masqués", + "other": "Cacher {count} champs masqués" } }, "sort": { "ascending": "Ascendant", "descending": "Descendant", + "by": "Par", + "empty": "Tri pas actif", "deleteAllSorts": "Supprimer tous les tris", "addSort": "Ajouter un tri", + "removeSorting": "Voulez-vous supprimer le tri ?", "deleteSort": "Supprimer le tri" }, "row": { @@ -656,6 +697,14 @@ "hideComplete": "Cacher les tâches terminées", "showComplete": "Afficher toutes les tâches" }, + "url": { + "launch": "Ouvrir dans le navigateur", + "copy": "Copier l'URL" + }, + "relation": { + "inRelatedDatabase": "Dans", + "emptySearchResult": "Aucun enregistrement trouvé" + }, "menuName": "Grille", "referencedGridPrefix": "Vue", "calculate": "Calculer", @@ -701,7 +750,7 @@ "referencedCalendar": "Calendrier référencé", "referencedDocument": "Document référencé", "autoGeneratorMenuItemName": "Rédacteur OpenAI", - "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...", + "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...", "autoGeneratorLearnMore": "Apprendre encore plus", "autoGeneratorGenerate": "Générer", "autoGeneratorHintText": "Demandez à OpenAI...", @@ -717,7 +766,7 @@ "smartEditCouldNotFetchResult": "Impossible de récupérer le résultat d'OpenAI", "smartEditCouldNotFetchKey": "Impossible de récupérer la clé OpenAI", "smartEditDisabled": "Connectez OpenAI dans les paramètres", - "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", + "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", "createInlineMathEquation": "Créer une équation", "fonts": "Polices", "toggleList": "Liste pliable", @@ -769,11 +818,13 @@ "left": "Gauche", "center": "Centre", "right": "Droite", - "defaultColor": "Défaut" + "defaultColor": "Défaut", + "depth": "Profond" }, "image": { "copiedToPasteBoard": "Le lien de l'image a été copié dans le presse-papiers", - "addAnImage": "Ajouter une image" + "addAnImage": "Ajouter une image", + "imageUploadFailed": "Téléchargement de l'image échoué" }, "urlPreview": { "copiedToPasteBoard": "Le lien a été copié dans le presse-papier" @@ -806,6 +857,9 @@ "date": "Date", "emoji": "Emoji" }, + "outlineBlock": { + "placeholder": "Table de contenu" + }, "textBlock": { "placeholder": "Tapez '/' pour les commandes" }, @@ -828,7 +882,7 @@ }, "stability_ai": { "label": "Générer une image à partir de Stability AI", - "placeholder": "Veuillez saisir l'invite permettant à Stability AI de générer une image." + "placeholder": "Veuillez saisir l'invite permettant à Stability AI de générer une image." }, "support": "La limite de taille d'image est de 5 Mo. Formats pris en charge : JPEG, PNG, GIF, SVG", "error": { @@ -852,7 +906,8 @@ "successToAddImageToGallery": "Image ajoutée à la galerie avec succès", "unableToLoadImage": "Impossible de charger l'image", "maximumImageSize": "La taille d'image maximale est 10Mo", - "uploadImageErrorImageSizeTooBig": "L'image doit faire moins de 10Mo" + "uploadImageErrorImageSizeTooBig": "L'image doit faire moins de 10Mo", + "imageIsUploading": "L'image est en cours de téléchargement" }, "codeBlock": { "language": { @@ -928,7 +983,7 @@ "mobile": { "editURL": "Modifier l'URL", "unhideGroup": "Afficher le groupe", - "unhideGroupContent": "Êtes-vous sûr de vouloir afficher ce groupe sur le tableau ?", + "unhideGroupContent": "Êtes-vous sûr de vouloir afficher ce groupe sur le tableau ?", "faildToLoad": "Échec du chargement de la vue du tableau" } }, @@ -974,13 +1029,13 @@ }, "message": { "copy": { - "success": "Copié !", + "success": "Copié !", "fail": "Impossible de copier" } }, "unSupportBlock": "La version actuelle ne prend pas en charge ce bloc.", "views": { - "deleteContentTitle": "Voulez-vous vraiment supprimer le {pageType} ?", + "deleteContentTitle": "Voulez-vous vraiment supprimer le {pageType}?", "deleteContentCaption": "si vous supprimez ce {pageType}, vous pouvez le restaurer à partir de la corbeille." }, "colors": { @@ -1110,7 +1165,8 @@ "replace": "Remplacer", "replaceAll": "Tout remplacer", "noResult": "Aucun résultat", - "caseSensitive": "Sensible à la casse" + "caseSensitive": "Sensible à la casse", + "searchMore": "Chercher pour trouver plus de résultat" }, "error": { "weAreSorry": "Nous sommes désolés", diff --git a/frontend/resources/translations/hin.json b/frontend/resources/translations/hin.json index 55cbce7f05..8ce86ed96b 100644 --- a/frontend/resources/translations/hin.json +++ b/frontend/resources/translations/hin.json @@ -1,743 +1,739 @@ { - "appName": "AppFlowy", - "defaultUsername": "मैं", - "welcomeText": " @:appName में आपका स्वागत है", - "githubStarText": "गिटहब पर स्टार करे", - "subscribeNewsletterText": "समाचार पत्रिका के लिए सदस्यता लें", - "letsGoButtonText": "जल्दी शुरू करे", - "title": "शीर्षक", - "youCanAlso": "आप भी कर सकते हैं", - "and": "और", - "blockActions": { - "addBelowTooltip": "नीचे जोड़ने के लिए क्लिक करें", - "addAboveCmd": "Alt+click ", - "addAboveMacCmd": "Option+click", - "addAboveTooltip": "ऊपर जोड़ने के लिए", - "dragTooltip": "ले जाने के लिए ड्रैग करें", - "openMenuTooltip": "मेनू खोलने के लिए क्लिक करें" + "appName": "AppFlowy", + "defaultUsername": "मैं", + "welcomeText": " @:appName में आपका स्वागत है", + "githubStarText": "गिटहब पर स्टार करे", + "subscribeNewsletterText": "समाचार पत्रिका के लिए सदस्यता लें", + "letsGoButtonText": "जल्दी शुरू करे", + "title": "शीर्षक", + "youCanAlso": "आप भी कर सकते हैं", + "and": "और", + "blockActions": { + "addBelowTooltip": "नीचे जोड़ने के लिए क्लिक करें", + "addAboveCmd": "Alt+click ", + "addAboveMacCmd": "Option+click", + "addAboveTooltip": "ऊपर जोड़ने के लिए", + "dragTooltip": "ले जाने के लिए ड्रैग करें", + "openMenuTooltip": "मेनू खोलने के लिए क्लिक करें" + }, + "signUp": { + "buttonText": "साइन अप करें", + "title": "साइन अप करें @:appName", + "getStartedText": "शुरू करे", + "emptyPasswordError": "पासवर्ड खाली नहीं हो सकता", + "repeatPasswordEmptyError": "रिपीट पासवर्ड खाली नहीं हो सकता", + "unmatchedPasswordError": "रिपीट पासवर्ड और पासवर्ड एक नहीं है", + "alreadyHaveAnAccount": "क्या आपके पास पहले से एक खाता मौजूद है?", + "emailHint": "ईमेल", + "passwordHint": "पासवर्ड", + "repeatPasswordHint": "रिपीट पासवर्ड", + "signUpWith": "इसके साथ साइन अप करें:" + }, + "signIn": { + "loginTitle": "लॉग इन करें @:appName", + "loginButtonText": "लॉग इन करें", + "loginStartWithAnonymous": "एक अज्ञात सत्र से प्रारंभ करें", + "continueAnonymousUser": "अज्ञात सत्र जारी रखें", + "buttonText": "साइन इन", + "forgotPassword": "पासवर्ड भूल गए?", + "emailHint": "ईमेल", + "passwordHint": "पासवर्ड", + "dontHaveAnAccount": "कोई खाता नहीं है?", + "repeatPasswordEmptyError": "रिपीट पासवर्ड खाली नहीं हो सकता", + "unmatchedPasswordError": "रिपीट पासवर्ड और पासवर्ड एक नहीं है", + "syncPromptMessage": "डेटा को सिंक करने में कुछ समय लग सकता है. कृपया इस पेज को बंद न करें", + "or": "या", + "LogInWithGoogle": "गूगल से लॉग इन करें", + "LogInWithGithub": "गिटहब से लॉग इन करें", + "LogInWithDiscord": "डिस्कॉर्ड से लॉग इन करें", + "signInWith": "इसके साथ साइन इन करें:" + }, + "workspace": { + "chooseWorkspace": "अपना कार्यक्षेत्र चुनें", + "create": "कार्यक्षेत्र बनाएं", + "reset": "कार्यक्षेत्र रीसेट करें", + "resetWorkspacePrompt": "कार्यक्षेत्र को रीसेट करने से उसमें मौजूद सभी पृष्ठ और डेटा हट जाएंगे। क्या आप वाकई कार्यक्षेत्र को रीसेट करना चाहते हैं? वैकल्पिक रूप से, आप कार्यक्षेत्र को पुनर्स्थापित करने के लिए सहायता टीम से संपर्क कर सकते हैं", + "hint": "कार्यक्षेत्र", + "notFoundError": "कार्यस्थल नहीं मिला" + }, + "shareAction": { + "buttonText": "शेयर", + "workInProgress": "जल्द आ रहा है", + "markdown": "markdown", + "csv": "csv", + "copyLink": "लिंक कॉपी करें" + }, + "moreAction": { + "small": "छोटा", + "medium": "मध्यम", + "large": "बड़ा", + "fontSize": "अक्षर का आकर", + "import": "आयात", + "moreOptions": "अधिक विकल्प" + }, + "importPanel": { + "textAndMarkdown": "Text & Markdown", + "documentFromV010": "Document from v0.1.0", + "databaseFromV010": "Database from v0.1.0", + "csv": "CSV", + "database": "Database" + }, + "disclosureAction": { + "rename": "नाम बदलें", + "delete": "हटाएं", + "duplicate": "डुप्लीकेट", + "unfavorite": "पसंदीदा से हटाएँ", + "favorite": "पसंदीदा में जोड़ें", + "openNewTab": "एक नए टैब में खोलें", + "moveTo": "स्थानांतरित करें", + "addToFavorites": "पसंदीदा में जोड़ें", + "copyLink": "कॉपी लिंक" + }, + "blankPageTitle": "रिक्त पेज", + "newPageText": "नया पेज", + "newDocumentText": "नया दस्तावेज़", + "newGridText": "नया ग्रिड", + "newCalendarText": "नया कैलेंडर", + "newBoardText": "नया बोर्ड", + "trash": { + "text": "कचरा", + "restoreAll": "सभी पुनर्स्थापित करें", + "deleteAll": "सभी हटाएँ", + "pageHeader": { + "fileName": "फ़ाइलनाम", + "lastModified": "अंतिम संशोधित", + "created": "बनाया गया" }, - "signUp": { - "buttonText": "साइन अप करें", - "title": "साइन अप करें @:appName", - "getStartedText": "शुरू करे", - "emptyPasswordError": "पासवर्ड खाली नहीं हो सकता", - "repeatPasswordEmptyError": "रिपीट पासवर्ड खाली नहीं हो सकता", - "unmatchedPasswordError": "रिपीट पासवर्ड और पासवर्ड एक नहीं है", - "alreadyHaveAnAccount": "क्या आपके पास पहले से एक खाता मौजूद है?", - "emailHint": "ईमेल", - "passwordHint": "पासवर्ड", - "repeatPasswordHint": "रिपीट पासवर्ड", - "signUpWith": "इसके साथ साइन अप करें:" + "confirmDeleteAll": { + "title": "क्या आप निश्चित रूप से ट्रैश में मौजूद सभी पेज को हटाना चाहते हैं?", + "caption": "यह कार्रवाई पूर्ववत नहीं की जा सकती।" }, - "signIn": { - "loginTitle": "लॉग इन करें @:appName", - "loginButtonText": "लॉग इन करें", - "loginStartWithAnonymous": "एक अज्ञात सत्र से प्रारंभ करें", - "continueAnonymousUser": "अज्ञात सत्र जारी रखें", - "buttonText": "साइन इन", - "forgotPassword": "पासवर्ड भूल गए?", - "emailHint": "ईमेल", - "passwordHint": "पासवर्ड", - "dontHaveAnAccount": "कोई खाता नहीं है?", - "repeatPasswordEmptyError": "रिपीट पासवर्ड खाली नहीं हो सकता", - "unmatchedPasswordError": "रिपीट पासवर्ड और पासवर्ड एक नहीं है", - "syncPromptMessage": "डेटा को सिंक करने में कुछ समय लग सकता है. कृपया इस पेज को बंद न करें", - "or": "या", - "LogInWithGoogle": "गूगल से लॉग इन करें", - "LogInWithGithub": "गिटहब से लॉग इन करें", - "LogInWithDiscord": "डिस्कॉर्ड से लॉग इन करें", - "signInWith": "इसके साथ साइन इन करें:" + "confirmRestoreAll": { + "title": "क्या आप निश्चित रूप से ट्रैश में सभी पेज को पुनर्स्थापित करना चाहते हैं?", + "caption": "यह कार्रवाई पूर्ववत नहीं की जा सकती।" + } + }, + "deletePagePrompt": { + "text": "यह पेज कूड़ेदान में है", + "restore": "पुनर्स्थापित पेज", + "deletePermanent": "स्थायी रूप से हटाएँ" + }, + "dialogCreatePageNameHint": "पेज का नाम", + "questionBubble": { + "shortcuts": "शॉर्टकट", + "whatsNew": "क्या नया है?", + "help": "सहायता", + "markdown": "markdown", + "debug": { + "name": "डीबग जानकारी", + "success": "डिबग जानकारी क्लिपबोर्ड पर कॉपी की गई!", + "fail": "डिबग जानकारी को क्लिपबोर्ड पर कॉपी करने में असमर्थ" }, - "workspace": { - "chooseWorkspace": "अपना कार्यक्षेत्र चुनें", - "create": "कार्यक्षेत्र बनाएं", - "reset": "कार्यक्षेत्र रीसेट करें", - "resetWorkspacePrompt": "कार्यक्षेत्र को रीसेट करने से उसमें मौजूद सभी पृष्ठ और डेटा हट जाएंगे। क्या आप वाकई कार्यक्षेत्र को रीसेट करना चाहते हैं? वैकल्पिक रूप से, आप कार्यक्षेत्र को पुनर्स्थापित करने के लिए सहायता टीम से संपर्क कर सकते हैं", - "hint": "कार्यक्षेत्र", - "notFoundError": "कार्यस्थल नहीं मिला" + "feedback": "जानकारी देना" + }, + "menuAppHeader": { + "moreButtonToolTip": "निकालें, नाम बदलें, और भी बहुत कुछ...", + "addPageTooltip": "जल्दी से अंदर एक पेज जोड़ें", + "defaultNewPageName": "शीर्षकहीन", + "renameDialog": "नाम बदलें" + }, + "toolbar": { + "undo": "अनडू", + "redo": "रीडू", + "bold": "बोल्ड", + "italic": "इटैलिक", + "underline": "अंडरलाइन", + "strike": "स्ट्राइकथ्रू", + "numList": "क्रमांकित सूची", + "bulletList": "बुलेट सूची", + "checkList": "चेकलिस्ट", + "inlineCode": "इनलाइन कोड", + "quote": "कोट", + "header": "हेडर", + "highlight": "हाइलाइट करें", + "color": "रंग", + "addLink": "लिंक जोड़ें", + "link": "लिंक" + }, + "tooltip": { + "lightMode": "लाइट मोड पर स्विच करें", + "darkMode": "डार्क मोड पर स्विच करें", + "openAsPage": "पेज के रूप में खोलें", + "addNewRow": "एक नई पंक्ति जोड़ें", + "openMenu": "मेनू खोलने के लिए क्लिक करें", + "dragRow": "पंक्ति को पुनः व्यवस्थित करने के लिए देर तक दबाएँ", + "viewDataBase": "डेटाबेस देखें", + "referencePage": "यह {name} रफेरेंसेड है", + "addBlockBelow": "नीचे एक ब्लॉक जोड़ें" + }, + "sideBar": { + "closeSidebar": "साइड बार बंद करें", + "openSidebar": "साइड बार खोलें", + "personal": "व्यक्तिगत", + "favorites": "पसंदीदा", + "clickToHidePersonal": "व्यक्तिगत अनुभाग को छिपाने के लिए क्लिक करें", + "clickToHideFavorites": "पसंदीदा अनुभाग को छिपाने के लिए क्लिक करें", + "addAPage": "एक पेज जोड़ें" + }, + "notifications": { + "export": { + "markdown": "आपका नोट मार्कडाउन के रूप में सफलतापूर्वक निर्यात कर दिया गया है।", + "path": "दस्तावेज़/प्रवाह" + } + }, + "contactsPage": { + "title": "संपर्क", + "whatsHappening": "इस सप्ताह क्या हो रहा है?", + "addContact": "संपर्क जोड़ें", + "editContact": "संपर्क संपादित करें" + }, + "button": { + "ok": "ठीक है", + "cancel": "रद्द करें", + "signIn": "साइन इन करें", + "signOut": "साइन आउट करें", + "complete": "पूर्ण", + "save": "सेव", + "generate": "उत्पन्न करें", + "esc": "एस्केप", + "keep": "रखें", + "tryAgain": "फिर से प्रयास करें", + "discard": "त्यागें", + "replace": "बदलें", + "insertBelow": "नीचे डालें", + "upload": "अपलोड करें", + "edit": "संपादित करें", + "delete": "हटाएं", + "duplicate": "डुप्लिकेट", + "done": "किया", + "putback": "पुन्हा डालिए" + }, + "label": { + "welcome": "आपका स्वागत है", + "firstName": "पहला नाम", + "middleName": "मध्य नाम", + "lastName": "अंतिम नाम", + "stepX": "स्टेप {X}" + }, + "oAuth": { + "err": { + "failedTitle": "आपके खाते से जुड़ने में असमर्थ।", + "failedMsg": "कृपया सुनिश्चित करें कि आपने अपने ब्राउज़र में साइन-इन प्रक्रिया पूरी कर ली है।" }, - "shareAction": { - "buttonText": "शेयर", - "workInProgress": "जल्द आ रहा है", - "markdown": "markdown", - "csv": "csv", - "copyLink": "लिंक कॉपी करें" + "google": { + "title": "Google साइन-इन", + "instruction1": "अपने Google संपर्कों को आयात करने के लिए, आपको अपने वेब ब्राउज़र का उपयोग करके इस एप्लिकेशन को अधिकृत करना होगा।", + "instruction2": "आइकन पर क्लिक करके या टेक्स्ट का चयन करके इस कोड को अपने क्लिपबोर्ड पर कॉपी करें:", + "instruction3": "अपने वेब ब्राउज़र में निम्नलिखित लिंक पर जाएँ, और उपरोक्त कोड दर्ज करें", + "instruction4": "साइनअप पूरा होने पर नीचे दिया गया बटन दबाएँ:" + } + }, + "settings": { + "title": "सेटिंग्स", + "menu": { + "appearance": "दृश्य", + "language": "भाषा", + "user": "उपयोगकर्ता", + "files": "फ़ाइलें", + "open": "सेटिंग्स खोलें", + "logout": "लॉगआउट", + "logoutPrompt": "क्या आप निश्चित रूप से लॉगआउट करना चाहते हैं?", + "selfEncryptionLogoutPrompt": "क्या आप वाकई लॉग आउट करना चाहते हैं? कृपया सुनिश्चित करें कि आपने एन्क्रिप्शन रहस्य की कॉपी बना ली है", + "syncSetting": "सिंक सेटिंग", + "enableSync": "सिंक इनेबल करें", + "enableEncrypt": "डेटा एन्क्रिप्ट करें", + "enableEncryptPrompt": "इस रहस्य के साथ अपने डेटा को सुरक्षित करने के लिए एन्क्रिप्शन सक्रिय करें। इसे सुरक्षित रूप से संग्रहीत करें; एक बार सक्षम होने के बाद, इसे बंद नहीं किया जा सकता है। यदि खो जाता है, तो आपका डेटा पुनर्प्राप्त नहीं किया जा सकता है। कॉपी करने के लिए क्लिक करें", + "inputEncryptPrompt": "कृपया अपना एन्क्रिप्शन रहस्य दर्ज करें", + "clickToCopySecret": "गुप्त कॉपी बनाने के लिए क्लिक करें", + "inputTextFieldHint": "आपका रहस्य", + "historicalUserList": "उपयोगकर्ता लॉगिन इतिहास", + "historicalUserListTooltip": "यह सूची आपके अज्ञात खातों को प्रदर्शित करती है। आप किसी खाते का विवरण देखने के लिए उस पर क्लिक कर सकते हैं। 'आरंभ करें' बटन पर क्लिक करके अज्ञात खाते बनाए जाते हैं", + "openHistoricalUser": "अज्ञात खाता खोलने के लिए क्लिक करें" }, - "moreAction": { - "small": "छोटा", - "medium": "मध्यम", - "large": "बड़ा", - "fontSize": "अक्षर का आकर", - "import": "आयात", - "moreOptions": "अधिक विकल्प" - }, - "importPanel": { - "textAndMarkdown": "Text & Markdown", - "documentFromV010": "Document from v0.1.0", - "databaseFromV010": "Database from v0.1.0", - "csv": "CSV", - "database": "Database" - }, - "disclosureAction": { - "rename": "नाम बदलें", - "delete": "हटाएं", - "duplicate": "डुप्लीकेट", - "unfavorite": "पसंदीदा से हटाएँ", - "favorite": "पसंदीदा में जोड़ें", - "openNewTab": "एक नए टैब में खोलें", - "moveTo": "स्थानांतरित करें", - "addToFavorites": "पसंदीदा में जोड़ें", - "copyLink": "कॉपी लिंक" - }, - "blankPageTitle": "रिक्त पेज", - "newPageText": "नया पेज", - "newDocumentText": "नया दस्तावेज़", - "newGridText": "नया ग्रिड", - "newCalendarText": "नया कैलेंडर", - "newBoardText": "नया बोर्ड", - "trash": { - "text": "कचरा", - "restoreAll": "सभी पुनर्स्थापित करें", - "deleteAll": "सभी हटाएँ", - "pageHeader": { - "fileName": "फ़ाइलनाम", - "lastModified": "अंतिम संशोधित", - "created": "बनाया गया" + "appearance": { + "resetSetting": "इस सेटिंग को रीसेट करें", + "fontFamily": { + "label": "फ़ॉन्ट फॅमिली", + "search": "खोजें" }, - "confirmDeleteAll": { - "title": "क्या आप निश्चित रूप से ट्रैश में मौजूद सभी पेज को हटाना चाहते हैं?", - "caption": "यह कार्रवाई पूर्ववत नहीं की जा सकती।" + "themeMode": { + "label": "थीम मोड", + "light": "लाइट मोड", + "dark": "डार्क मोड", + "system": "सिस्टम के अनुसार अनुकूलित करें" }, - "confirmRestoreAll": { - "title": "क्या आप निश्चित रूप से ट्रैश में सभी पेज को पुनर्स्थापित करना चाहते हैं?", - "caption": "यह कार्रवाई पूर्ववत नहीं की जा सकती।" - } - }, - "deletePagePrompt": { - "text": "यह पेज कूड़ेदान में है", - "restore": "पुनर्स्थापित पेज", - "deletePermanent": "स्थायी रूप से हटाएँ" - }, - "dialogCreatePageNameHint": "पेज का नाम", - "questionBubble": { - "shortcuts": "शॉर्टकट", - "whatsNew": "क्या नया है?", - "help": "सहायता", - "markdown": "markdown", - "debug": { - "name": "डीबग जानकारी", - "success": "डिबग जानकारी क्लिपबोर्ड पर कॉपी की गई!", - "fail": "डिबग जानकारी को क्लिपबोर्ड पर कॉपी करने में असमर्थ" + "layoutDirection": { + "label": "लेआउट दिशा", + "hint": "अपनी स्क्रीन पर सामग्री के प्रवाह को बाएँ से दाएँ या दाएँ से बाएँ नियंत्रित करें।", + "ltr": "एलटीआर", + "rtl": "आरटीएल" }, - "feedback": "जानकारी देना" - }, - "menuAppHeader": { - "moreButtonToolTip": "निकालें, नाम बदलें, और भी बहुत कुछ...", - "addPageTooltip": "जल्दी से अंदर एक पेज जोड़ें", - "defaultNewPageName": "शीर्षकहीन", - "renameDialog": "नाम बदलें" - }, - "toolbar": { - "undo": "अनडू", - "redo": "रीडू", - "bold": "बोल्ड", - "italic": "इटैलिक", - "underline": "अंडरलाइन", - "strike": "स्ट्राइकथ्रू", - "numList": "क्रमांकित सूची", - "bulletList": "बुलेट सूची", - "checkList": "चेकलिस्ट", - "inlineCode": "इनलाइन कोड", - "quote": "कोट", - "header": "हेडर", - "highlight": "हाइलाइट करें", - "color": "रंग", - "addLink": "लिंक जोड़ें", - "link": "लिंक" - }, - "tooltip": { - "lightMode": "लाइट मोड पर स्विच करें", - "darkMode": "डार्क मोड पर स्विच करें", - "openAsPage": "पेज के रूप में खोलें", - "addNewRow": "एक नई पंक्ति जोड़ें", - "openMenu": "मेनू खोलने के लिए क्लिक करें", - "dragRow": "पंक्ति को पुनः व्यवस्थित करने के लिए देर तक दबाएँ", - "viewDataBase": "डेटाबेस देखें", - "referencePage": "यह {name} रफेरेंसेड है", - "addBlockBelow": "नीचे एक ब्लॉक जोड़ें" - }, - "sideBar": { - "closeSidebar": "साइड बार बंद करें", - "openSidebar": "साइड बार खोलें", - "personal": "व्यक्तिगत", - "favorites": "पसंदीदा", - "clickToHidePersonal": "व्यक्तिगत अनुभाग को छिपाने के लिए क्लिक करें", - "clickToHideFavorites": "पसंदीदा अनुभाग को छिपाने के लिए क्लिक करें", - "addAPage": "एक पेज जोड़ें" - }, - "notifications": { - "export": { - "markdown": "आपका नोट मार्कडाउन के रूप में सफलतापूर्वक निर्यात कर दिया गया है।", - "path": "दस्तावेज़/प्रवाह" - } - }, - "contactsPage": { - "title": "संपर्क", - "whatsHappening": "इस सप्ताह क्या हो रहा है?", - "addContact": "संपर्क जोड़ें", - "editContact": "संपर्क संपादित करें" - }, - "button": { - "ok": "ठीक है", - "cancel": "रद्द करें", - "signIn": "साइन इन करें", - "signOut": "साइन आउट करें", - "complete": "पूर्ण", - "save": "सेव", - "generate": "उत्पन्न करें", - "esc": "एस्केप", - "keep": "रखें", - "tryAgain": "फिर से प्रयास करें", - "discard": "त्यागें", - "replace": "बदलें", - "insertBelow": "नीचे डालें", - "upload": "अपलोड करें", - "edit": "संपादित करें", - "delete": "हटाएं", - "duplicate": "डुप्लिकेट", - "done": "किया", - "putback": "पुन्हा डालिए" - }, - "label": { - "welcome": "आपका स्वागत है", - "firstName": "पहला नाम", - "middleName": "मध्य नाम", - "lastName": "अंतिम नाम", - "stepX": "स्टेप {X}" - }, - "oAuth": { - "err": { - "failedTitle": "आपके खाते से जुड़ने में असमर्थ।", - "failedMsg": "कृपया सुनिश्चित करें कि आपने अपने ब्राउज़र में साइन-इन प्रक्रिया पूरी कर ली है।" + "textDirection": { + "label": "डिफ़ॉल्ट वाक्य दिशा", + "hint": "निर्दिष्ट करें कि वाक्य को डिफ़ॉल्ट के रूप में बाएँ या दाएँ से प्रारंभ करना चाहिए।", + "ltr": "एलटीआर", + "rtl": "आरटीएल", + "auto": "ऑटो", + "fallback": "लेआउट दिशा के समान" }, - "google": { - "title": "Google साइन-इन", - "instruction1": "अपने Google संपर्कों को आयात करने के लिए, आपको अपने वेब ब्राउज़र का उपयोग करके इस एप्लिकेशन को अधिकृत करना होगा।", - "instruction2": "आइकन पर क्लिक करके या टेक्स्ट का चयन करके इस कोड को अपने क्लिपबोर्ड पर कॉपी करें:", - "instruction3": "अपने वेब ब्राउज़र में निम्नलिखित लिंक पर जाएँ, और उपरोक्त कोड दर्ज करें", - "instruction4": "साइनअप पूरा होने पर नीचे दिया गया बटन दबाएँ:" - } + "themeUpload": { + "button": "अपलोड करें", + "uploadTheme": "थीम अपलोड करें", + "description": "नीचे दिए गए बटन का उपयोग करके अपनी खुद की AppFlowy थीम अपलोड करें।", + "failure": "जो थीम अपलोड किया गया था उसका प्रारूप अमान्य था।", + "loading": "कृपया तब तक प्रतीक्षा करें जब तक हम आपकी थीम को सत्यापित और अपलोड नहीं कर देते...", + "uploadSuccess": "आपका थीम सफलतापूर्वक अपलोड किया गया", + "deletionFailure": "थीम को हटाने में विफल। इसे मैन्युअल रूप से हटाने का प्रयास करें।", + "filePickerDialogTitle": "एक .flowy_plugin फ़ाइल चुनें", + "urlUploadFailure": "URL खोलने में विफल: {}" + }, + "theme": "थीम", + "builtInsLabel": "डिफ़ॉल्ट थीम", + "pluginsLabel": "प्लगइन्स", + "showNamingDialogWhenCreatingPage": "पेज बनाते समय उसका नाम लेने के लिए डायलॉग देखे" + }, + "files": { + "copy": "कॉपी करें", + "defaultLocation": "फ़ाइलें और डेटा संग्रहण स्थान पढ़ें", + "exportData": "अपना डेटा निर्यात करें", + "doubleTapToCopy": "पथ को कॉपी करने के लिए दो बार टैप करें", + "restoreLocation": "AppFlowy डिफ़ॉल्ट पथ पर रीस्टार्ट करें", + "customizeLocation": "कोई अन्य फ़ोल्डर खोलें", + "restartApp": "परिवर्तनों को प्रभावी बनाने के लिए कृपया ऐप को रीस्टार्ट करें।", + "exportDatabase": "डेटाबेस निर्यात करें", + "selectFiles": "उन फ़ाइलों का चयन करें जिन्हें निर्यात करने की आवश्यकता है", + "selectAll": "सभी का चयन करें", + "deselectAll": "सभी को अचयनित करें", + "createNewFolder": "एक नया फ़ोल्डर बनाएँ", + "createNewFolderDesc": "हमें बताएं कि आप अपना डेटा कहां संग्रहीत करना चाहते हैं", + "defineWhereYourDataIsStored": "परिभाषित करें कि आपका डेटा कहाँ संग्रहीत है", + "open": "खोलें", + "openFolder": "मौजूदा फ़ोल्डर खोलें", + "openFolderDesc": "इसे पढ़ें और इसे अपने मौजूदा AppFlowy फ़ोल्डर में लिखें", + "folderHintText": "फ़ोल्डर का नाम", + "location": "एक नया फ़ोल्डर बनाना", + "locationDesc": "अपने AppFlowy डेटा फ़ोल्डर के लिए एक नाम चुनें", + "browser": "ब्राउज़ करें", + "create": "बनाएँ", + "set": "सेट", + "folderPath": "आपके फ़ोल्डर को संग्रहीत करने का पथ", + "locationCannotBeEmpty": "पथ खाली नहीं हो सकता", + "pathCopiedSnackbar": "फ़ाइल संग्रहण पथ क्लिपबोर्ड पर कॉपी किया गया!", + "changeLocationTooltips": "डेटा निर्देशिका बदलें", + "change": "परिवर्तन", + "openLocationTooltips": "अन्य डेटा निर्देशिका खोलें", + "openCurrentDataFolder": "वर्तमान डेटा निर्देशिका खोलें", + "recoverLocationTooltips": "AppFlowy की डिफ़ॉल्ट डेटा निर्देशिका पर रीसेट करें", + "exportFileSuccess": "फ़ाइल सफलतापूर्वक निर्यात हुई", + "exportFileFail": "फ़ाइल निर्यात विफल रहा!", + "export": "निर्यात" + }, + "user": { + "name": "नाम", + "email": "ईमेल", + "tooltipSelectIcon": "आइकन चुनें", + "selectAnIcon": "एक आइकन चुनें", + "pleaseInputYourOpenAIKey": "कृपया अपनी OpenAI key इनपुट करें", + "clickToLogout": "वर्तमान उपयोगकर्ता को लॉगआउट करने के लिए क्लिक करें" + }, + "shortcuts": { + "shortcutsLabel": "शॉर्टकट", + "command": "कमांड", + "keyBinding": "कीबाइंडिंग", + "addNewCommand": "नया कमांड जोड़ें", + "updateShortcutStep": "इच्छित key संयोजन दबाएँ और ENTER दबाएँ", + "shortcutIsAlreadyUsed": "यह शॉर्टकट पहले से ही इसके लिए उपयोग किया जा चुका है: {conflict}", + "resetToDefault": "डिफ़ॉल्ट कीबाइंडिंग पर रीसेट करें", + "couldNotLoadErrorMsg": "शॉर्टकट लोड नहीं हो सका, पुनः प्रयास करें", + "couldNotSaveErrorMsg": "शॉर्टकट सेव नहीं किये जा सके, पुनः प्रयास करें" + } + }, + "grid": { + "deleteView": "क्या आप वाकई इस दृश्य को हटाना चाहते हैं?", + "createView": "नया", + "title": { + "placeholder": "शीर्षकहीन" }, "settings": { - "title": "सेटिंग्स", - "menu": { - "appearance": "दृश्य", - "language": "भाषा", - "user": "उपयोगकर्ता", - "files": "फ़ाइलें", - "open": "सेटिंग्स खोलें", - "logout": "लॉगआउट", - "logoutPrompt": "क्या आप निश्चित रूप से लॉगआउट करना चाहते हैं?", - "selfEncryptionLogoutPrompt": "क्या आप वाकई लॉग आउट करना चाहते हैं? कृपया सुनिश्चित करें कि आपने एन्क्रिप्शन रहस्य की कॉपी बना ली है", - "syncSetting": "सिंक सेटिंग", - "enableSync": "सिंक इनेबल करें", - "enableEncrypt": "डेटा एन्क्रिप्ट करें", - "enableEncryptPrompt": "इस रहस्य के साथ अपने डेटा को सुरक्षित करने के लिए एन्क्रिप्शन सक्रिय करें। इसे सुरक्षित रूप से संग्रहीत करें; एक बार सक्षम होने के बाद, इसे बंद नहीं किया जा सकता है। यदि खो जाता है, तो आपका डेटा पुनर्प्राप्त नहीं किया जा सकता है। कॉपी करने के लिए क्लिक करें", - "inputEncryptPrompt": "कृपया अपना एन्क्रिप्शन रहस्य दर्ज करें", - "clickToCopySecret": "गुप्त कॉपी बनाने के लिए क्लिक करें", - "inputTextFieldHint": "आपका रहस्य", - "historicalUserList": "उपयोगकर्ता लॉगिन इतिहास", - "historicalUserListTooltip": "यह सूची आपके अज्ञात खातों को प्रदर्शित करती है। आप किसी खाते का विवरण देखने के लिए उस पर क्लिक कर सकते हैं। 'आरंभ करें' बटन पर क्लिक करके अज्ञात खाते बनाए जाते हैं", - "openHistoricalUser": "अज्ञात खाता खोलने के लिए क्लिक करें" - }, - "appearance": { - "resetSetting": "इस सेटिंग को रीसेट करें", - "fontFamily": { - "label": "फ़ॉन्ट फॅमिली", - "search": "खोजें" - }, - "themeMode": { - "label": "थीम मोड", - "light": "लाइट मोड", - "dark": "डार्क मोड", - "system": "सिस्टम के अनुसार अनुकूलित करें" - }, - "layoutDirection": { - "label": "लेआउट दिशा", - "hint": "अपनी स्क्रीन पर सामग्री के प्रवाह को बाएँ से दाएँ या दाएँ से बाएँ नियंत्रित करें।", - "ltr": "एलटीआर", - "rtl": "आरटीएल" - }, - "textDirection": { - "label": "डिफ़ॉल्ट वाक्य दिशा", - "hint": "निर्दिष्ट करें कि वाक्य को डिफ़ॉल्ट के रूप में बाएँ या दाएँ से प्रारंभ करना चाहिए।", - "ltr": "एलटीआर", - "rtl": "आरटीएल", - "auto": "ऑटो", - "fallback": "लेआउट दिशा के समान" - }, - "themeUpload": { - "button": "अपलोड करें", - "uploadTheme": "थीम अपलोड करें", - "description": "नीचे दिए गए बटन का उपयोग करके अपनी खुद की AppFlowy थीम अपलोड करें।", - "failure": "जो थीम अपलोड किया गया था उसका प्रारूप अमान्य था।", - "loading": "कृपया तब तक प्रतीक्षा करें जब तक हम आपकी थीम को सत्यापित और अपलोड नहीं कर देते...", - "uploadSuccess": "आपका थीम सफलतापूर्वक अपलोड किया गया", - "deletionFailure": "थीम को हटाने में विफल। इसे मैन्युअल रूप से हटाने का प्रयास करें।", - "filePickerDialogTitle": "एक .flowy_plugin फ़ाइल चुनें", - "urlUploadFailure": "URL खोलने में विफल: {}" - }, - "theme": "थीम", - "builtInsLabel": "डिफ़ॉल्ट थीम", - "pluginsLabel": "प्लगइन्स", - "showNamingDialogWhenCreatingPage": "पेज बनाते समय उसका नाम लेने के लिए डायलॉग देखे" - }, - "files": { - "copy": "कॉपी करें", - "defaultLocation": "फ़ाइलें और डेटा संग्रहण स्थान पढ़ें", - "exportData": "अपना डेटा निर्यात करें", - "doubleTapToCopy": "पथ को कॉपी करने के लिए दो बार टैप करें", - "restoreLocation": "AppFlowy डिफ़ॉल्ट पथ पर रीस्टार्ट करें", - "customizeLocation": "कोई अन्य फ़ोल्डर खोलें", - "restartApp": "परिवर्तनों को प्रभावी बनाने के लिए कृपया ऐप को रीस्टार्ट करें।", - "exportDatabase": "डेटाबेस निर्यात करें", - "selectFiles": "उन फ़ाइलों का चयन करें जिन्हें निर्यात करने की आवश्यकता है", - "selectAll": "सभी का चयन करें", - "deselectAll": "सभी को अचयनित करें", - "createNewFolder": "एक नया फ़ोल्डर बनाएँ", - "createNewFolderDesc": "हमें बताएं कि आप अपना डेटा कहां संग्रहीत करना चाहते हैं", - "defineWhereYourDataIsStored": "परिभाषित करें कि आपका डेटा कहाँ संग्रहीत है", - "open": "खोलें", - "openFolder": "मौजूदा फ़ोल्डर खोलें", - "openFolderDesc": "इसे पढ़ें और इसे अपने मौजूदा AppFlowy फ़ोल्डर में लिखें", - "folderHintText": "फ़ोल्डर का नाम", - "location": "एक नया फ़ोल्डर बनाना", - "locationDesc": "अपने AppFlowy डेटा फ़ोल्डर के लिए एक नाम चुनें", - "browser": "ब्राउज़ करें", - "create": "बनाएँ", - "set": "सेट", - "folderPath": "आपके फ़ोल्डर को संग्रहीत करने का पथ", - "locationCannotBeEmpty": "पथ खाली नहीं हो सकता", - "pathCopiedSnackbar": "फ़ाइल संग्रहण पथ क्लिपबोर्ड पर कॉपी किया गया!", - "changeLocationTooltips": "डेटा निर्देशिका बदलें", - "change": "परिवर्तन", - "openLocationTooltips": "अन्य डेटा निर्देशिका खोलें", - "openCurrentDataFolder": "वर्तमान डेटा निर्देशिका खोलें", - "recoverLocationTooltips": "AppFlowy की डिफ़ॉल्ट डेटा निर्देशिका पर रीसेट करें", - "exportFileSuccess": "फ़ाइल सफलतापूर्वक निर्यात हुई", - "exportFileFail": "फ़ाइल निर्यात विफल रहा!", - "export": "निर्यात" - }, - "user": { - "name": "नाम", - "email": "ईमेल", - "tooltipSelectIcon": "आइकन चुनें", - "selectAnIcon": "एक आइकन चुनें", - "pleaseInputYourOpenAIKey": "कृपया अपनी OpenAI key इनपुट करें", - "clickToLogout": "वर्तमान उपयोगकर्ता को लॉगआउट करने के लिए क्लिक करें" - }, - "shortcuts": { - "shortcutsLabel": "शॉर्टकट", - "command": "कमांड", - "keyBinding": "कीबाइंडिंग", - "addNewCommand": "नया कमांड जोड़ें", - "updateShortcutStep": "इच्छित key संयोजन दबाएँ और ENTER दबाएँ", - "shortcutIsAlreadyUsed": "यह शॉर्टकट पहले से ही इसके लिए उपयोग किया जा चुका है: {conflict}", - "resetToDefault": "डिफ़ॉल्ट कीबाइंडिंग पर रीसेट करें", - "couldNotLoadErrorMsg": "शॉर्टकट लोड नहीं हो सका, पुनः प्रयास करें", - "couldNotSaveErrorMsg": "शॉर्टकट सेव नहीं किये जा सके, पुनः प्रयास करें" - } - }, - "grid": { - "deleteView": "क्या आप वाकई इस दृश्य को हटाना चाहते हैं?", - "createView": "नया", - "title": { - "placeholder": "शीर्षकहीन" - }, - "settings": { - "filter": "फ़िल्टर", - "sort": "क्रमबद्ध करें", - "sortBy": "क्रमबद्ध करें", - "properties": "गुण", - "reorderPropertiesTooltip": "गुणों को पुनः व्यवस्थित करने के लिए खींचें", - "group": "समूह", - "addFilter": "फ़िल्टर करें...", - "deleteFilter": "फ़िल्टर हटाएँ", - "filterBy": "फ़िल्टरबाय...", - "typeAValue": "एक वैल्यू टाइप करें...", - "layout": "लेआउट", - "databaseLayout": "लेआउट" - }, - "textFilter": { - "contains": "शामिल है", - "doesNotContain": "इसमें शामिल नहीं है", - "endsWith": "समाप्त होता है", - "startWith": "से प्रारंभ होता है", - "is": "है", - "isNot": "नहीं है", - "isEmpty": "खाली है", - "isNotEmpty": "खाली नहीं है", - "choicechipPrefix": { - "isNot": "नहीं है", - "startWith": "से प्रारंभ होता है", - "endWith": "के साथ समाप्त होता है", - "isEmpty": "खाली है", - "isNotEmpty": "खाली नहीं है" - } - }, - "checkboxFilter": { - "isChecked": "चेक किया गया", - "isUnchecked": "अनचेक किया हुआ", - "choicechipPrefix": { - "is": "है" - } - }, - "checklistFilter": { - "isComplete": "पूर्ण है", - "isIncomplted": "अपूर्ण है" - }, - "singleSelectOptionFilter": { - "is": "है", - "isNot": "नहीं है", - "isEmpty": "खाली है", - "isNotEmpty": "खाली नहीं है" - }, - "multiSelectOptionFilter": { - "contains": "शामिल है", - "doesNotContain": "इसमें शामिल नहीं है", - "isEmpty": "खाली है", - "isNotEmpty": "खाली नहीं है" - }, - "field": { - "hide": "छिपाएँ", - "insertLeft": "बायाँ सम्मिलित करें", - "insertRight": "दाएँ सम्मिलित करें", - "duplicate": "डुप्लिकेट", - "delete": "हटाएं", - "textFieldName": "लेख", - "checkboxFieldName": "चेकबॉक्स", - "dateFieldName": "दिनांक", - "updatedAtFieldName": "अंतिम संशोधित समय", - "createdAtFieldName": "बनाने का समय", - "numberFieldName": "संख्या", - "singleSelectFieldName": "चुनाव", - "multiSelectFieldName": "बहु चुनाव", - "urlFieldName": "URL", - "checklistFieldName": "चेकलिस्ट", - "numberFormat": "संख्या प्रारूप", - "dateFormat": "दिनांक प्रारूप", - "includeTime": "समय शामिल करें", - "isRange": "अंतिम तिथि", - "dateFormatFriendly": "माह दिन, वर्ष", - "dateFormatISO": "वर्ष-महीना-दिन", - "dateFormatLocal": "महीना/दिन/वर्ष", - "dateFormatUS": "वर्ष/महीना/दिन", - "dateFormatDayMonthYear": "दिन/माह/वर्ष", - "timeFormat": "समय प्रारूप", - "invalidTimeFormat": "अमान्य प्रारूप", - "timeFormatTwelveHour": "१२ घंटा", - "timeFormatTwentyFourHour": "२४ घंटे", - "clearDate": "तिथि मिटाए", - "addSelectOption": "एक विकल्प जोड़ें", - "optionTitle": "विकल्प", - "addOption": "विकल्प जोड़ें", - "editProperty": "डेटा का प्रकार संपादित करें", - "newProperty": "नया डेटा का प्रकार", - "deleteFieldPromptMessage": "क्या आप निश्चित हैं? यह डेटा का प्रकार हटा दी जाएगी", - "newColumn": "नया कॉलम" - }, - "sort": { - "ascending": "असेंडिंग", - "descending": "डिसेंडिंग", - "deleteAllSorts": "सभी प्रकार हटाएँ", - "addSort": "सॉर्ट जोड़ें" - }, - "row": { - "duplicate": "डुप्लिकेट", - "delete": "डिलीट", - "titlePlaceholder": "शीर्षकहीन", - "textPlaceholder": "रिक्त", - "copyProperty": "डेटा के प्रकार को क्लिपबोर्ड पर कॉपी किया गया", - "count": "गिनती", - "newRow": "नई पंक्ति", - "action": "कार्रवाई", - "add": "नीचे जोड़ें पर क्लिक करें", - "drag": "स्थानांतरित करने के लिए खींचें" - }, - "selectOption": { - "create": "बनाएँ", - "purpleColor": "बैंगनी", - "pinkColor": "गुलाबी", - "lightPinkColor": "हल्का गुलाबी", - "orangeColor": "नारंगी", - "yellowColor": "पीला", - "limeColor": "नींबू", - "greenColor": "हरा", - "aquaColor": "एक्वा", - "blueColor": "नीला", - "deleteTag": "टैग हटाएँ", - "colorPanelTitle": "रंग", - "panelTitle": "एक विकल्प चुनें या एक बनाएं", - "searchOption": "एक विकल्प खोजें", - "searchOrCreateOption": "कोई विकल्प खोजें या बनाएँ...", - "createNew": "एक नया बनाएँ", - "orSelectOne": "या एक विकल्प चुनें" - }, - "checklist": { - "taskHint": "कार्य विवरण", - "addNew": "एक नया कार्य जोड़ें", - "submitNewTask": "बनाएँ" - }, - "menuName": "ग्रिड", - "referencedGridPrefix": "का दृश्य" - }, - "document": { - "menuName": "दस्तावेज़ ", - "date": { - "timeHintTextInTwelveHour": "01:00 PM", - "timeHintTextInTwentyFourHour": "13:00" - }, - "slashMenu": { - "board": { - "selectABoardToLinkTo": "लिंक करने के लिए एक बोर्ड चुनें", - "createANewBoard": "एक नया बोर्ड बनाएं" - }, - "grid": { - "selectAGridToLinkTo": "लिंक करने के लिए एक ग्रिड चुनें", - "createANewGrid": "एक नया ग्रिड बनाएं" - }, - "calendar": { - "selectACalendarToLinkTo": "लिंक करने के लिए एक कैलेंडर चुनें", - "createANewCalendar": "एक नया कैलेंडर बनाएं" - } - }, - "selectionMenu": { - "outline": "रूपरेखा", - "codeBlock": "कोड ब्लॉक" - }, - "plugins": { - "referencedBoard": "रेफेरेंस बोर्ड", - "referencedGrid": "रेफेरेंस ग्रिड", - "referencedCalendar": "रेफेरेंस कैलेंडर", - "autoGeneratorMenuItemName": "OpenAI लेखक", - "autoGeneratorTitleName": "OpenAI: AI को कुछ भी लिखने के लिए कहें...", - "autoGeneratorLearnMore": "और जानें", - "autoGeneratorGenerate": "उत्पन्न करें", - "autoGeneratorHintText": "OpenAI से पूछें...", - "autoGeneratorCantGetOpenAIKey": "OpenAI key नहीं मिल सकी", - "autoGeneratorRewrite": "पुनः लिखें", - "smartEdit": "AI सहायक", - "openAI": "OpenAI", - "smartEditFixSpelling": "वर्तनी ठीक करें", - "warning": "⚠️ AI प्रतिक्रियाएँ गलत या भ्रामक हो सकती हैं।", - "smartEditSummarize": "सारांश", - "smartEditImproveWriting": "लेख में सुधार करें", - "smartEditMakeLonger": "लंबा बनाएं", - "smartEditCouldNotFetchResult": "OpenAI से परिणाम प्राप्त नहीं किया जा सका", - "smartEditCouldNotFetchKey": "OpenAI key नहीं लायी जा सकी", - "smartEditDisabled": "सेटिंग्स में OpenAI कनेक्ट करें", - "discardResponse": "क्या आप AI प्रतिक्रियाओं को छोड़ना चाहते हैं?", - "createInlineMathEquation": "समीकरण बनाएं", - "toggleList": "सूची टॉगल करें", - "cover": { - "changeCover": "कवर बदलें", - "colors": "रंग", - "images": "छवियां", - "clearAll": "सभी साफ़ करें", - "abstract": "सार", - "addCover": "कवर जोड़ें", - "addLocalImage": "स्थानीय छवि जोड़ें", - "invalidImageUrl": "अमान्य छवि URL", - "failedToAddImageToGallery": "गैलरी में छवि जोड़ने में विफल", - "enterImageUrl": "छवि URL दर्ज करें", - "add": "जोड़ें", - "back": "पीछे", - "saveToGallery": "गैलरी में सेव करे", - "removeIcon": "आइकन हटाएँ", - "pasteImageUrl": "छवि URL चिपकाएँ", - "or": "या", - "pickFromFiles": "फ़ाइलों में से चुनें", - "couldNotFetchImage": "छवि नहीं लाया जा सका", - "imageSavingFailed": "छवि सहेजना विफल", - "addIcon": "आइकन जोड़ें", - "coverRemoveAlert": "हटाने के बाद इसे कवर से हटा दिया जाएगा।", - "alertDialogConfirmation": "क्या आप निश्चित हैं, आप जारी रखना चाहते हैं?" - }, - "mathEquation": { - "addMathEquation": "गणित समीकरण जोड़ें", - "editMathEquation": "गणित समीकरण संपादित करें" - }, - "optionAction": { - "click": "क्लिक करें", - "toOpenMenu": "मेनू खोलने के लिए", - "delete": "हटाएं", - "duplicate": "डुप्लिकेट", - "turnInto": "टर्नइनटू", - "moveUp": "ऊपर बढ़ें", - "moveDown": "नीचे जाएँ", - "color": "रंग", - "align": "संरेखित करें", - "left": "बांया", - "center": "केंद्र", - "right": "सही", - "defaultColor": "डिफ़ॉल्ट" - }, - "image": { - "copiedToPasteBoard": "छवि लिंक को क्लिपबोर्ड पर कॉपी कर दिया गया है" - }, - "outline": { - "addHeadingToCreateOutline": "सामग्री की तालिका बनाने के लिए शीर्षक जोड़ें।" - }, - "table": { - "addAfter": "बाद में जोड़ें", - "addBefore": "पहले जोड़ें", - "delete": "हटाएं", - "clear": "साफ़ करें", - "duplicate": "डुप्लिकेट", - "bgColor": "पृष्ठभूमि रंग" - }, - "contextMenu": { - "copy": "कॉपी करें", - "cut": "कट करे", - "paste": "पेस्ट करें" - } - }, - "textBlock": { - "placeholder": "कमांड के लिए '/' टाइप करें" - }, - "title": { - "placeholder": "शीर्षकहीन" - }, - "imageBlock": { - "placeholder": "छवि जोड़ने के लिए क्लिक करें", - "अपलोड करें": { - "label": "अपलोड करें", - "placeholder": "छवि अपलोड करने के लिए क्लिक करें" - }, - "url": { - "label": "छवि URL ", - "placeholder": "छवि URL दर्ज करें" - }, - "support": "छवि आकार सीमा 5 एमबी है। समर्थित प्रारूप: JPEG, PNG, GIF, SVG", - "error": { - "invalidImage": "अमान्य छवि", - "invalidImageSize": "छवि का आकार 5MB से कम होना चाहिए", - "invalidImageFormat": "छवि प्रारूप समर्थित नहीं है। समर्थित प्रारूप: JPEG, PNG, GIF, SVG", - "invalidImageUrl": "अमान्य छवि URL" - } - }, - "codeBlock": { - "language": { - "label": "भाषा", - "placeholder": "भाषा चुनें" - } - }, - "inlineLink": { - "placeholder": "लिंक चिपकाएँ या टाइप करें", - "openInNewTab": "नए टैब में खोलें", - "copyLink": "लिंक कॉपी करें", - "removeLink": "लिंक हटाएँ", - "url": { - "label": "लिंक URL", - "placeholder": "लिंक URL दर्ज करें" - }, - "title": { - "label": "लिंक शीर्षक", - "placeholder": "लिंक शीर्षक दर्ज करें" - } - }, - "mention": { - "placeholder": "किसी व्यक्ति या पेज या दिनांक का उल्लेख करें...", - "page": { - "label": "पेज से लिंक करें", - "tooltip": "पेज खोलने के लिए क्लिक करें" - } - }, - "toolbar": { - "resetToDefaultFont": "डिफ़ॉल्ट पर रीसेट करें" - } - }, - "board": { - "column": { - "createNewCard": "नया" - }, - "menuName": "बोर्ड", - "referencedBoardPrefix": "का दृश्य" - }, - "calendar": { - "menuName": "कैलेंडर", - "defaultNewCalendarTitle": "शीर्षकहीन", - "newEventButtonTooltip": "एक नया ईवेंट जोड़ें", - "navigation": { - "today": "आज", - "jumpToday": "जम्प टू टुडे", - "previousMonth": "पिछला महीना", - "nextMonth": "अगले महीने" - }, - "settings": { - "showWeekNumbers": "सप्ताह संख्याएँ दिखाएँ", - "showWeekends": "सप्ताहांत दिखाएँ", - "firstDayOfWeek": "सप्ताह प्रारंभ करें", - "layoutDateField": "लेआउट कैलेंडर", - "noDateTitle": "कोई दिनांक नहीं", - "noDateHint": "अनिर्धारित घटनाएँ यहाँ दिखाई देंगी", - "clickToAdd": "कैलेंडर में जोड़ने के लिए क्लिक करें", - "name": "कैलेंडर लेआउट" - }, - "referencedCalendarPrefix": "का दृश्य" - }, - "errorDialog": { - "title": "AppFlowy error", - "howToFixFallback": "असुविधा के लिए हमें खेद है! हमारे GitHub पेज पर एक मुद्दा सबमिट करें जो आपकी error का वर्णन करता है।", - "github": "GitHub पर देखें " - }, - "search": { - "label": "खोजें", - "placeholder": { - "actions": "खोज क्रियाएँ..." - } - }, - "message": { - "copy": { - "success": "कॉपी सफलता पूर्ण हुआ!", - "fail": "कॉपी करने में असमर्थ" - } - }, - "unSupportBlock": "वर्तमान संस्करण इस ब्लॉक का समर्थन नहीं करता है।", - "views": { - "deleteContentTitle": "क्या आप वाकई {pageType} को हटाना चाहते हैं?", - "deleteContentCaption": "यदि आप इस {pageType} को हटाते हैं, तो आप इसे ट्रैश से पुनर्स्थापित कर सकते हैं।" - }, - "colors": { - "custom": "कस्टम", - "default": "डिफ़ॉल्ट", - "red": "लाल", - "orange": "नारंगी", - "yellow": "पीला", - "green": "हरा", - "blue": "नीला", - "purple": "बैंगनी", - "pink": "गुलाबी", - "brown": "भूरा", - "gray": "ग्रे" - }, - "emoji": { "filter": "फ़िल्टर", - "random": "रैंडम", - "selectSkinTone": "त्वचा का रंग चुनें", - "remove": "इमोजी हटाएं", - "categories": { - "smileys": "स्माइलीज़ एंड इमोशन", - "people": "लोग और शरीर", - "animals": "जानवर और प्रकृति", - "food": "खाद्य और पेय", - "activities": "गतिविधियाँ", - "places": "यात्रा एवं स्थान", - "objects": "ऑब्जेक्ट्स", - "symbols": "प्रतीक", - "flags": "झंडे", - "nature": "प्रकृति", - "frequentlyUsed": "अक्सर उपयोग किया जाता है" + "sort": "क्रमबद्ध करें", + "sortBy": "क्रमबद्ध करें", + "properties": "गुण", + "reorderPropertiesTooltip": "गुणों को पुनः व्यवस्थित करने के लिए खींचें", + "group": "समूह", + "addFilter": "फ़िल्टर करें...", + "deleteFilter": "फ़िल्टर हटाएँ", + "filterBy": "फ़िल्टरबाय...", + "typeAValue": "एक वैल्यू टाइप करें...", + "layout": "लेआउट", + "databaseLayout": "लेआउट" + }, + "textFilter": { + "contains": "शामिल है", + "doesNotContain": "इसमें शामिल नहीं है", + "endsWith": "समाप्त होता है", + "startWith": "से प्रारंभ होता है", + "is": "है", + "isNot": "नहीं है", + "isEmpty": "खाली है", + "isNotEmpty": "खाली नहीं है", + "choicechipPrefix": { + "isNot": "नहीं है", + "startWith": "से प्रारंभ होता है", + "endWith": "के साथ समाप्त होता है", + "isEmpty": "खाली है", + "isNotEmpty": "खाली नहीं है" } + }, + "checkboxFilter": { + "isChecked": "चेक किया गया", + "isUnchecked": "अनचेक किया हुआ", + "choicechipPrefix": { + "is": "है" + } + }, + "checklistFilter": { + "isComplete": "पूर्ण है", + "isIncomplted": "अपूर्ण है" + }, + "selectOptionFilter": { + "is": "है", + "isNot": "नहीं है", + "contains": "शामिल है", + "doesNotContain": "इसमें शामिल नहीं है", + "isEmpty": "खाली है", + "isNotEmpty": "खाली नहीं है" + }, + "field": { + "hide": "छिपाएँ", + "insertLeft": "बायाँ सम्मिलित करें", + "insertRight": "दाएँ सम्मिलित करें", + "duplicate": "डुप्लिकेट", + "delete": "हटाएं", + "textFieldName": "लेख", + "checkboxFieldName": "चेकबॉक्स", + "dateFieldName": "दिनांक", + "updatedAtFieldName": "अंतिम संशोधित समय", + "createdAtFieldName": "बनाने का समय", + "numberFieldName": "संख्या", + "singleSelectFieldName": "चुनाव", + "multiSelectFieldName": "बहु चुनाव", + "urlFieldName": "URL", + "checklistFieldName": "चेकलिस्ट", + "numberFormat": "संख्या प्रारूप", + "dateFormat": "दिनांक प्रारूप", + "includeTime": "समय शामिल करें", + "isRange": "अंतिम तिथि", + "dateFormatFriendly": "माह दिन, वर्ष", + "dateFormatISO": "वर्ष-महीना-दिन", + "dateFormatLocal": "महीना/दिन/वर्ष", + "dateFormatUS": "वर्ष/महीना/दिन", + "dateFormatDayMonthYear": "दिन/माह/वर्ष", + "timeFormat": "समय प्रारूप", + "invalidTimeFormat": "अमान्य प्रारूप", + "timeFormatTwelveHour": "१२ घंटा", + "timeFormatTwentyFourHour": "२४ घंटे", + "clearDate": "तिथि मिटाए", + "addSelectOption": "एक विकल्प जोड़ें", + "optionTitle": "विकल्प", + "addOption": "विकल्प जोड़ें", + "editProperty": "डेटा का प्रकार संपादित करें", + "newProperty": "नया डेटा का प्रकार", + "deleteFieldPromptMessage": "क्या आप निश्चित हैं? यह डेटा का प्रकार हटा दी जाएगी", + "newColumn": "नया कॉलम" + }, + "sort": { + "ascending": "असेंडिंग", + "descending": "डिसेंडिंग", + "deleteAllSorts": "सभी प्रकार हटाएँ", + "addSort": "सॉर्ट जोड़ें" + }, + "row": { + "duplicate": "डुप्लिकेट", + "delete": "डिलीट", + "titlePlaceholder": "शीर्षकहीन", + "textPlaceholder": "रिक्त", + "copyProperty": "डेटा के प्रकार को क्लिपबोर्ड पर कॉपी किया गया", + "count": "गिनती", + "newRow": "नई पंक्ति", + "action": "कार्रवाई", + "add": "नीचे जोड़ें पर क्लिक करें", + "drag": "स्थानांतरित करने के लिए खींचें" + }, + "selectOption": { + "create": "बनाएँ", + "purpleColor": "बैंगनी", + "pinkColor": "गुलाबी", + "lightPinkColor": "हल्का गुलाबी", + "orangeColor": "नारंगी", + "yellowColor": "पीला", + "limeColor": "नींबू", + "greenColor": "हरा", + "aquaColor": "एक्वा", + "blueColor": "नीला", + "deleteTag": "टैग हटाएँ", + "colorPanelTitle": "रंग", + "panelTitle": "एक विकल्प चुनें या एक बनाएं", + "searchOption": "एक विकल्प खोजें", + "searchOrCreateOption": "कोई विकल्प खोजें या बनाएँ...", + "createNew": "एक नया बनाएँ", + "orSelectOne": "या एक विकल्प चुनें" + }, + "checklist": { + "taskHint": "कार्य विवरण", + "addNew": "एक नया कार्य जोड़ें", + "submitNewTask": "बनाएँ" + }, + "menuName": "ग्रिड", + "referencedGridPrefix": "का दृश्य" + }, + "document": { + "menuName": "दस्तावेज़ ", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "लिंक करने के लिए एक बोर्ड चुनें", + "createANewBoard": "एक नया बोर्ड बनाएं" + }, + "grid": { + "selectAGridToLinkTo": "लिंक करने के लिए एक ग्रिड चुनें", + "createANewGrid": "एक नया ग्रिड बनाएं" + }, + "calendar": { + "selectACalendarToLinkTo": "लिंक करने के लिए एक कैलेंडर चुनें", + "createANewCalendar": "एक नया कैलेंडर बनाएं" + } + }, + "selectionMenu": { + "outline": "रूपरेखा", + "codeBlock": "कोड ब्लॉक" + }, + "plugins": { + "referencedBoard": "रेफेरेंस बोर्ड", + "referencedGrid": "रेफेरेंस ग्रिड", + "referencedCalendar": "रेफेरेंस कैलेंडर", + "autoGeneratorMenuItemName": "OpenAI लेखक", + "autoGeneratorTitleName": "OpenAI: AI को कुछ भी लिखने के लिए कहें...", + "autoGeneratorLearnMore": "और जानें", + "autoGeneratorGenerate": "उत्पन्न करें", + "autoGeneratorHintText": "OpenAI से पूछें...", + "autoGeneratorCantGetOpenAIKey": "OpenAI key नहीं मिल सकी", + "autoGeneratorRewrite": "पुनः लिखें", + "smartEdit": "AI सहायक", + "openAI": "OpenAI", + "smartEditFixSpelling": "वर्तनी ठीक करें", + "warning": "⚠️ AI प्रतिक्रियाएँ गलत या भ्रामक हो सकती हैं।", + "smartEditSummarize": "सारांश", + "smartEditImproveWriting": "लेख में सुधार करें", + "smartEditMakeLonger": "लंबा बनाएं", + "smartEditCouldNotFetchResult": "OpenAI से परिणाम प्राप्त नहीं किया जा सका", + "smartEditCouldNotFetchKey": "OpenAI key नहीं लायी जा सकी", + "smartEditDisabled": "सेटिंग्स में OpenAI कनेक्ट करें", + "discardResponse": "क्या आप AI प्रतिक्रियाओं को छोड़ना चाहते हैं?", + "createInlineMathEquation": "समीकरण बनाएं", + "toggleList": "सूची टॉगल करें", + "cover": { + "changeCover": "कवर बदलें", + "colors": "रंग", + "images": "छवियां", + "clearAll": "सभी साफ़ करें", + "abstract": "सार", + "addCover": "कवर जोड़ें", + "addLocalImage": "स्थानीय छवि जोड़ें", + "invalidImageUrl": "अमान्य छवि URL", + "failedToAddImageToGallery": "गैलरी में छवि जोड़ने में विफल", + "enterImageUrl": "छवि URL दर्ज करें", + "add": "जोड़ें", + "back": "पीछे", + "saveToGallery": "गैलरी में सेव करे", + "removeIcon": "आइकन हटाएँ", + "pasteImageUrl": "छवि URL चिपकाएँ", + "or": "या", + "pickFromFiles": "फ़ाइलों में से चुनें", + "couldNotFetchImage": "छवि नहीं लाया जा सका", + "imageSavingFailed": "छवि सहेजना विफल", + "addIcon": "आइकन जोड़ें", + "coverRemoveAlert": "हटाने के बाद इसे कवर से हटा दिया जाएगा।", + "alertDialogConfirmation": "क्या आप निश्चित हैं, आप जारी रखना चाहते हैं?" + }, + "mathEquation": { + "addMathEquation": "गणित समीकरण जोड़ें", + "editMathEquation": "गणित समीकरण संपादित करें" + }, + "optionAction": { + "click": "क्लिक करें", + "toOpenMenu": "मेनू खोलने के लिए", + "delete": "हटाएं", + "duplicate": "डुप्लिकेट", + "turnInto": "टर्नइनटू", + "moveUp": "ऊपर बढ़ें", + "moveDown": "नीचे जाएँ", + "color": "रंग", + "align": "संरेखित करें", + "left": "बांया", + "center": "केंद्र", + "right": "सही", + "defaultColor": "डिफ़ॉल्ट" + }, + "image": { + "copiedToPasteBoard": "छवि लिंक को क्लिपबोर्ड पर कॉपी कर दिया गया है" + }, + "outline": { + "addHeadingToCreateOutline": "सामग्री की तालिका बनाने के लिए शीर्षक जोड़ें।" + }, + "table": { + "addAfter": "बाद में जोड़ें", + "addBefore": "पहले जोड़ें", + "delete": "हटाएं", + "clear": "साफ़ करें", + "duplicate": "डुप्लिकेट", + "bgColor": "पृष्ठभूमि रंग" + }, + "contextMenu": { + "copy": "कॉपी करें", + "cut": "कट करे", + "paste": "पेस्ट करें" + } + }, + "textBlock": { + "placeholder": "कमांड के लिए '/' टाइप करें" + }, + "title": { + "placeholder": "शीर्षकहीन" + }, + "imageBlock": { + "placeholder": "छवि जोड़ने के लिए क्लिक करें", + "अपलोड करें": { + "label": "अपलोड करें", + "placeholder": "छवि अपलोड करने के लिए क्लिक करें" + }, + "url": { + "label": "छवि URL ", + "placeholder": "छवि URL दर्ज करें" + }, + "support": "छवि आकार सीमा 5 एमबी है। समर्थित प्रारूप: JPEG, PNG, GIF, SVG", + "error": { + "invalidImage": "अमान्य छवि", + "invalidImageSize": "छवि का आकार 5MB से कम होना चाहिए", + "invalidImageFormat": "छवि प्रारूप समर्थित नहीं है। समर्थित प्रारूप: JPEG, PNG, GIF, SVG", + "invalidImageUrl": "अमान्य छवि URL" + } + }, + "codeBlock": { + "language": { + "label": "भाषा", + "placeholder": "भाषा चुनें" + } + }, + "inlineLink": { + "placeholder": "लिंक चिपकाएँ या टाइप करें", + "openInNewTab": "नए टैब में खोलें", + "copyLink": "लिंक कॉपी करें", + "removeLink": "लिंक हटाएँ", + "url": { + "label": "लिंक URL", + "placeholder": "लिंक URL दर्ज करें" + }, + "title": { + "label": "लिंक शीर्षक", + "placeholder": "लिंक शीर्षक दर्ज करें" + } + }, + "mention": { + "placeholder": "किसी व्यक्ति या पेज या दिनांक का उल्लेख करें...", + "page": { + "label": "पेज से लिंक करें", + "tooltip": "पेज खोलने के लिए क्लिक करें" + } + }, + "toolbar": { + "resetToDefaultFont": "डिफ़ॉल्ट पर रीसेट करें" } - } \ No newline at end of file + }, + "board": { + "column": { + "createNewCard": "नया" + }, + "menuName": "बोर्ड", + "referencedBoardPrefix": "का दृश्य" + }, + "calendar": { + "menuName": "कैलेंडर", + "defaultNewCalendarTitle": "शीर्षकहीन", + "newEventButtonTooltip": "एक नया ईवेंट जोड़ें", + "navigation": { + "today": "आज", + "jumpToday": "जम्प टू टुडे", + "previousMonth": "पिछला महीना", + "nextMonth": "अगले महीने" + }, + "settings": { + "showWeekNumbers": "सप्ताह संख्याएँ दिखाएँ", + "showWeekends": "सप्ताहांत दिखाएँ", + "firstDayOfWeek": "सप्ताह प्रारंभ करें", + "layoutDateField": "लेआउट कैलेंडर", + "noDateTitle": "कोई दिनांक नहीं", + "noDateHint": "अनिर्धारित घटनाएँ यहाँ दिखाई देंगी", + "clickToAdd": "कैलेंडर में जोड़ने के लिए क्लिक करें", + "name": "कैलेंडर लेआउट" + }, + "referencedCalendarPrefix": "का दृश्य" + }, + "errorDialog": { + "title": "AppFlowy error", + "howToFixFallback": "असुविधा के लिए हमें खेद है! हमारे GitHub पेज पर एक मुद्दा सबमिट करें जो आपकी error का वर्णन करता है।", + "github": "GitHub पर देखें " + }, + "search": { + "label": "खोजें", + "placeholder": { + "actions": "खोज क्रियाएँ..." + } + }, + "message": { + "copy": { + "success": "कॉपी सफलता पूर्ण हुआ!", + "fail": "कॉपी करने में असमर्थ" + } + }, + "unSupportBlock": "वर्तमान संस्करण इस ब्लॉक का समर्थन नहीं करता है।", + "views": { + "deleteContentTitle": "क्या आप वाकई {pageType} को हटाना चाहते हैं?", + "deleteContentCaption": "यदि आप इस {pageType} को हटाते हैं, तो आप इसे ट्रैश से पुनर्स्थापित कर सकते हैं।" + }, + "colors": { + "custom": "कस्टम", + "default": "डिफ़ॉल्ट", + "red": "लाल", + "orange": "नारंगी", + "yellow": "पीला", + "green": "हरा", + "blue": "नीला", + "purple": "बैंगनी", + "pink": "गुलाबी", + "brown": "भूरा", + "gray": "ग्रे" + }, + "emoji": { + "filter": "फ़िल्टर", + "random": "रैंडम", + "selectSkinTone": "त्वचा का रंग चुनें", + "remove": "इमोजी हटाएं", + "categories": { + "smileys": "स्माइलीज़ एंड इमोशन", + "people": "लोग और शरीर", + "animals": "जानवर और प्रकृति", + "food": "खाद्य और पेय", + "activities": "गतिविधियाँ", + "places": "यात्रा एवं स्थान", + "objects": "ऑब्जेक्ट्स", + "symbols": "प्रतीक", + "flags": "झंडे", + "nature": "प्रकृति", + "frequentlyUsed": "अक्सर उपयोग किया जाता है" + } + } +} \ No newline at end of file diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index 2f3c0cfece..1f7fc8718c 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -325,13 +325,9 @@ "isComplete": "teljes", "isIncomplted": "hiányos" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Is", "isNot": "Nem", - "isEmpty": "Üres", - "isNotEmpty": "Nem üres" - }, - "multiSelectOptionFilter": { "contains": "Tartalmaz", "doesNotContain": "Nem tartalmaz", "isEmpty": "Üres", diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index bf37c0c7ac..14e85ada70 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -452,13 +452,9 @@ "isComplete": "selesai", "isIncomplted": "tidak lengkap" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Adalah", "isNot": "Tidak", - "isEmpty": "Kosong", - "isNotEmpty": "Tidak kosong" - }, - "multiSelectOptionFilter": { "contains": "Mengandung", "doesNotContain": "Tidak mengandung", "isEmpty": "Kosong", diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index 74d72997f6..f7adf22039 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -183,9 +183,7 @@ "dragRow": "Premere a lungo per riordinare la riga", "viewDataBase": "Visualizza banca dati", "referencePage": "Questo {nome} è referenziato", - "addBlockBelow": "Aggiungi un blocco qui sotto", - "urlLaunchAccessory": "Apri nel browser", - "urlCopyAccessory": "Copia l'URL" + "addBlockBelow": "Aggiungi un blocco qui sotto" }, "sideBar": { "closeSidebar": "Close sidebar", @@ -518,13 +516,9 @@ "isComplete": "è completo", "isIncomplted": "è incompleto" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "È", "isNot": "Non è", - "isEmpty": "È vuoto", - "isNotEmpty": "Non è vuoto" - }, - "multiSelectOptionFilter": { "contains": "Contiene", "doesNotContain": "Non contiene", "isEmpty": "È vuoto", @@ -619,8 +613,8 @@ "cannotFindCreatableField": "Impossibile trovare un campo adatto per l'ordinamento", "deleteAllSorts": "Elimina tutti gli ordinamenti", "addSort": "Aggiungi ordinamento", - "deleteSort": "Elimina ordinamento", - "removeSorting": "Si desidera rimuovere l'ordinamento?" + "removeSorting": "Si desidera rimuovere l'ordinamento?", + "deleteSort": "Elimina ordinamento" }, "row": { "duplicate": "Duplicare", @@ -665,6 +659,10 @@ "hideComplete": "Nascondi le attività completate", "showComplete": "Mostra tutte le attività" }, + "url": { + "launch": "Apri nel browser", + "copy": "Copia l'URL" + }, "menuName": "Griglia", "referencedGridPrefix": "Vista di", "calculate": "Calcolare", @@ -1264,4 +1262,4 @@ "userIcon": "Icona utente" }, "noLogFiles": "Non ci sono file di log" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index 0a8364a2dc..251fb50d01 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -110,7 +110,7 @@ "caption": "この操作は元に戻すことができません。" }, "mobile": { - "empty": "ゴミ箱を殻にする", + "empty": "ゴミ箱を空にする", "emptyDescription": "削除されたファイルはありません", "isDeleted": "削除済み" } @@ -412,13 +412,9 @@ "isComplete": "完了", "isIncomplted": "未完了" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "等しい", "isNot": "等しくない", - "isEmpty": "空である", - "isNotEmpty": "空ではない" - }, - "multiSelectOptionFilter": { "contains": "を含む", "doesNotContain": "を含まない", "isEmpty": "空である", diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index 4570f95d23..60dbea5f7a 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -324,13 +324,9 @@ "isComplete": "완료되었습니다", "isIncomplted": "불완전하다" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "~이다", "isNot": "아니다", - "isEmpty": "비었다", - "isNotEmpty": "비어 있지 않음" - }, - "multiSelectOptionFilter": { "contains": "포함", "doesNotContain": "포함되어 있지 않다", "isEmpty": "비었다", diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 89657c91c8..21b52053db 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -454,13 +454,9 @@ "isComplete": "jest kompletna", "isIncomplted": "jest niekompletna" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Jest", "isNot": "Nie jest", - "isEmpty": "Jest pusty", - "isNotEmpty": "Nie jest pusty" - }, - "multiSelectOptionFilter": { "contains": "Zawiera", "doesNotContain": "Nie zawiera", "isEmpty": "Jest pusty", diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 3f8f334b2e..680edcc525 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -180,9 +180,7 @@ "dragRow": "Pressione e segure para reordenar a linha", "viewDataBase": "Visualizar banco de dados", "referencePage": "Esta {name} é uma referência", - "addBlockBelow": "Adicione um bloco abaixo", - "urlLaunchAccessory": "Abrir com o navegador", - "urlCopyAccessory": "Copiar URL" + "addBlockBelow": "Adicione um bloco abaixo" }, "sideBar": { "closeSidebar": "Fechar barra lateral", @@ -513,13 +511,9 @@ "isComplete": "está completo", "isIncomplted": "está imcompleto" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Está", "isNot": "Não está", - "isEmpty": "Está vazio", - "isNotEmpty": "Não está vazio" - }, - "multiSelectOptionFilter": { "contains": "Contém", "doesNotContain": "Não contém", "isEmpty": "Está vazio", @@ -648,6 +642,10 @@ "hideComplete": "Ocultar tarefas concluídas", "showComplete": "Mostrar todas as tarefas" }, + "url": { + "launch": "Abrir com o navegador", + "copy": "Copiar URL" + }, "menuName": "Grade", "referencedGridPrefix": "Vista de" }, diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index f20c5dba52..3ececbedc3 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -426,13 +426,9 @@ "isComplete": "está completo", "isIncomplted": "está incompleto" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "É", "isNot": "não é", - "isEmpty": "Está vazia", - "isNotEmpty": "Não está vazio" - }, - "multiSelectOptionFilter": { "contains": "contém", "doesNotContain": "Não contém", "isEmpty": "Está vazia", diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 68942931d0..c14663f2dc 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -187,9 +187,7 @@ "dragRow": "Перетащите для изменения порядка строк", "viewDataBase": "Просмотр базы данных", "referencePage": "Ссылки на {name}", - "addBlockBelow": "Добавьте блок ниже", - "urlLaunchAccessory": "Открыть в браузере", - "urlCopyAccessory": "Скопировать URL" + "addBlockBelow": "Добавьте блок ниже" }, "sideBar": { "closeSidebar": "Закрыть боковое меню", @@ -493,14 +491,14 @@ "typeAValue": "Введите значение...", "layout": "Вид", "databaseLayout": "Вид базы данных", - "viewList": "Представление базы данных", "editView": "Редактировать представление", "boardSettings": "Настройки доски", "calendarSettings": "Настройки календаря", "createView": "Новое представление", "duplicateView": "Дублировать представление", "deleteView": "Удалить представление", - "numberOfVisibleFields": "{} показано" + "numberOfVisibleFields": "{} показано", + "viewList": "Представление базы данных" }, "textFilter": { "contains": "Содержит", @@ -530,13 +528,9 @@ "isComplete": "завершено", "isIncomplted": "не завершено" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Является", "isNot": "Не является", - "isEmpty": "Пусто", - "isNotEmpty": "Не пусто" - }, - "multiSelectOptionFilter": { "contains": "Содержит", "doesNotContain": "Не содержит", "isEmpty": "Пусто", @@ -689,6 +683,10 @@ "hideComplete": "Скрыть выполненные задачи", "showComplete": "Показать все задачи" }, + "url": { + "launch": "Открыть в браузере", + "copy": "Скопировать URL" + }, "relation": { "relatedDatabasePlaceLabel": "Связанная база данных", "relatedDatabasePlaceholder": "Пусто", @@ -1313,4 +1311,4 @@ "userIcon": "Пользовательская иконка" }, "noLogFiles": "Нет файлов журналов" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/sv-SE.json b/frontend/resources/translations/sv-SE.json index c7791cfea5..b15861fec3 100644 --- a/frontend/resources/translations/sv-SE.json +++ b/frontend/resources/translations/sv-SE.json @@ -2,41 +2,49 @@ "appName": "AppFlowy", "defaultUsername": "Jag", "welcomeText": "Välkommen till @:appName", - "githubStarText": "Stjärnmärk på GitHub", + "failedToOpenUrl": "Det gick inte att öppna webbadressen: {}", + "githubStarText": "Stjärna på GitHub", "subscribeNewsletterText": "Prenumerera på nyhetsbrev", - "letsGoButtonText": "Kör igång", - "title": "Namn", + "letsGoButtonText": "Snabbstart", + "title": "Titel", + "welcomeTo": "Välkommen till", "youCanAlso": "Du kan också", "and": "och", "blockActions": { "addBelowTooltip": "Klicka för att lägga till nedan", "addAboveCmd": "Alt+klicka", "addAboveMacCmd": "Alternativ+klicka", - "addAboveTooltip": "att lägga till ovan" + "addAboveTooltip": "att lägga till ovan", + "dragTooltip": "Dra för att flytta", + "openMenuTooltip": "Klicka för att öppna menyn" }, "signUp": { - "buttonText": "Registrera dig", - "title": "Registrera dig på @:appName", - "getStartedText": "Sätt igång", - "emptyPasswordError": "Lösenordet kan inte vara tomt", - "repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt", - "unmatchedPasswordError": "Upprepat lösenord är inte samma som det första", + "buttonText": "Registera", + "title": "Registrera dig för @:appName", + "getStartedText": "Kom igång", + "emptyPasswordError": "Lösenordet får inte vara tomt", + "repeatPasswordEmptyError": "Upprepa lösenordet får inte vara tomt", + "unmatchedPasswordError": "Upprepa lösenord är inte detsamma som lösenord", "alreadyHaveAnAccount": "Har du redan ett konto?", - "emailHint": "E-post", + "emailHint": "Epost", "passwordHint": "Lösenord", - "repeatPasswordHint": "Upprepa lösenordet" + "repeatPasswordHint": "Upprepa lösenord", + "signUpWith": "Registrera med" }, "signIn": { "loginTitle": "Logga in till @:appName", "loginButtonText": "Logga in", - "buttonText": "Registrering", - "forgotPassword": "Glömt lösenordet?", - "emailHint": "E-post", + "buttonText": "Registrera", + "forgotPassword": "Glömt ditt lösenord?", + "emailHint": "Epost", "passwordHint": "Lösenord", "dontHaveAnAccount": "Har du inget konto?", "repeatPasswordEmptyError": "Upprepat lösenord kan inte vara tomt", "unmatchedPasswordError": "Upprepat lösenord är inte samma som det första", - "loginAsGuestButtonText": "Komma igång" + "loginAsGuestButtonText": "Komma igång", + "continueAnonymousUser": "Fortsätt med en anonym session", + "loginStartWithAnonymous": "Börja med en anonym session", + "signingInText": "Loggar in..." }, "workspace": { "create": "Skapa arbetsyta", @@ -177,7 +185,26 @@ "duplicate": "Duplicera", "putback": "Ställ tillbaka", "ok": "OK", - "cancel": "Avbryt" + "cancel": "Avbryt", + "update": "Uppdatering", + "share": "Dela", + "removeFromFavorites": "Ta bort från favoriter", + "addToFavorites": "Lägg till i favoriter", + "rename": "Döp om", + "helpCenter": "Hjälpcenter", + "add": "Lägg till", + "yes": "Ja", + "clear": "Klar", + "remove": "Ta bort", + "dontRemove": "Ta inte bort", + "copyLink": "Kopiera länk", + "align": "Jämka", + "login": "Logga in", + "logout": "Logga ut", + "back": "Tillbaka", + "signInGoogle": "Logga in med Google", + "signInGithub": "Logga in med Github", + "signInDiscord": "Logga in med Discord" }, "label": { "welcome": "Välkommen!", @@ -188,15 +215,15 @@ }, "oAuth": { "err": { - "failedTitle": "Kan inte ansluta till ditt konto.", - "failedMsg": "Tillse att du har slutfört registreringsprocessen i din webbläsare." + "failedMsg": "Se till att du har slutfört inloggningsprocessen i din webbläsare.", + "failedTitle": "Det går inte att ansluta till ditt konto." }, "google": { - "title": "GOOGLE-inloggning", - "instruction1": "För att kunna importera dina Google-kontakter, måste du auktorisera detta program med hjälp av din webbläsare.", - "instruction2": "Kopiera den här koden till urklipp genom att klicka på ikonen eller genom att markera texten:", - "instruction3": "Gå till följande länk i din webbläsare, och ange ovanstående kod:", - "instruction4": "Tryck på nedanstående knapp när du slutfört registreringen:" + "instruction1": "För att kunna importera dina Google-kontakter måste du auktorisera denna applikation med din webbläsare.", + "instruction2": "Kopiera den här koden till ditt urklipp genom att klicka på ikonen eller välja texten:", + "instruction3": "Navigera till följande länk i din webbläsare och ange koden ovan:", + "instruction4": "Tryck på knappen nedan när du har slutfört registreringen:", + "title": "GOOGLE LOGGA IN" } }, "settings": { @@ -206,8 +233,53 @@ "language": "Språk", "user": "Användare", "files": "Filer", - "open": "Öppna inställningarna", - "supabaseSetting": "Supabase-inställning" + "open": "Öppna Inställningar", + "supabaseSetting": "Supabase-inställning", + "appFlowyCloudUrlCanNotBeEmpty": "Molnets webbadress får inte vara tom", + "changeServerTip": "När du har bytt server måste du klicka på omstartsknappen för att ändringarna ska träda i kraft", + "clickToCopy": "Klicka för att kopiera", + "clickToCopySecret": "Klicka för att kopiera hemlighet", + "cloudLocal": "Lokal", + "cloudServerType": "Molnserver", + "cloudServerTypeTip": "Observera att det kan logga ut ditt nuvarande konto efter att ha bytt molnserver", + "cloudSettings": "Molninställningar", + "cloudSupabase": "Supabase", + "cloudSupabaseAnonKey": "Supabase anonym nyckel", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Anon-nyckeln kan inte vara tom", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "Webbadressen för supabase kan inte vara tom", + "cloudURL": "Grund-URL", + "cloudURLHint": "Ange grundadressen till din server", + "cloudWSURL": "Websocket URL", + "cloudWSURLHint": "Infoga websocket-adressen till din server", + "configServerGuide": "Efter att ha valt `Quick Start`, navigera till `Settings` och sedan \"Cloud Settings\" för att konfigurera din egen värdserver.", + "configServerSetting": "Konfigurera dina serverinställningar", + "customPathPrompt": "Att lagra AppFlowy-datamappen i en molnsynkroniserad mapp som Google Drive kan innebära risker. Om databasen i den här mappen nås eller ändras från flera platser samtidigt, kan det resultera i synkroniseringskonflikter och potentiell datakorruption", + "enableEncrypt": "Kryptera data", + "enableEncryptPrompt": "Aktivera kryptering för att säkra dina data med denna hemlighet. Förvara det säkert; när den väl är aktiverad kan den inte stängas av. Om din data går förlorad blir den omöjlig att återställa. Klicka för att kopiera", + "enableSync": "Aktivera synkronisering", + "historicalUserList": "Användarinloggningshistorik", + "historicalUserListTooltip": "Den här listan visar dina anonyma konton. Du kan klicka på ett konto för att se dess detaljer. Anonyma konton skapas genom att klicka på knappen \"Kom igång\".", + "importAppFlowyData": "Importera data från extern AppFlowy-mapp", + "importAppFlowyDataDescription": "Kopiera data från en extern AppFlowy-datamapp och importera den till den aktuella AppFlowy-datamappen", + "importFailed": "Det gick inte att importera AppFlowy-datamappen", + "importGuide": "För ytterligare information, snälla se det refererade dokumentet", + "importingAppFlowyDataTip": "Dataimport pågår. Stäng inte appen", + "importSuccess": "AppFlowy-datamappen har importerats", + "inputEncryptPrompt": "Vänligen ange din krypteringshemlighet för", + "inputTextFieldHint": "Din hemlighet", + "invalidCloudURLScheme": "Ogiltigt schema", + "logout": "Logga ut", + "logoutPrompt": "Är du säker på att logga ut?", + "notifications": "Aviseringar", + "openHistoricalUser": "Klicka för att öppna det anonyma kontot", + "restartApp": "Omstart", + "restartAppTip": "Starta om programmet för att ändringarna ska träda i kraft. Observera att detta kan logga ut ditt nuvarande konto", + "selfEncryptionLogoutPrompt": "Är du säker på att du vill logga ut? Se till att du har kopierat krypteringshemligheten", + "selfHostContent": "dokument", + "selfHostEnd": "för vägledning om hur du själv är värd för din egen server", + "selfHostStart": "Om du inte har en server, vänligen se", + "syncSetting": "Synkroniseringsinställning" }, "appearance": { "fontFamily": { @@ -322,13 +394,9 @@ "isComplete": "är komplett", "isIncomplted": "är ofullständig" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Är", "isNot": "Är inte", - "isEmpty": "Är tom", - "isNotEmpty": "Är inte tom" - }, - "multiSelectOptionFilter": { "contains": "Innehåller", "doesNotContain": "Innehåller inte", "isEmpty": "Är tom", @@ -595,4 +663,4 @@ "deleteContentTitle": "Är du säker på att du vill ta bort {pageType}?", "deleteContentCaption": "om du tar bort denna {pageType} kan du återställa den från papperskorgen." } -} +} \ No newline at end of file diff --git a/frontend/resources/translations/th-TH.json b/frontend/resources/translations/th-TH.json index 0f8a9d8161..993846528f 100644 --- a/frontend/resources/translations/th-TH.json +++ b/frontend/resources/translations/th-TH.json @@ -177,9 +177,7 @@ "dragRow": "กดค้างเพื่อเรียงลำดับแถวใหม่", "viewDataBase": "ดูฐานข้อมูล", "referencePage": "{name} ถูกอ้างอิงถึง", - "addBlockBelow": "เพิ่มบล็อกด้านล่าง", - "urlLaunchAccessory": "เปิดในเบราว์เซอร์", - "urlCopyAccessory": "คัดลอก URL" + "addBlockBelow": "เพิ่มบล็อกด้านล่าง" }, "sideBar": { "closeSidebar": "ปิดแถบด้านข้าง", @@ -480,13 +478,9 @@ "isComplete": "เสร็จสมบูรณ์", "isIncomplted": "ไม่เสร็จสมบูรณ์" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "เป็น", "isNot": "ไม่เป็น", - "isEmpty": "ว่างเปล่า", - "isNotEmpty": "ไม่ว่างเปล่า" - }, - "multiSelectOptionFilter": { "contains": "ประกอบด้วย", "doesNotContain": "ไม่ประกอบด้วย", "isEmpty": "ว่างเปล่า", @@ -557,7 +551,7 @@ "showHiddenFields": { "one": "แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์", "many": "แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์", - "other": "แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์" + "other": "แสดงฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์" }, "hideHiddenFields": { "one": "ซ่อนฟิลด์ที่ซ่อนอยู่ {count} ฟิลด์", @@ -614,6 +608,10 @@ "hideComplete": "ซ่อนงานเสร็จ", "showComplete": "แสดงงานทั้งหมด" }, + "url": { + "launch": "เปิดในเบราว์เซอร์", + "copy": "คัดลอก URL" + }, "menuName": "ตาราง", "referencedGridPrefix": "มุมมองของ" }, @@ -630,11 +628,11 @@ }, "grid": { "selectAGridToLinkTo": "เลือกตารางเพื่อเชื่อมโยง", - "createANewGrid": "สร้างตารางใหม่" + "createANewGrid": "สร้างตารางใหม่" }, "calendar": { "selectACalendarToLinkTo": "เลือกปฏิทินเพื่อเชื่อมโยง", - "createANewCalendar": "สร้างปฏิทินใหม่" + "createANewCalendar": "สร้างปฏิทินใหม่" }, "document": { "selectADocumentToLinkTo": "เลือกเอกสารเพื่อเชื่อมโยง" @@ -764,7 +762,7 @@ }, "stability_ai": { "label": "สร้างรูปภาพจาก Stability AI", - "placeholder": "โปรดระบุคำขอใช้ Stability AI สร้างรูปภาพ" + "placeholder": "โปรดระบุคำขอใช้ Stability AI สร้างรูปภาพ" }, "support": "ขนาดรูปภาพจำกัดอยู่ที่ 5MB รูปแบบที่รองรับ: JPEG, PNG, GIF, SVG", "error": { diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index c33713ca61..00968ad4c5 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -454,13 +454,9 @@ "isComplete": "Tamamlanmış", "isIncomplted": "Tamamlanmamış" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Şu olan", "isNot": "Şu olmayan", - "isEmpty": "Boş olan", - "isNotEmpty": "Boş olmayan" - }, - "multiSelectOptionFilter": { "contains": "Şunu içeren", "doesNotContain": "Şunu içermeyen", "isEmpty": "Boş olan", diff --git a/frontend/resources/translations/uk-UA.json b/frontend/resources/translations/uk-UA.json index c714e62a88..3c36875f96 100644 --- a/frontend/resources/translations/uk-UA.json +++ b/frontend/resources/translations/uk-UA.json @@ -281,7 +281,6 @@ "auto": "АВТО", "fallback": "Такий же, як і напрямок макету" }, - "themeUpload": { "button": "Завантажити", "uploadTheme": "Завантажити тему", @@ -347,7 +346,6 @@ "exportFileFail": "Помилка експорту файлу!", "export": "Експорт" }, - "user": { "name": "Ім'я", "email": "Електронна пошта", @@ -416,13 +414,9 @@ "isComplete": "є завершено", "isIncomplted": "є незавершено" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "є", "isNot": "не є", - "isEmpty": "порожнє", - "isNotEmpty": "не порожнє" - }, - "multiSelectOptionFilter": { "contains": "Містить", "doesNotContain": "Не містить", "isEmpty": "порожнє", diff --git a/frontend/resources/translations/ur.json b/frontend/resources/translations/ur.json index 0ec8763bcf..1d4f936d37 100644 --- a/frontend/resources/translations/ur.json +++ b/frontend/resources/translations/ur.json @@ -391,7 +391,7 @@ "isComplete": "مکمل ہے", "isIncomplted": "نامکمل ہے" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "ہے", "isNot": "نہیں ہے", "isEmpty": "خالی ہے", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 8609c0ff57..72a19ab795 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -175,8 +175,7 @@ "openMenu": "Bấm để mở menu", "dragRow": "Nhấn và giữ để sắp xếp lại hàng", "viewDataBase": "Xem cơ sở dữ liệu", - "referencePage": "{name} này được tham chiếu", - "urlCopyAccessory": "Sao chép URL" + "referencePage": "{name} này được tham chiếu" }, "sideBar": { "closeSidebar": "Đóng thanh bên", @@ -492,13 +491,9 @@ "isComplete": "hoàn tất", "isIncomplted": "chưa hoàn tất" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "Là", "isNot": "Không phải", - "isEmpty": "Rỗng", - "isNotEmpty": "Không rỗng" - }, - "multiSelectOptionFilter": { "contains": "Chứa", "doesNotContain": "Không chứa", "isEmpty": "Rỗng", @@ -612,6 +607,9 @@ "searchOrCreateOption": "Tìm kiếm hoặc tạo một tùy chọn...", "tagName": "Tên thẻ" }, + "url": { + "copy": "Sao chép URL" + }, "menuName": "Lưới" }, "document": { diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index da547efc5d..31003230e2 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -187,9 +187,7 @@ "dragRow": "长按重新排序该行", "viewDataBase": "查看数据库", "referencePage": "这个 {name} 已被引用", - "addBlockBelow": "在下面添加一个块", - "urlLaunchAccessory": "在浏览器中打开", - "urlCopyAccessory": "复制链接" + "addBlockBelow": "在下面添加一个块" }, "sideBar": { "closeSidebar": "关闭侧边栏", @@ -533,13 +531,9 @@ "isComplete": "已完成", "isIncomplted": "未完成" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "是", "isNot": "不是", - "isEmpty": "为空", - "isNotEmpty": "不为空" - }, - "multiSelectOptionFilter": { "contains": "包含", "doesNotContain": "不包含", "isEmpty": "为空", @@ -644,8 +638,8 @@ "descending": "降序", "deleteAllSorts": "删除所有排序", "addSort": "添加排序", - "deleteSort": "取消排序", - "removeSorting": "你确定要移除排序吗?" + "removeSorting": "你确定要移除排序吗?", + "deleteSort": "取消排序" }, "row": { "duplicate": "复制", @@ -691,6 +685,10 @@ "hideComplete": "隐藏已完成的任务", "showComplete": "显示所有任务" }, + "url": { + "launch": "在浏览器中打开", + "copy": "复制链接" + }, "relation": { "emptySearchResult": "无结果" }, @@ -1291,4 +1289,4 @@ "userIcon": "用户图标" }, "noLogFiles": "没有日志文件" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 563aeb478d..42d043756e 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -181,9 +181,7 @@ "dragRow": "長按以重新排序列", "viewDataBase": "檢視資料庫", "referencePage": "這個 {name} 已被引用", - "addBlockBelow": "在下方新增一個區塊", - "urlLaunchAccessory": "在瀏覽器中開啟", - "urlCopyAccessory": "複製網址" + "addBlockBelow": "在下方新增一個區塊" }, "sideBar": { "closeSidebar": "關閉側欄", @@ -515,13 +513,9 @@ "isComplete": "已完成", "isIncomplted": "未完成" }, - "singleSelectOptionFilter": { + "selectOptionFilter": { "is": "是", "isNot": "不是", - "isEmpty": "為空", - "isNotEmpty": "不為空" - }, - "multiSelectOptionFilter": { "contains": "包含", "doesNotContain": "不包含", "isEmpty": "為空", @@ -650,6 +644,10 @@ "hideComplete": "隱藏已完成任務", "showComplete": "顯示所有任務" }, + "url": { + "launch": "在瀏覽器中開啟", + "copy": "複製網址" + }, "menuName": "網格", "referencedGridPrefix": "檢視", "calculate": "計算", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index c378e5fb28..f4e3d86950 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -133,12 +133,6 @@ dependencies = [ "alloc-no-stdlib", ] -[[package]] -name = "allocator-api2" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" - [[package]] name = "android-tzdata" version = "0.1.1" @@ -163,7 +157,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "bincode", @@ -174,8 +168,10 @@ dependencies = [ "serde_repr", "thiserror", "tokio", + "tsify", "url", "uuid", + "wasm-bindgen", ] [[package]] @@ -673,7 +669,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "again", "anyhow", @@ -683,8 +679,11 @@ dependencies = [ "brotli", "bytes", "chrono", + "client-websocket", "collab", "collab-entity", + "collab-rt-entity", + "collab-rt-protocol", "database-entity", "futures-core", "futures-util", @@ -693,11 +692,8 @@ dependencies = [ "gotrue-entity", "governor", "mime", - "mime_guess", "parking_lot 0.12.1", "prost", - "realtime-entity", - "realtime-protocol", "reqwest", "scraper 0.17.1", "semver", @@ -713,11 +709,28 @@ dependencies = [ "url", "uuid", "wasm-bindgen-futures", - "websocket", "workspace-template", "yrs", ] +[[package]] +name = "client-websocket" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "futures-channel", + "futures-util", + "http", + "httparse", + "js-sys", + "percent-encoding", + "thiserror", + "tokio", + "tokio-tungstenite", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "cmd_lib" version = "1.3.0" @@ -746,7 +759,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-trait", @@ -762,6 +775,7 @@ dependencies = [ "tokio", "tokio-stream", "tracing", + "unicode-segmentation", "web-sys", "yrs", ] @@ -769,7 +783,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-trait", @@ -777,12 +791,13 @@ dependencies = [ "collab", "collab-entity", "collab-plugins", + "dashmap", "getrandom 0.2.10", "js-sys", "lazy_static", - "lru", "nanoid", "parking_lot 0.12.1", + "rayon", "serde", "serde_json", "serde_repr", @@ -798,7 +813,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "collab", @@ -817,7 +832,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "bytes", @@ -832,7 +847,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "chrono", @@ -858,6 +873,7 @@ dependencies = [ "collab", "collab-entity", "collab-plugins", + "futures", "lib-infra", "parking_lot 0.12.1", "serde", @@ -869,7 +885,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "async-stream", @@ -905,10 +921,49 @@ dependencies = [ "yrs", ] +[[package]] +name = "collab-rt-entity" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "bytes", + "chrono", + "client-websocket", + "collab", + "collab-entity", + "collab-rt-protocol", + "database-entity", + "prost", + "prost-build", + "protoc-bin-vendored", + "serde", + "serde_json", + "serde_repr", + "thiserror", + "tokio-tungstenite", + "yrs", +] + +[[package]] +name = "collab-rt-protocol" +version = "0.1.0" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" +dependencies = [ + "anyhow", + "bincode", + "collab", + "serde", + "thiserror", + "tracing", + "yrs", +] + [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=9b189e7dc180ddc7fd79c49ed16f9c8a46216380#9b189e7dc180ddc7fd79c49ed16f9c8a46216380" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5f66f8c921646d7d8762cafc8bbec72d56c2e157#5f66f8c921646d7d8762cafc8bbec72d56c2e157" dependencies = [ "anyhow", "collab", @@ -1238,7 +1293,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -1729,6 +1784,7 @@ dependencies = [ "lib-infra", "lib-log", "parking_lot 0.12.1", + "semver", "serde", "serde_json", "serde_repr", @@ -1779,7 +1835,6 @@ dependencies = [ "lazy_static", "lib-dispatch", "lib-infra", - "lru", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -1840,6 +1895,7 @@ dependencies = [ "collab-entity", "collab-integrate", "collab-plugins", + "dashmap", "flowy-codegen", "flowy-derive", "flowy-document-pub", @@ -1851,7 +1907,6 @@ dependencies = [ "indexmap 2.1.0", "lib-dispatch", "lib-infra", - "lru", "nanoid", "parking_lot 0.12.1", "protobuf", @@ -2014,6 +2069,7 @@ dependencies = [ "mime_guess", "parking_lot 0.12.1", "postgrest", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -2118,6 +2174,7 @@ dependencies = [ "quickcheck_macros", "rand 0.8.5", "rand_core 0.6.4", + "semver", "serde", "serde_json", "serde_repr", @@ -2409,10 +2466,23 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "futures-util", @@ -2429,7 +2499,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -2500,10 +2570,6 @@ name = "hashbrown" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" -dependencies = [ - "ahash 0.8.6", - "allocator-api2", -] [[package]] name = "hdrhistogram" @@ -2823,7 +2889,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "reqwest", @@ -3005,8 +3071,7 @@ checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "librocksdb-sys" version = "0.11.0+8.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3386f101bcb4bd252d8e9d2fb41ec3b0862a15a62b478c355b2982efa469e3e" +source = "git+https://github.com/LucasXu0/rust-rocksdb?rev=21cf4a23ec131b9d82dc94e178fe8efc0c147b09#21cf4a23ec131b9d82dc94e178fe8efc0c147b09" dependencies = [ "bindgen", "bzip2-sys", @@ -3061,15 +3126,6 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" -[[package]] -name = "lru" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efa59af2ddfad1854ae27d75009d538d0998b4b2fd47083e743ac1a10e46c60" -dependencies = [ - "hashbrown 0.14.3", -] - [[package]] name = "mac" version = "0.1.1" @@ -4276,9 +4332,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -4303,43 +4359,6 @@ dependencies = [ "rand_core 0.3.1", ] -[[package]] -name = "realtime-entity" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "anyhow", - "bincode", - "bytes", - "collab", - "collab-entity", - "database-entity", - "prost", - "prost-build", - "protoc-bin-vendored", - "realtime-protocol", - "serde", - "serde_json", - "serde_repr", - "thiserror", - "tokio-tungstenite", - "websocket", - "yrs", -] - -[[package]] -name = "realtime-protocol" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "anyhow", - "bincode", - "collab", - "serde", - "thiserror", - "yrs", -] - [[package]] name = "redox_syscall" version = "0.1.57" @@ -4546,8 +4565,7 @@ dependencies = [ [[package]] name = "rocksdb" version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb6f170a4041d50a0ce04b0d2e14916d6ca863ea2e422689a5b694395d299ffe" +source = "git+https://github.com/LucasXu0/rust-rocksdb?rev=21cf4a23ec131b9d82dc94e178fe8efc0c147b09#21cf4a23ec131b9d82dc94e178fe8efc0c147b09" dependencies = [ "libc", "librocksdb-sys", @@ -4819,6 +4837,17 @@ dependencies = [ "syn 2.0.47", ] +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.47", +] + [[package]] name = "serde_json" version = "1.0.111" @@ -4924,7 +4953,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "app-error", @@ -5729,6 +5758,31 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils", + "serde", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.47", +] + [[package]] name = "tungstenite" version = "0.20.1" @@ -6111,24 +6165,6 @@ version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" -[[package]] -name = "websocket" -version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" -dependencies = [ - "futures-channel", - "futures-util", - "http", - "httparse", - "js-sys", - "percent-encoding", - "thiserror", - "tokio", - "tokio-tungstenite", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "which" version = "4.4.2" @@ -6355,7 +6391,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0bee7cd0dff240eeca972f3d15d6de3df9c0aceb#0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=aa4df32f6d3b53bec5e3715f5abfe4fb9079021b#aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" dependencies = [ "anyhow", "async-trait", @@ -6385,8 +6421,7 @@ dependencies = [ [[package]] name = "yrs" version = "0.17.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68aea14c6c33f2edd8a5ff9415360cfa5b98d90cce30c5ee3be59a8419fb15a9" +source = "git+https://github.com/appflowy/y-crdt?rev=3f25bb510ca5274e7657d3713fbed41fb46b4487#3f25bb510ca5274e7657d3713fbed41fb46b4487" dependencies = [ "atomic_refcell", "rand 0.7.3", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index d098d11338..78d55472a2 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -74,7 +74,7 @@ tokio = "1.34.0" tokio-stream = "0.1.14" async-trait = "0.1.74" chrono = { version = "0.4.31", default-features = false, features = ["clock"] } -lru = "0.12.0" +yrs = { version = "0.17.2" } [profile.dev] opt-level = 0 @@ -100,12 +100,18 @@ lto = false incremental = false [patch.crates-io] +yrs = { git = "https://github.com/appflowy/y-crdt", rev = "3f25bb510ca5274e7657d3713fbed41fb46b4487" } + +# TODO(Lucas.Xu) Upgrade to the latest version of RocksDB once PR(https://github.com/rust-rocksdb/rust-rocksdb/pull/869) is merged. +# Currently, using the following revision id. This commit is patched to fix the 32-bit build issue and it's checked out from 0.21.0, not 0.22.0. +rocksdb = { git = "https://github.com/LucasXu0/rust-rocksdb", rev = "21cf4a23ec131b9d82dc94e178fe8efc0c147b09" } + # Please using the following command to update the revision id # Current directory: frontend -# Run the script: +# Run the script.add_workspace_members: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0bee7cd0dff240eeca972f3d15d6de3df9c0aceb" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "aa4df32f6d3b53bec5e3715f5abfe4fb9079021b" } # Please use the following script to update collab. # Working directory: frontend # @@ -115,10 +121,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0be # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "9b189e7dc180ddc7fd79c49ed16f9c8a46216380" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5f66f8c921646d7d8762cafc8bbec72d56c2e157" } diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs index bb5ef9301d..768147c10a 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/lib.rs @@ -22,6 +22,7 @@ pub struct ProtoCache { pub enum Project { Tauri, + TauriApp, Web { relative_path: String }, Native, } @@ -30,6 +31,9 @@ impl Project { pub fn dst(&self) -> String { match self { Project::Tauri => "appflowy_tauri/src/services/backend".to_string(), + Project::TauriApp => { + "appflowy_web_app/src/application/services/tauri-services/backend".to_string() + }, Project::Web { .. } => "appflowy_web/src/services/backend".to_string(), Project::Native => panic!("Native project is not supported yet."), } @@ -37,7 +41,7 @@ impl Project { pub fn event_root(&self) -> String { match self { - Project::Tauri => "../../".to_string(), + Project::Tauri | Project::TauriApp => "../../".to_string(), Project::Web { relative_path } => relative_path.to_string(), Project::Native => panic!("Native project is not supported yet."), } @@ -45,7 +49,7 @@ impl Project { pub fn model_root(&self) -> String { match self { - Project::Tauri => "../../".to_string(), + Project::Tauri | Project::TauriApp => "../../".to_string(), Project::Web { relative_path } => relative_path.to_string(), Project::Native => panic!("Native project is not supported yet."), } @@ -53,7 +57,7 @@ impl Project { pub fn event_imports(&self) -> String { match self { - Project::Tauri => r#" + Project::TauriApp | Project::Tauri => r#" /// Auto generate. Do not edit import { Ok, Err, Result } from "ts-results"; import { invoke } from "@tauri-apps/api/tauri"; diff --git a/frontend/rust-lib/collab-integrate/Cargo.toml b/frontend/rust-lib/collab-integrate/Cargo.toml index 19f5e879ab..048eecabf5 100644 --- a/frontend/rust-lib/collab-integrate/Cargo.toml +++ b/frontend/rust-lib/collab-integrate/Cargo.toml @@ -19,6 +19,7 @@ parking_lot.workspace = true async-trait.workspace = true tokio = { workspace = true, features = ["sync"]} lib-infra = { workspace = true } +futures = "0.3" [features] default = [] \ No newline at end of file diff --git a/frontend/rust-lib/collab-integrate/src/collab_builder.rs b/frontend/rust-lib/collab-integrate/src/collab_builder.rs index 6efb41a01f..9d36fd3a10 100644 --- a/frontend/rust-lib/collab-integrate/src/collab_builder.rs +++ b/frontend/rust-lib/collab-integrate/src/collab_builder.rs @@ -3,7 +3,7 @@ use std::sync::{Arc, Weak}; use crate::CollabKVDB; use anyhow::Error; -use collab::core::collab::{CollabDocState, MutexCollab}; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab::preclude::CollabBuilder; use collab_entity::{CollabObject, CollabType}; use collab_plugins::connect_state::{CollabConnectReachability, CollabConnectState}; @@ -21,7 +21,7 @@ use collab_plugins::local_storage::CollabPersistenceConfig; use lib_infra::{if_native, if_wasm}; use parking_lot::{Mutex, RwLock}; -use tracing::trace; +use tracing::{instrument, trace}; #[derive(Clone, Debug)] pub enum CollabPluginProviderType { @@ -68,7 +68,7 @@ impl Display for CollabPluginProviderContext { pub struct AppFlowyCollabBuilder { network_reachability: CollabConnectReachability, workspace_id: RwLock>, - plugin_provider: tokio::sync::RwLock>, + plugin_provider: RwLock>, snapshot_persistence: Mutex>>, #[cfg(not(target_arch = "wasm32"))] rocksdb_backup: Mutex>>, @@ -97,7 +97,7 @@ impl AppFlowyCollabBuilder { Self { network_reachability: CollabConnectReachability::new(), workspace_id: Default::default(), - plugin_provider: tokio::sync::RwLock::new(Arc::new(storage_provider)), + plugin_provider: RwLock::new(Arc::new(storage_provider)), snapshot_persistence: Default::default(), #[cfg(not(target_arch = "wasm32"))] rocksdb_backup: Default::default(), @@ -167,22 +167,20 @@ impl AppFlowyCollabBuilder { uid: i64, object_id: &str, object_type: CollabType, - collab_doc_state: CollabDocState, + collab_doc_state: DocStateSource, collab_db: Weak, build_config: CollabBuilderConfig, ) -> Result, Error> { let persistence_config = CollabPersistenceConfig::default(); - self - .build_with_config( - uid, - object_id, - object_type, - collab_db, - collab_doc_state, - persistence_config, - build_config, - ) - .await + self.build_with_config( + uid, + object_id, + object_type, + collab_db, + collab_doc_state, + persistence_config, + build_config, + ) } /// Creates a new collaboration builder with the custom configuration. @@ -200,88 +198,92 @@ impl AppFlowyCollabBuilder { /// - `collab_db`: A weak reference to the [CollabKVDB]. /// #[allow(clippy::too_many_arguments)] - pub async fn build_with_config( + #[instrument( + level = "trace", + skip(self, collab_db, collab_doc_state, persistence_config, build_config) + )] + pub fn build_with_config( &self, uid: i64, object_id: &str, object_type: CollabType, collab_db: Weak, - collab_doc_state: CollabDocState, + collab_doc_state: DocStateSource, #[allow(unused_variables)] persistence_config: CollabPersistenceConfig, build_config: CollabBuilderConfig, ) -> Result, Error> { - let mut builder = CollabBuilder::new(uid, object_id) + let collab = CollabBuilder::new(uid, object_id) .with_doc_state(collab_doc_state) - .with_device_id(self.device_id.clone()); + .with_device_id(self.device_id.clone()) + .build()?; #[cfg(target_arch = "wasm32")] { - builder = builder.with_plugin(IndexeddbDiskPlugin::new( + collab.lock().add_plugin(Box::new(IndexeddbDiskPlugin::new( uid, object_id.to_string(), object_type.clone(), collab_db.clone(), - )); + ))); } #[cfg(not(target_arch = "wasm32"))] { - builder = builder.with_plugin(RocksdbDiskPlugin::new_with_config( - uid, - object_id.to_string(), - object_type.clone(), - collab_db.clone(), - persistence_config.clone(), - None, - )); + collab + .lock() + .add_plugin(Box::new(RocksdbDiskPlugin::new_with_config( + uid, + object_id.to_string(), + object_type.clone(), + collab_db.clone(), + persistence_config.clone(), + None, + ))); } - let collab = Arc::new(builder.build()?); + let arc_collab = Arc::new(collab); + { - let collab_object = self.collab_object(uid, object_id, object_type)?; + let collab_object = self.collab_object(uid, object_id, object_type.clone())?; if build_config.sync_enable { - let provider_type = self.plugin_provider.read().await.provider_type(); + let provider_type = self.plugin_provider.read().provider_type(); let span = tracing::span!(tracing::Level::TRACE, "collab_builder", object_id = %object_id); let _enter = span.enter(); match provider_type { CollabPluginProviderType::AppFlowyCloud => { - trace!("init appflowy cloud collab plugins"); - let local_collab = Arc::downgrade(&collab); - let plugins = self - .plugin_provider - .read() - .await - .get_plugins(CollabPluginProviderContext::AppFlowyCloud { - uid, - collab_object, - local_collab, - }) - .await; + let local_collab = Arc::downgrade(&arc_collab); + let plugins = + self + .plugin_provider + .read() + .get_plugins(CollabPluginProviderContext::AppFlowyCloud { + uid, + collab_object, + local_collab, + }); - trace!("add appflowy cloud collab plugins: {}", plugins.len()); for plugin in plugins { - collab.lock().add_plugin(plugin); + arc_collab.lock().add_plugin(plugin); } }, CollabPluginProviderType::Supabase => { #[cfg(not(target_arch = "wasm32"))] { trace!("init supabase collab plugins"); - let local_collab = Arc::downgrade(&collab); + let local_collab = Arc::downgrade(&arc_collab); let local_collab_db = collab_db.clone(); - let plugins = self - .plugin_provider - .read() - .await - .get_plugins(CollabPluginProviderContext::Supabase { - uid, - collab_object, - local_collab, - local_collab_db, - }) - .await; + let plugins = + self + .plugin_provider + .read() + .get_plugins(CollabPluginProviderContext::Supabase { + uid, + collab_object, + local_collab, + local_collab_db, + }); for plugin in plugins { - collab.lock().add_plugin(plugin); + arc_collab.lock().add_plugin(plugin); } } }, @@ -291,12 +293,12 @@ impl AppFlowyCollabBuilder { } #[cfg(target_arch = "wasm32")] - collab.lock().initialize().await; + futures::executor::block_on(arc_collab.lock().initialize()); #[cfg(not(target_arch = "wasm32"))] - collab.lock().initialize(); + arc_collab.lock().initialize(); - trace!("collab initialized: {}", object_id); - Ok(collab) + trace!("collab initialized: {}:{}", object_type, object_id); + Ok(arc_collab) } } diff --git a/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs index f5b6ce5225..94256bb439 100644 --- a/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs +++ b/frontend/rust-lib/collab-integrate/src/native/plugin_provider.rs @@ -1,12 +1,11 @@ use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderType}; use collab::preclude::CollabPlugin; -use lib_infra::future::Fut; use std::sync::Arc; pub trait CollabCloudPluginProvider: Send + Sync + 'static { fn provider_type(&self) -> CollabPluginProviderType; - fn get_plugins(&self, context: CollabPluginProviderContext) -> Fut>>; + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec>; fn is_sync_enabled(&self) -> bool; } @@ -19,7 +18,7 @@ where (**self).provider_type() } - fn get_plugins(&self, context: CollabPluginProviderContext) -> Fut>> { + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec> { (**self).get_plugins(context) } diff --git a/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs b/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs index 86c4a26a63..545e6c461c 100644 --- a/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs +++ b/frontend/rust-lib/collab-integrate/src/wasm/plugin_provider.rs @@ -2,12 +2,11 @@ use crate::collab_builder::{CollabPluginProviderContext, CollabPluginProviderTyp use collab::preclude::CollabPlugin; use lib_infra::future::Fut; use std::rc::Rc; -use std::sync::Arc; pub trait CollabCloudPluginProvider: 'static { fn provider_type(&self) -> CollabPluginProviderType; - fn get_plugins(&self, context: CollabPluginProviderContext) -> Fut>>; + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec>; fn is_sync_enabled(&self) -> bool; } @@ -20,7 +19,7 @@ where (**self).provider_type() } - fn get_plugins(&self, context: CollabPluginProviderContext) -> Fut>> { + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec> { (**self).get_plugins(context) } diff --git a/frontend/rust-lib/dart-ffi/Cargo.toml b/frontend/rust-lib/dart-ffi/Cargo.toml index 97142f8e00..83473f40ad 100644 --- a/frontend/rust-lib/dart-ffi/Cargo.toml +++ b/frontend/rust-lib/dart-ffi/Cargo.toml @@ -27,8 +27,12 @@ tracing.workspace = true # workspace lib-dispatch = { workspace = true } + +# Core #flowy-core = { workspace = true, features = ["profiling"] } +#flowy-core = { workspace = true, features = ["verbose_log"] } flowy-core = { workspace = true } + flowy-notification = { workspace = true, features = ["dart"] } flowy-document = { workspace = true, features = ["dart"] } flowy-config = { workspace = true, features = ["dart"] } @@ -47,6 +51,7 @@ dart = ["flowy-core/dart"] rev-sqlite = ["flowy-core/rev-sqlite"] http_sync = ["flowy-core/http_sync", "flowy-core/use_bunyan"] openssl_vendored = ["flowy-core/openssl_vendored"] +verbose_log = [] [build-dependencies] flowy-codegen = { workspace = true, features = ["dart"] } diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 0ae56ce015..0c52c8ecab 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -29,7 +29,6 @@ mod env_serde; mod model; mod notification; mod protobuf; -mod util; lazy_static! { static ref APPFLOWY_CORE: MutexAppFlowyCore = MutexAppFlowyCore::new(); @@ -65,15 +64,13 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { let _ = save_appflowy_cloud_config(&configuration.root, &configuration.appflowy_cloud_config); } - let log_crates = vec!["flowy-ffi".to_string()]; let config = AppFlowyCoreConfig::new( configuration.app_version, configuration.custom_app_path, configuration.origin_app_path, configuration.device_id, DEFAULT_NAME.to_string(), - ) - .log_filter("info", log_crates); + ); // Ensure that the database is closed before initialization. Also, verify that the init_sdk function can be called // multiple times (is reentrant). Currently, only the database resource is exclusive. @@ -95,6 +92,7 @@ pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { #[allow(clippy::let_underscore_future)] pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); + #[cfg(feature = "sync_verbose_log")] trace!( "[FFI]: {} Async Event: {:?} with {} port", &request.id, @@ -113,6 +111,7 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { dispatcher.as_ref(), request, move |resp: AFPluginEventResponse| { + #[cfg(feature = "sync_verbose_log")] trace!("[FFI]: Post data to dart through {} port", port); Box::pin(post_to_flutter(resp, port)) }, @@ -122,6 +121,7 @@ pub extern "C" fn async_event(port: i64, input: *const u8, len: usize) { #[no_mangle] pub extern "C" fn sync_event(input: *const u8, len: usize) -> *const u8 { let request: AFPluginRequest = FFIRequest::from_u8_pointer(input, len).into(); + #[cfg(feature = "sync_verbose_log")] trace!("[FFI]: {} Sync Event: {:?}", &request.id, &request.event,); let dispatcher = match APPFLOWY_CORE.dispatcher() { @@ -162,6 +162,7 @@ async fn post_to_flutter(response: AFPluginEventResponse, port: i64) { .await { Ok(_success) => { + #[cfg(feature = "sync_verbose_log")] trace!("[FFI]: Post data to dart success"); }, Err(e) => { diff --git a/frontend/rust-lib/dart-ffi/src/util.rs b/frontend/rust-lib/dart-ffi/src/util.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/frontend/rust-lib/dart-ffi/src/util.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/frontend/rust-lib/event-integration/src/database_event.rs b/frontend/rust-lib/event-integration/src/database_event.rs index 3f2a84a34c..424936b6a8 100644 --- a/frontend/rust-lib/event-integration/src/database_event.rs +++ b/frontend/rust-lib/event-integration/src/database_event.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::convert::TryFrom; use bytes::Bytes; @@ -35,6 +36,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -65,6 +67,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -90,6 +93,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -200,7 +204,7 @@ impl EventIntegrationTest { &self, view_id: &str, row_position: OrderObjectPositionPB, - data: Option, + data: Option>, ) -> RowMetaPB { EventBuilder::new(self.clone()) .event(DatabaseEvent::CreateRow) @@ -208,7 +212,7 @@ impl EventIntegrationTest { view_id: view_id.to_string(), row_position, group_id: None, - data, + data: data.unwrap_or_default(), }) .async_send() .await diff --git a/frontend/rust-lib/event-integration/src/document/document_event.rs b/frontend/rust-lib/event-integration/src/document/document_event.rs index 2070b6adf9..ec6cdacfdb 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -46,7 +46,7 @@ impl DocumentEventTest { .await .unwrap(); let guard = doc.lock(); - guard.get_collab().encode_collab_v1() + guard.encode_collab().unwrap() } pub async fn create_document(&self) -> ViewPB { @@ -64,6 +64,7 @@ impl DocumentEventTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(core.clone()) .event(FolderEvent::CreateView) diff --git a/frontend/rust-lib/event-integration/src/document_event.rs b/frontend/rust-lib/event-integration/src/document_event.rs index c4904042ef..95103159fd 100644 --- a/frontend/rust-lib/event-integration/src/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document_event.rs @@ -41,6 +41,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; let view = EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -103,7 +104,7 @@ impl EventIntegrationTest { } pub fn assert_document_data_equal(doc_state: &[u8], doc_id: &str, expected: DocumentData) { - let collab = MutexCollab::new(CollabOrigin::Server, doc_id, vec![]); + let collab = MutexCollab::new(CollabOrigin::Server, doc_id, vec![], false); collab.lock().with_origin_transact_mut(|txn| { let update = Update::decode_v1(doc_state).unwrap(); txn.apply_update(update); diff --git a/frontend/rust-lib/event-integration/src/event_builder.rs b/frontend/rust-lib/event-integration/src/event_builder.rs index e25e6629bf..0d083b1037 100644 --- a/frontend/rust-lib/event-integration/src/event_builder.rs +++ b/frontend/rust-lib/event-integration/src/event_builder.rs @@ -62,18 +62,12 @@ impl EventBuilder { let response = self.get_response(); match response.clone().parse::() { Ok(Ok(data)) => data, - Ok(Err(e)) => panic!( - "Parser {:?} failed: {:?}, response {:?}", - std::any::type_name::(), - e, - response - ), - Err(e) => panic!( - "Dispatch {:?} failed: {:?}, response {:?}", - std::any::type_name::(), - e, - response - ), + Ok(Err(e)) => { + panic!("Parser {:?} failed: {:?}", std::any::type_name::(), e) + }, + Err(e) => { + panic!("Parser {:?} failed: {:?}", std::any::type_name::(), e) + }, } } diff --git a/frontend/rust-lib/event-integration/src/folder_event.rs b/frontend/rust-lib/event-integration/src/folder_event.rs index 6d44c4e6a3..e39354a0a0 100644 --- a/frontend/rust-lib/event-integration/src/folder_event.rs +++ b/frontend/rust-lib/event-integration/src/folder_event.rs @@ -89,7 +89,7 @@ impl EventIntegrationTest { pub async fn get_all_workspace_views(&self) -> Vec { EventBuilder::new(self.clone()) - .event(FolderEvent::ReadWorkspaceViews) + .event(FolderEvent::ReadCurrentWorkspaceViews) .async_send() .await .parse::() @@ -147,6 +147,7 @@ impl EventIntegrationTest { meta: Default::default(), set_as_current: false, index: None, + section: None, }; EventBuilder::new(self.clone()) .event(FolderEvent::CreateView) @@ -166,6 +167,15 @@ impl EventIntegrationTest { .await .parse::() } + + pub async fn import_data(&self, data: ImportPB) -> ViewPB { + EventBuilder::new(self.clone()) + .event(FolderEvent::ImportData) + .payload(data) + .async_send() + .await + .parse::() + } } pub struct ViewTest { @@ -188,6 +198,7 @@ impl ViewTest { meta: Default::default(), set_as_current: true, index: None, + section: None, }; let view = EventBuilder::new(sdk.clone()) @@ -224,7 +235,7 @@ async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str) -> }; EventBuilder::new(sdk.clone()) - .event(CreateWorkspace) + .event(CreateFolderWorkspace) .payload(request) .async_send() .await diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index a91125ca54..f335e290e8 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -1,4 +1,4 @@ -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab::core::origin::CollabOrigin; use collab_document::blocks::DocumentData; use collab_document::document::Document; @@ -108,31 +108,34 @@ impl EventIntegrationTest { pub async fn get_collab_doc_state( &self, oid: &str, - collay_type: CollabType, - ) -> Result { + collab_type: CollabType, + ) -> Result, FlowyError> { let server = self.server_provider.get_server().unwrap(); let workspace_id = self.get_current_workspace().await.id; let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() - .get_folder_doc_state(&workspace_id, uid, collay_type, oid) + .get_folder_doc_state(&workspace_id, uid, collab_type, oid) .await?; Ok(doc_state) } } -pub fn document_data_from_document_doc_state( - doc_id: &str, - doc_state: CollabDocState, -) -> DocumentData { +pub fn document_data_from_document_doc_state(doc_id: &str, doc_state: Vec) -> DocumentData { document_from_document_doc_state(doc_id, doc_state) .get_document_data() .unwrap() } -pub fn document_from_document_doc_state(doc_id: &str, doc_state: CollabDocState) -> Document { - Document::from_doc_state(CollabOrigin::Empty, doc_state, doc_id, vec![]).unwrap() +pub fn document_from_document_doc_state(doc_id: &str, doc_state: Vec) -> Document { + Document::from_doc_state( + CollabOrigin::Empty, + DocStateSource::FromDocState(doc_state), + doc_id, + vec![], + ) + .unwrap() } async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index 4f8b858d6e..dca523253f 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -18,10 +18,10 @@ use flowy_server::af_cloud::define::{USER_DEVICE_ID, USER_EMAIL, USER_SIGN_IN_UR use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::AuthenticatorType; use flowy_user::entities::{ - AuthenticatorPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, OauthSignInPB, - RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, - UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, - UserWorkspacePB, + AuthenticatorPB, ChangeWorkspaceIconPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, + OauthSignInPB, RenameWorkspacePB, RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, + SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, UserProfilePB, + UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; @@ -247,6 +247,27 @@ impl EventIntegrationTest { } } + pub async fn change_workspace_icon( + &self, + workspace_id: &str, + new_icon: &str, + ) -> Result<(), FlowyError> { + let payload = ChangeWorkspaceIconPB { + workspace_id: workspace_id.to_owned(), + new_icon: new_icon.to_owned(), + }; + match EventBuilder::new(self.clone()) + .event(UserEvent::ChangeWorkspaceIcon) + .payload(payload) + .async_send() + .await + .error() + { + Some(err) => Err(err), + None => Ok(()), + } + } + pub async fn folder_read_current_workspace(&self) -> WorkspacePB { EventBuilder::new(self.clone()) .event(FolderEvent::ReadCurrentWorkspace) @@ -255,9 +276,9 @@ impl EventIntegrationTest { .parse() } - pub async fn folder_read_workspace_views(&self) -> RepeatedViewPB { + pub async fn folder_read_current_workspace_views(&self) -> RepeatedViewPB { EventBuilder::new(self.clone()) - .event(FolderEvent::ReadWorkspaceViews) + .event(FolderEvent::ReadCurrentWorkspaceViews) .async_send() .await .parse() @@ -292,6 +313,17 @@ impl EventIntegrationTest { .async_send() .await; } + + pub async fn leave_workspace(&self, workspace_id: &str) { + let payload = UserWorkspaceIdPB { + workspace_id: workspace_id.to_string(), + }; + EventBuilder::new(self.clone()) + .event(UserEvent::LeaveWorkspace) + .payload(payload) + .async_send() + .await; + } } #[derive(Clone)] diff --git a/frontend/rust-lib/event-integration/tests/asset/csv_10240r_15c.csv.zip b/frontend/rust-lib/event-integration/tests/asset/csv_10240r_15c.csv.zip new file mode 100644 index 0000000000..9d6b57efc3 Binary files /dev/null and b/frontend/rust-lib/event-integration/tests/asset/csv_10240r_15c.csv.zip differ diff --git a/frontend/rust-lib/event-integration/tests/asset/csv_492r_17c.csv.zip b/frontend/rust-lib/event-integration/tests/asset/csv_492r_17c.csv.zip new file mode 100644 index 0000000000..0d38422ac3 Binary files /dev/null and b/frontend/rust-lib/event-integration/tests/asset/csv_492r_17c.csv.zip differ diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs index e9ed09813a..556624e7ff 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/group_test.rs @@ -89,7 +89,6 @@ async fn rename_group_event_test() { .create_board(¤t_workspace.id, "my board view".to_owned(), vec![]) .await; - // Empty to group id let groups = test.get_groups(&board_view.id).await; let error = test .update_group( @@ -101,9 +100,6 @@ async fn rename_group_event_test() { ) .await; assert!(error.is_none()); - - let groups = test.get_groups(&board_view.id).await; - assert_eq!(groups[1].group_name, "new name".to_owned()); } #[tokio::test] @@ -144,9 +140,6 @@ async fn update_group_name_test() { let groups = test.get_groups(&board_view.id).await; assert_eq!(groups.len(), 4); - assert_eq!(groups[1].group_name, "To Do"); - assert_eq!(groups[2].group_name, "Doing"); - assert_eq!(groups[3].group_name, "Done"); test .update_group( @@ -160,8 +153,6 @@ async fn update_group_name_test() { let groups = test.get_groups(&board_view.id).await; assert_eq!(groups.len(), 4); - assert_eq!(groups[1].group_name, "To Do?"); - assert_eq!(groups[2].group_name, "Doing"); } #[tokio::test] @@ -174,14 +165,9 @@ async fn delete_group_test() { let groups = test.get_groups(&board_view.id).await; assert_eq!(groups.len(), 4); - assert_eq!(groups[1].group_name, "To Do"); - assert_eq!(groups[2].group_name, "Doing"); - assert_eq!(groups[3].group_name, "Done"); test.delete_group(&board_view.id, &groups[1].group_id).await; let groups = test.get_groups(&board_view.id).await; assert_eq!(groups.len(), 3); - assert_eq!(groups[1].group_name, "Doing"); - assert_eq!(groups[2].group_name, "Done"); } diff --git a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs index 6849e0a8a3..1c2edd339d 100644 --- a/frontend/rust-lib/event-integration/tests/database/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/local_test/test.rs @@ -812,6 +812,30 @@ async fn update_relation_cell_test() { .await; assert_eq!(cell.row_ids.len(), 3); + + // update the relation cell + let changeset = RelationCellChangesetPB { + view_id: grid_view.id.clone(), + cell_id: CellIdPB { + view_id: grid_view.id.clone(), + field_id: relation_field.id.clone(), + row_id: database.rows[0].id.clone(), + }, + removed_row_ids: vec![ + "row1rowid".to_string(), + "row3rowid".to_string(), + "row4rowid".to_string(), + ], + ..Default::default() + }; + test.update_relation_cell(changeset).await; + + // get the cell + let cell = test + .get_relation_cell(&grid_view.id, &relation_field.id, &database.rows[0].id) + .await; + + assert_eq!(cell.row_ids.len(), 1); } #[tokio::test] diff --git a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs index 73488492da..f3bf6f9e48 100644 --- a/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/database/supabase_test/helper.rs @@ -73,6 +73,7 @@ impl FlowySupabaseDatabaseTest { .get_database_object_doc_state(database_id, CollabType::Database, &workspace_id) .await .unwrap() + .unwrap() } } @@ -81,7 +82,7 @@ pub fn assert_database_collab_content( collab_update: &[u8], expected: JsonValue, ) { - let collab = MutexCollab::new(CollabOrigin::Server, database_id, vec![]); + let collab = MutexCollab::new(CollabOrigin::Server, database_id, vec![], false); collab.lock().with_origin_transact_mut(|txn| { let update = Update::decode_v1(collab_update).unwrap(); txn.apply_update(update); diff --git a/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs b/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs index 6877e511c2..537cdf80d8 100644 --- a/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/database/supabase_test/test.rs @@ -1,7 +1,7 @@ use std::time::Duration; use flowy_database2::entities::{ - DatabaseSnapshotStatePB, DatabaseSyncStatePB, FieldChangesetPB, FieldType, + DatabaseSnapshotStatePB, DatabaseSyncState, DatabaseSyncStatePB, FieldChangesetPB, FieldType, }; use flowy_database2::notification::DatabaseNotification::DidUpdateDatabaseSnapshotState; @@ -53,7 +53,9 @@ async fn supabase_edit_database_test() { // wait all updates are send to the remote let rx = test .notification_sender - .subscribe_with_condition::(&database.id, |pb| pb.is_finish); + .subscribe_with_condition::(&database.id, |pb| { + pb.value == DatabaseSyncState::SyncFinished + }); receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs index 8c94fceab0..c0165bd8ca 100644 --- a/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/af_cloud_test/edit_test.rs @@ -6,7 +6,7 @@ use event_integration::document_event::assert_document_data_equal; use event_integration::user_event::user_localhost_af_cloud; use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; -use flowy_document::entities::DocumentSyncStatePB; +use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; use crate::util::{receive_with_timeout, unzip_history_user_db}; @@ -30,7 +30,9 @@ async fn af_cloud_edit_document_test() { // wait all update are send to the remote let rx = test .notification_sender - .subscribe_with_condition::(&document_id, |pb| !pb.is_syncing); + .subscribe_with_condition::(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); let _ = receive_with_timeout(rx, Duration::from_secs(30)).await; let document_data = test.get_document_data(&document_id).await; @@ -61,7 +63,9 @@ async fn af_cloud_sync_anon_user_document_test() { // wait all update are send to the remote let rx = test .notification_sender - .subscribe_with_condition::(&document_id, |pb| !pb.is_syncing); + .subscribe_with_condition::(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); let _ = receive_with_timeout(rx, Duration::from_secs(30)).await; let doc_state = test.get_document_doc_state(&document_id).await; diff --git a/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs b/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs index f11b4acb7c..ba761d347d 100644 --- a/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs +++ b/frontend/rust-lib/event-integration/tests/document/supabase_test/edit_test.rs @@ -1,7 +1,7 @@ use std::time::Duration; use event_integration::document_event::assert_document_data_equal; -use flowy_document::entities::DocumentSyncStatePB; +use flowy_document::entities::{DocumentSyncState, DocumentSyncStatePB}; use crate::document::supabase_test::helper::FlowySupabaseDocumentTest; use crate::util::receive_with_timeout; @@ -23,7 +23,9 @@ async fn supabase_document_edit_sync_test() { // wait all update are send to the remote let rx = test .notification_sender - .subscribe_with_condition::(&document_id, |pb| !pb.is_syncing); + .subscribe_with_condition::(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); @@ -49,7 +51,9 @@ async fn supabase_document_edit_sync_test2() { // wait all update are send to the remote let rx = test .notification_sender - .subscribe_with_condition::(&document_id, |pb| !pb.is_syncing); + .subscribe_with_condition::(&document_id, |pb| { + pb.value != DocumentSyncState::Syncing + }); receive_with_timeout(rx, Duration::from_secs(30)) .await .unwrap(); diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs new file mode 100644 index 0000000000..7b812fc821 --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/import_test.rs @@ -0,0 +1,55 @@ +use crate::util::unzip_history_user_db; +use event_integration::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; +use flowy_folder::entities::{ImportPB, ImportTypePB, ViewLayoutPB}; + +#[tokio::test] +async fn import_492_row_csv_file_test() { + // csv_500r_15c.csv is a file with 492 rows and 17 columns + let file_name = "csv_492r_17c.csv".to_string(); + let (cleaner, csv_file_path) = unzip_history_user_db("./tests/asset", &file_name).unwrap(); + + let csv_string = std::fs::read_to_string(csv_file_path).unwrap(); + let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; + test.sign_up_as_guest().await; + + let workspace_id = test.get_current_workspace().await.id; + let import_data = gen_import_data(file_name, csv_string, workspace_id); + + let view = test.import_data(import_data).await; + let database = test.get_database(&view.id).await; + assert_eq!(database.rows.len(), 492); + drop(cleaner); +} + +#[tokio::test] +async fn import_10240_row_csv_file_test() { + // csv_22577r_15c.csv is a file with 10240 rows and 15 columns + let file_name = "csv_10240r_15c.csv".to_string(); + let (cleaner, csv_file_path) = unzip_history_user_db("./tests/asset", &file_name).unwrap(); + + let csv_string = std::fs::read_to_string(csv_file_path).unwrap(); + let test = EventIntegrationTest::new_with_name(DEFAULT_NAME).await; + test.sign_up_as_guest().await; + + let workspace_id = test.get_current_workspace().await.id; + let import_data = gen_import_data(file_name, csv_string, workspace_id); + + let view = test.import_data(import_data).await; + let database = test.get_database(&view.id).await; + assert_eq!(database.rows.len(), 10240); + + drop(cleaner); +} + +fn gen_import_data(file_name: String, csv_string: String, workspace_id: String) -> ImportPB { + let import_data = ImportPB { + parent_view_id: workspace_id.clone(), + name: file_name, + data: Some(csv_string.as_bytes().to_vec()), + file_path: None, + view_layout: ViewLayoutPB::Grid, + import_type: ImportTypePB::CSV, + }; + import_data +} diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/mod.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/mod.rs index e13c3c1e76..aa58a02baf 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/mod.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/mod.rs @@ -1,4 +1,5 @@ mod folder_test; +mod import_test; mod script; mod subscription_test; mod test; diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs index a43aafc144..b2a1ee98d3 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/script.rs @@ -210,7 +210,7 @@ pub async fn create_workspace(sdk: &EventIntegrationTest, name: &str, desc: &str }; EventBuilder::new(sdk.clone()) - .event(CreateWorkspace) + .event(CreateFolderWorkspace) .payload(request) .async_send() .await @@ -246,6 +246,7 @@ pub async fn create_view( meta: Default::default(), set_as_current: true, index: None, + section: None, }; EventBuilder::new(sdk.clone()) .event(CreateView) @@ -275,6 +276,8 @@ pub async fn move_view( view_id, new_parent_id: parent_id, prev_view_id, + from_section: None, + to_section: None, }; let error = EventBuilder::new(sdk.clone()) .event(MoveNestedView) diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs index 0c67bf7373..8e60baef3a 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/test.rs @@ -12,7 +12,7 @@ async fn create_workspace_event_test() { desc: "".to_owned(), }; let view_pb = EventBuilder::new(test) - .event(flowy_folder::event_map::FolderEvent::CreateWorkspace) + .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) .payload(request) .async_send() .await @@ -474,7 +474,7 @@ async fn create_parent_view_with_invalid_name() { }; assert_eq!( EventBuilder::new(sdk) - .event(flowy_folder::event_map::FolderEvent::CreateWorkspace) + .event(flowy_folder::event_map::FolderEvent::CreateFolderWorkspace) .payload(request) .async_send() .await @@ -549,6 +549,8 @@ async fn move_folder_nested_view( view_id, new_parent_id, prev_view_id, + from_section: None, + to_section: None, }; EventBuilder::new(sdk) .event(flowy_folder::event_map::FolderEvent::MoveNestedView) diff --git a/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs b/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs index c00f4750f7..17497f14dd 100644 --- a/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs +++ b/frontend/rust-lib/event-integration/tests/folder/supabase_test/helper.rs @@ -67,7 +67,7 @@ pub fn assert_folder_collab_content(workspace_id: &str, collab_update: &[u8], ex panic!("collab update is empty"); } - let collab = MutexCollab::new(CollabOrigin::Server, workspace_id, vec![]); + let collab = MutexCollab::new(CollabOrigin::Server, workspace_id, vec![], false); collab.lock().with_origin_transact_mut(|txn| { let update = Update::decode_v1(collab_update).unwrap(); txn.apply_update(update); diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs index b61c872658..fce101a273 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/import_af_data_folder_test.rs @@ -1,9 +1,8 @@ use crate::util::unzip_history_user_db; use assert_json_diff::assert_json_include; use collab_database::rows::database_row_document_id_from_row_id; -use collab_entity::CollabType; use event_integration::user_event::user_localhost_af_cloud; -use event_integration::{document_data_from_document_doc_state, EventIntegrationTest}; +use event_integration::EventIntegrationTest; use flowy_core::DEFAULT_NAME; use flowy_user::errors::ErrorCode; use serde_json::{json, Value}; @@ -284,12 +283,12 @@ async fn assert_040_local_2_import_content(test: &EventIntegrationTest, view_id: let data = test.get_document_data(&doc_1.id).await; assert_json_include!(actual: json!(data), expected: expected_doc_1_json()); - // Check doc 1 remote content - let doc_1_doc_state = test - .get_collab_doc_state(&doc_1.id, CollabType::Document) - .await - .unwrap(); - assert_json_include!(actual:document_data_from_document_doc_state(&doc_1.id, doc_1_doc_state), expected: expected_doc_1_json()); + // // Check doc 1 remote content + // let doc_1_doc_state = test + // .get_collab_doc_state(&doc_1.id, CollabType::Document) + // .await + // .unwrap(); + // assert_json_include!(actual:document_data_from_document_doc_state(&doc_1.id, doc_1_doc_state), expected: expected_doc_1_json()); // Check doc 2 local content let doc_2 = local_2_getting_started_child_views[1].clone(); @@ -298,8 +297,8 @@ async fn assert_040_local_2_import_content(test: &EventIntegrationTest, view_id: assert_json_include!(actual: json!(data), expected: expected_doc_2_json()); // Check doc 2 remote content - let doc_2_doc_state = test.get_document_doc_state(&doc_2.id).await; - assert_json_include!(actual:document_data_from_document_doc_state(&doc_2.id, doc_2_doc_state), expected: expected_doc_2_json()); + // let doc_2_doc_state = test.get_document_doc_state(&doc_2.id).await; + // assert_json_include!(actual:document_data_from_document_doc_state(&doc_2.id, doc_2_doc_state), expected: expected_doc_2_json()); let grid_1 = local_2_getting_started_child_views[2].clone(); assert_eq!(grid_1.name, "Grid1"); diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs index 2d53be52fb..f553362155 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/member_test.rs @@ -1,9 +1,8 @@ +use crate::user::af_cloud_test::util::get_synced_workspaces; use event_integration::user_event::user_localhost_af_cloud; use event_integration::EventIntegrationTest; use flowy_user_pub::entities::Role; -use crate::user::af_cloud_test::workspace_test::get_synced_workspaces; - #[tokio::test] async fn af_cloud_invite_workspace_member() { user_localhost_af_cloud().await; @@ -76,3 +75,28 @@ async fn af_cloud_delete_workspace_member_test() { assert_eq!(members.len(), 1); assert_eq!(members[0].email, user_1.email); } + +#[tokio::test] +async fn af_cloud_leave_workspace_test() { + user_localhost_af_cloud().await; + let test_1 = EventIntegrationTest::new().await; + let user_1 = test_1.af_cloud_sign_up().await; + + let test_2 = EventIntegrationTest::new().await; + let user_2 = test_2.af_cloud_sign_up().await; + + test_1 + .add_workspace_member(&user_1.workspace_id, &user_2.email) + .await; + + // test_2 should have 2 workspace + let workspaces = get_synced_workspaces(&test_2, user_2.id).await; + assert_eq!(workspaces.len(), 2); + + // user_2 leaves the workspace + test_2.leave_workspace(&user_1.workspace_id).await; + + // user_2 should have 1 workspace + let workspaces = get_synced_workspaces(&test_2, user_2.id).await; + assert_eq!(workspaces.len(), 1); +} diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/mod.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/mod.rs index d6b6b4b382..3fbfac5a1f 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/mod.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/mod.rs @@ -2,4 +2,5 @@ mod anon_user_test; mod auth_test; mod import_af_data_folder_test; mod member_test; +mod util; mod workspace_test; diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/util.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/util.rs new file mode 100644 index 0000000000..3302f442de --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/util.rs @@ -0,0 +1,27 @@ +use std::time::Duration; + +use event_integration::EventIntegrationTest; +use flowy_user::{ + entities::{RepeatedUserWorkspacePB, UserWorkspacePB}, + protobuf::UserNotification, +}; + +use crate::util::receive_with_timeout; + +pub async fn get_synced_workspaces( + test: &EventIntegrationTest, + user_id: i64, +) -> Vec { + let _workspaces = test.get_all_workspaces().await.items; + let sub_id = user_id.to_string(); + let rx = test + .notification_sender + .subscribe::( + &sub_id, + UserNotification::DidUpdateUserWorkspaces as i32, + ); + receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap() + .items +} diff --git a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs index 12dbbd1c1c..68adb22065 100644 --- a/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs @@ -1,11 +1,7 @@ -use std::time::Duration; - use event_integration::user_event::user_localhost_af_cloud; use event_integration::EventIntegrationTest; -use flowy_user::entities::{RepeatedUserWorkspacePB, UserWorkspacePB}; -use flowy_user::protobuf::UserNotification; -use crate::util::receive_with_timeout; +use crate::user::af_cloud_test::util::get_synced_workspaces; #[tokio::test] async fn af_cloud_workspace_delete() { @@ -29,18 +25,28 @@ async fn af_cloud_workspace_delete() { } #[tokio::test] -async fn af_cloud_workspace_name_change() { +async fn af_cloud_workspace_change_name_and_icon() { user_localhost_af_cloud().await; let test = EventIntegrationTest::new().await; let user_profile_pb = test.af_cloud_sign_up().await; let workspaces = test.get_all_workspaces().await; let workspace_id = workspaces.items[0].workspace_id.as_str(); + let new_workspace_name = "new_workspace_name".to_string(); + let new_icon = "🚀".to_string(); test - .rename_workspace(workspace_id, "new_workspace_name") + .rename_workspace(workspace_id, &new_workspace_name) .await .expect("failed to rename workspace"); + test + .change_workspace_icon(workspace_id, &new_icon) + .await + .expect("failed to change workspace icon"); let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; - assert_eq!(workspaces[0].name, "new_workspace_name".to_string()); + assert_eq!(workspaces[0].name, new_workspace_name); + assert_eq!(workspaces[0].icon, new_icon); + let local_workspaces = test.get_all_workspaces().await; + assert_eq!(local_workspaces.items[0].name, new_workspace_name); + assert_eq!(local_workspaces.items[0].icon, new_icon); } #[tokio::test] @@ -58,13 +64,16 @@ async fn af_cloud_create_workspace_test() { let workspaces = get_synced_workspaces(&test, user_profile_pb.id).await; assert_eq!(workspaces.len(), 2); - assert_eq!(workspaces[1].name, "my second workspace".to_string()); + let _second_workspace = workspaces + .iter() + .find(|w| w.name == "my second workspace") + .expect("created workspace not found"); { // before opening new workspace let folder_ws = test.folder_read_current_workspace().await; assert_eq!(&folder_ws.id, first_workspace_id); - let views = test.folder_read_workspace_views().await; + let views = test.folder_read_current_workspace_views().await; assert_eq!(views.items[0].parent_view_id.as_str(), first_workspace_id); } { @@ -72,7 +81,7 @@ async fn af_cloud_create_workspace_test() { test.open_workspace(&created_workspace.workspace_id).await; let folder_ws = test.folder_read_current_workspace().await; assert_eq!(folder_ws.id, created_workspace.workspace_id); - let views = test.folder_read_workspace_views().await; + let views = test.folder_read_current_workspace_views().await; assert_eq!( views.items[0].parent_view_id.as_str(), created_workspace.workspace_id @@ -98,21 +107,3 @@ async fn af_cloud_open_workspace_test() { assert_eq!(views[1].name, "my first document".to_string()); assert_eq!(views[2].name, "my second document".to_string()); } - -pub async fn get_synced_workspaces( - test: &EventIntegrationTest, - user_id: i64, -) -> Vec { - let _workspaces = test.get_all_workspaces().await.items; - let sub_id = user_id.to_string(); - let rx = test - .notification_sender - .subscribe::( - &sub_id, - UserNotification::DidUpdateUserWorkspaces as i32, - ); - receive_with_timeout(rx, Duration::from_secs(30)) - .await - .unwrap() - .items -} diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs index 8e1223f566..24418e7edd 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/user_awareness_test.rs @@ -22,6 +22,7 @@ async fn user_update_with_reminder() { object_id: "".to_string(), meta, }; + let _ = EventBuilder::new(sdk.clone()) .event(CreateReminder) .payload(payload) diff --git a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs index f42671cb1c..8ca0b4e696 100644 --- a/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/supabase_test/auth_test.rs @@ -123,7 +123,7 @@ async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { let test = EventIntegrationTest::new_with_guest_user().await; let old_views = test .folder_manager - .get_current_workspace_views() + .get_current_workspace_public_views() .await .unwrap(); let old_workspace = test.folder_manager.get_current_workspace().await.unwrap(); @@ -132,7 +132,7 @@ async fn sign_up_as_guest_and_then_update_to_new_cloud_user_test() { test.supabase_sign_up_with_uuid(&uuid, None).await.unwrap(); let new_views = test .folder_manager - .get_current_workspace_views() + .get_current_workspace_public_views() .await .unwrap(); let new_workspace = test.folder_manager.get_current_workspace().await.unwrap(); @@ -163,7 +163,7 @@ async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { let old_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); let old_cloud_views = test .folder_manager - .get_current_workspace_views() + .get_current_workspace_public_views() .await .unwrap(); assert_eq!(old_cloud_views.len(), 1); @@ -189,7 +189,7 @@ async fn sign_up_as_guest_and_then_update_to_existing_cloud_user_test() { let new_cloud_workspace = test.folder_manager.get_current_workspace().await.unwrap(); let new_cloud_views = test .folder_manager - .get_current_workspace_views() + .get_current_workspace_public_views() .await .unwrap(); assert_eq!(new_cloud_workspace, old_cloud_workspace); diff --git a/frontend/rust-lib/flowy-config/build.rs b/frontend/rust-lib/flowy-config/build.rs index 84b506ee31..e015eb2580 100644 --- a/frontend/rust-lib/flowy-config/build.rs +++ b/frontend/rust-lib/flowy-config/build.rs @@ -13,5 +13,11 @@ fn main() { env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri, ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); } } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index 1798e1fefb..d086707466 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -48,6 +48,7 @@ serde_repr.workspace = true futures.workspace = true walkdir = "2.4.0" sysinfo = "0.30.5" +semver = "1.0.22" [features] default = ["rev-sqlite"] @@ -66,4 +67,5 @@ ts = [ "flowy-config/tauri_ts", ] rev-sqlite = ["flowy-user/rev-sqlite"] -openssl_vendored = ["flowy-sqlite/openssl_vendored"] \ No newline at end of file +openssl_vendored = ["flowy-sqlite/openssl_vendored"] +verbose_log = ["flowy-document/verbose_log", "client-api/sync_verbose_log"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs index 5cc7bdbf03..1876392eeb 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/document_deps.rs @@ -89,6 +89,14 @@ impl DocumentUserService for DocumentUserImpl { .user_id() } + fn device_id(&self) -> Result { + self + .0 + .upgrade() + .ok_or(FlowyError::internal().with_context("Unexpected error: UserSession is None"))? + .device_id() + } + fn workspace_id(&self) -> Result { self .0 diff --git a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs index 4becb36241..4e7d681e44 100644 --- a/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs +++ b/frontend/rust-lib/flowy-core/src/deps_resolve/folder_deps.rs @@ -126,6 +126,15 @@ impl FolderOperationHandler for DocumentFolderOperation { }) } + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + manager.open_document(&view_id).await?; + Ok(()) + }) + } + /// Close the document view. fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { let manager = self.0.clone(); @@ -236,6 +245,15 @@ impl FolderOperationHandler for DocumentFolderOperation { struct DatabaseFolderOperation(Arc); impl FolderOperationHandler for DatabaseFolderOperation { + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { + let database_manager = self.0.clone(); + let view_id = view_id.to_string(); + FutureResult::new(async move { + database_manager.open_database_view(view_id).await?; + Ok(()) + }) + } + fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError> { let database_manager = self.0.clone(); let view_id = view_id.to_string(); @@ -360,8 +378,11 @@ impl FolderOperationHandler for DatabaseFolderOperation { _ => CSVFormat::Original, }; FutureResult::new(async move { - let content = - String::from_utf8(bytes).map_err(|err| FlowyError::internal().with_context(err))?; + let content = tokio::task::spawn_blocking(move || { + String::from_utf8(bytes).map_err(|err| FlowyError::internal().with_context(err)) + }) + .await??; + database_manager .import_csv(view_id, content, format) .await?; diff --git a/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs b/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs index 08f6a6a2c9..171fc20010 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/collab_interact.rs @@ -27,7 +27,7 @@ impl CollabInteract for CollabInteractImpl { .handle_reminder_action(DocumentReminderAction::Add { reminder }) .await; }, - Err(e) => tracing::error!("Failed to convert reminder: {:?}", e), + Err(e) => tracing::error!("Failed to add reminder: {:?}", e), } } Ok(()) @@ -56,7 +56,7 @@ impl CollabInteract for CollabInteractImpl { .handle_reminder_action(DocumentReminderAction::Update { reminder }) .await; }, - Err(e) => tracing::error!("Failed to convert reminder: {:?}", e), + Err(e) => tracing::error!("Failed to update reminder: {:?}", e), } } Ok(()) diff --git a/frontend/rust-lib/flowy-core/src/integrate/log.rs b/frontend/rust-lib/flowy-core/src/integrate/log.rs index 7a66353275..44ea8da213 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/log.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/log.rs @@ -4,6 +4,11 @@ use crate::AppFlowyCoreConfig; static INIT_LOG: AtomicBool = AtomicBool::new(false); pub(crate) fn init_log(config: &AppFlowyCoreConfig) { + #[cfg(debug_assertions)] + if get_bool_from_env_var("DISABLE_CI_TEST_LOG") { + return; + } + if !INIT_LOG.load(Ordering::SeqCst) { INIT_LOG.store(true, Ordering::SeqCst); @@ -12,6 +17,7 @@ pub(crate) fn init_log(config: &AppFlowyCoreConfig) { .build(); } } + pub(crate) fn create_log_filter(level: String, with_crates: Vec) -> String { let level = std::env::var("RUST_LOG").unwrap_or(level); let mut filters = with_crates @@ -32,10 +38,14 @@ pub(crate) fn create_log_filter(level: String, with_crates: Vec) -> Stri filters.push(format!("flowy_server={}", level)); filters.push(format!("flowy_notification={}", "info")); filters.push(format!("lib_infra={}", level)); - // filters.push(format!("lib_dispatch={}", level)); + filters.push(format!("dart_ffi={}", level)); + + // ⚠️Enable debug log for dart_ffi, flowy_sqlite and lib_dispatch as needed. Don't enable them by default. + { + // filters.push(format!("flowy_sqlite={}", "info")); + // filters.push(format!("lib_dispatch={}", level)); + } - filters.push(format!("dart_ffi={}", "info")); - filters.push(format!("flowy_sqlite={}", "info")); filters.push(format!("client_api={}", level)); #[cfg(feature = "profiling")] filters.push(format!("tokio={}", level)); @@ -45,3 +55,15 @@ pub(crate) fn create_log_filter(level: String, with_crates: Vec) -> Stri filters.join(",") } + +#[cfg(debug_assertions)] +fn get_bool_from_env_var(env_var_name: &str) -> bool { + match std::env::var(env_var_name) { + Ok(value) => match value.to_lowercase().as_str() { + "true" | "1" => true, + "false" | "0" => false, + _ => false, + }, + Err(_) => false, + } +} diff --git a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs index 9e7a4c0dfa..c521c2fe9f 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use anyhow::Error; use client_api::collab_sync::{SinkConfig, SyncObject, SyncPlugin}; -use collab::core::collab::CollabDocState; + use collab::core::origin::{CollabClient, CollabOrigin}; use collab::preclude::CollabPlugin; use collab_entity::CollabType; @@ -26,7 +26,7 @@ use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_storage::ObjectValue; use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserTokenState}; -use lib_infra::future::{to_fut, Fut, FutureResult}; +use lib_infra::future::FutureResult; use crate::integrate::server::{Server, ServerProvider}; @@ -184,7 +184,7 @@ impl FolderCloudService for ServerProvider { uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult { + ) -> FutureResult, Error> { let object_id = object_id.to_string(); let workspace_id = workspace_id.to_string(); let server = self.get_server(); @@ -225,7 +225,7 @@ impl DatabaseCloudService for ServerProvider { object_id: &str, collab_type: CollabType, workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult>, Error> { let workspace_id = workspace_id.to_string(); let server = self.get_server(); let database_id = object_id.to_string(); @@ -274,7 +274,7 @@ impl DocumentCloudService for ServerProvider { &self, document_id: &str, workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult, FlowyError> { let workspace_id = workspace_id.to_string(); let document_id = document_id.to_string(); let server = self.get_server(); @@ -326,61 +326,58 @@ impl CollabCloudPluginProvider for ServerProvider { } #[instrument(level = "debug", skip(self, context), fields(server_type = %self.get_server_type()))] - fn get_plugins(&self, context: CollabPluginProviderContext) -> Fut>> { + fn get_plugins(&self, context: CollabPluginProviderContext) -> Vec> { // If the user is local, we don't need to create a sync plugin. if self.get_server_type().is_local() { debug!( "User authenticator is local, skip create sync plugin for: {}", context ); - return to_fut(async move { vec![] }); + return vec![]; } match context { - CollabPluginProviderContext::Local => to_fut(async move { vec![] }), + CollabPluginProviderContext::Local => vec![], CollabPluginProviderContext::AppFlowyCloud { uid: _, collab_object, local_collab, } => { if let Ok(server) = self.get_server() { - to_fut(async move { - let mut plugins: Vec> = vec![]; + // to_fut(async move { + let mut plugins: Vec> = vec![]; + // If the user is local, we don't need to create a sync plugin. - // If the user is local, we don't need to create a sync plugin. - - match server.collab_ws_channel(&collab_object.object_id).await { - Ok(Some((channel, ws_connect_state, is_connected))) => { - let origin = CollabOrigin::Client(CollabClient::new( - collab_object.uid, - collab_object.device_id.clone(), - )); - let sync_object = SyncObject::from(collab_object); - let (sink, stream) = (channel.sink(), channel.stream()); - let sink_config = SinkConfig::new().send_timeout(8); - let sync_plugin = SyncPlugin::new( - origin, - sync_object, - local_collab, - sink, - sink_config, - stream, - Some(channel), - !is_connected, - ws_connect_state, - ); - plugins.push(Box::new(sync_plugin)); - }, - Ok(None) => { - tracing::error!("🔴Failed to get collab ws channel: channel is none"); - }, - Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err), - } - - plugins - }) + match server.collab_ws_channel(&collab_object.object_id) { + Ok(Some((channel, ws_connect_state, is_connected))) => { + let origin = CollabOrigin::Client(CollabClient::new( + collab_object.uid, + collab_object.device_id.clone(), + )); + let sync_object = SyncObject::from(collab_object); + let (sink, stream) = (channel.sink(), channel.stream()); + let sink_config = SinkConfig::new().send_timeout(8); + let sync_plugin = SyncPlugin::new( + origin, + sync_object, + local_collab, + sink, + sink_config, + stream, + Some(channel), + !is_connected, + ws_connect_state, + ); + plugins.push(Box::new(sync_plugin)); + }, + Ok(None) => { + tracing::error!("🔴Failed to get collab ws channel: channel is none"); + }, + Err(err) => tracing::error!("🔴Failed to get collab ws channel: {:?}", err), + } + plugins } else { - to_fut(async move { vec![] }) + vec![] } }, CollabPluginProviderContext::Supabase { @@ -404,8 +401,7 @@ impl CollabCloudPluginProvider for ServerProvider { local_collab_db, ))); } - - to_fut(async move { plugins }) + plugins }, } } diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index c1e2fbcb82..636f78b6f9 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -1,6 +1,7 @@ #![allow(unused_doc_comments)] use flowy_storage::ObjectStorageService; +use semver::Version; use std::sync::Arc; use std::time::Duration; use sysinfo::System; @@ -65,7 +66,7 @@ impl AppFlowyCore { #[allow(clippy::if_same_then_else)] if cfg!(debug_assertions) { /// The profiling can be used to tracing the performance of the application. - /// Check out the [Link](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/profiling) + /// Check out the [Link](https://docs.appflowy.io/docs/documentation/software-contributions/architecture/backend/profiling#enable-profiling) /// for more information. #[cfg(feature = "profiling")] console_subscriber::init(); @@ -93,6 +94,7 @@ impl AppFlowyCore { server_type, Arc::downgrade(&store_preference), )); + let app_version = Version::parse(&config.app_version).unwrap_or_else(|_| Version::new(0, 5, 4)); event!(tracing::Level::DEBUG, "Init managers",); let ( @@ -115,6 +117,7 @@ impl AppFlowyCore { &config.storage_path, &config.application_path, &config.device_id, + app_version, ); let authenticate_user = Arc::new(AuthenticateUser::new( diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index 5e1bb5e1c9..30b8d034b1 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -1,23 +1,24 @@ +use anyhow::Error; +use collab::core::collab::DocStateSource; +use collab_entity::CollabType; +use lib_infra::future::FutureResult; use std::collections::HashMap; -use anyhow::Error; -use collab::core::collab::CollabDocState; -use collab_entity::CollabType; - -use lib_infra::future::FutureResult; - -pub type CollabDocStateByOid = HashMap; +pub type CollabDocStateByOid = HashMap; /// A trait for database cloud service. /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. +/// +/// returns the doc state of the object with the given object_id. +/// None if the object is not found. pub trait DatabaseCloudService: Send + Sync { fn get_database_object_doc_state( &self, object_id: &str, collab_type: CollabType, workspace_id: &str, - ) -> FutureResult; + ) -> FutureResult>, Error>; fn batch_get_database_object_doc_state( &self, diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index 2d6cc2ec97..79563a7929 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -40,14 +40,13 @@ futures.workspace = true dashmap = "5" anyhow.workspace = true async-stream = "0.3.4" -rayon = "1.6.1" +rayon = "1.9.0" nanoid = "0.4.0" async-trait.workspace = true chrono-tz = "0.8.2" csv = "1.1.6" strum = "0.25" strum_macros = "0.25" -lru.workspace = true validator = { version = "0.16.0", features = ["derive"] } [dev-dependencies] diff --git a/frontend/rust-lib/flowy-database2/build.rs b/frontend/rust-lib/flowy-database2/build.rs index 90f29201b0..aeaaee42f3 100644 --- a/frontend/rust-lib/flowy-database2/build.rs +++ b/frontend/rust-lib/flowy-database2/build.rs @@ -13,5 +13,11 @@ fn main() { env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri, ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs index 9002ca295e..688e878caa 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -1,9 +1,8 @@ use collab::core::collab_state::SyncState; use collab_database::rows::RowId; -use collab_database::user::DatabaseViewTracker; use collab_database::views::DatabaseLayout; -use flowy_derive::ProtoBuf; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::{ErrorCode, FlowyError}; use lib_infra::validator_fn::required_not_empty_str; @@ -203,23 +202,18 @@ impl TryInto for MoveGroupRowPayloadPB { } #[derive(Debug, Default, ProtoBuf)] -pub struct DatabaseDescriptionPB { +pub struct DatabaseMetaPB { #[pb(index = 1)] pub database_id: String, -} -impl From for DatabaseDescriptionPB { - fn from(data: DatabaseViewTracker) -> Self { - Self { - database_id: data.database_id, - } - } + #[pb(index = 2)] + pub inline_view_id: String, } #[derive(Debug, Default, ProtoBuf)] pub struct RepeatedDatabaseDescriptionPB { #[pb(index = 1)] - pub items: Vec, + pub items: Vec, } #[derive(Debug, Clone, Default, ProtoBuf)] @@ -279,18 +273,27 @@ impl TryInto for DatabaseLayoutMetaPB { #[derive(Debug, Default, ProtoBuf)] pub struct DatabaseSyncStatePB { #[pb(index = 1)] - pub is_syncing: bool, + pub value: DatabaseSyncState, +} - #[pb(index = 2)] - pub is_finish: bool, +#[derive(Debug, Default, ProtoBuf_Enum, PartialEq, Eq, Clone, Copy)] +pub enum DatabaseSyncState { + #[default] + InitSyncBegin = 0, + InitSyncEnd = 1, + Syncing = 2, + SyncFinished = 3, } impl From for DatabaseSyncStatePB { fn from(value: SyncState) -> Self { - Self { - is_syncing: value.is_syncing(), - is_finish: value.is_sync_finished(), - } + let value = match value { + SyncState::InitSyncBegin => DatabaseSyncState::InitSyncBegin, + SyncState::InitSyncEnd => DatabaseSyncState::InitSyncEnd, + SyncState::Syncing => DatabaseSyncState::Syncing, + SyncState::SyncFinished => DatabaseSyncState::SyncFinished, + }; + Self { value } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs index 2db4ffb1b8..22b8c29858 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/field_entities.rs @@ -10,6 +10,7 @@ use strum_macros::{EnumCount as EnumCountMacro, EnumIter}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::position_entities::OrderObjectPositionPB; @@ -620,6 +621,30 @@ impl TryInto for DuplicateFieldPayloadPB { } } +#[derive(Debug, Clone, Default, ProtoBuf, Validate)] +pub struct ClearFieldPayloadPB { + #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub field_id: String, + + #[pb(index = 2)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub view_id: String, +} + +impl TryInto for ClearFieldPayloadPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::DatabaseIdIsEmpty)?; + let field_id = NotEmptyStr::parse(self.field_id).map_err(|_| ErrorCode::FieldIdIsEmpty)?; + Ok(FieldIdParams { + view_id: view_id.0, + field_id: field_id.0, + }) + } +} + #[derive(Debug, Clone, Default, ProtoBuf)] pub struct DeleteFieldPayloadPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs index 4b2f9fb888..6dde92ac3d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checkbox_filter.rs @@ -1,7 +1,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::filter::ParseFilterData; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct CheckboxFilterPB { @@ -9,9 +9,8 @@ pub struct CheckboxFilterPB { pub condition: CheckboxFilterConditionPB, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] -#[derive(Default)] pub enum CheckboxFilterConditionPB { #[default] IsChecked = 0, @@ -24,7 +23,7 @@ impl std::convert::From for u32 { } } -impl std::convert::TryFrom for CheckboxFilterConditionPB { +impl TryFrom for CheckboxFilterConditionPB { type Error = ErrorCode; fn try_from(value: u8) -> Result { @@ -36,22 +35,10 @@ impl std::convert::TryFrom for CheckboxFilterConditionPB { } } -impl FromFilterString for CheckboxFilterPB { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized, - { +impl ParseFilterData for CheckboxFilterPB { + fn parse(condition: u8, _content: String) -> Self { CheckboxFilterPB { - condition: CheckboxFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(CheckboxFilterConditionPB::IsChecked), - } - } -} - -impl std::convert::From<&Filter> for CheckboxFilterPB { - fn from(filter: &Filter) -> Self { - CheckboxFilterPB { - condition: CheckboxFilterConditionPB::try_from(filter.condition as u8) + condition: CheckboxFilterConditionPB::try_from(condition) .unwrap_or(CheckboxFilterConditionPB::IsChecked), } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs index 0c2e7fc037..97597f2d9b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/checklist_filter.rs @@ -1,7 +1,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::filter::ParseFilterData; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct ChecklistFilterPB { @@ -9,12 +9,11 @@ pub struct ChecklistFilterPB { pub condition: ChecklistFilterConditionPB, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] -#[derive(Default)] pub enum ChecklistFilterConditionPB { - IsComplete = 0, #[default] + IsComplete = 0, IsIncomplete = 1, } @@ -36,22 +35,10 @@ impl std::convert::TryFrom for ChecklistFilterConditionPB { } } -impl FromFilterString for ChecklistFilterPB { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized, - { - ChecklistFilterPB { - condition: ChecklistFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(ChecklistFilterConditionPB::IsIncomplete), - } - } -} - -impl std::convert::From<&Filter> for ChecklistFilterPB { - fn from(filter: &Filter) -> Self { - ChecklistFilterPB { - condition: ChecklistFilterConditionPB::try_from(filter.condition as u8) +impl ParseFilterData for ChecklistFilterPB { + fn parse(condition: u8, _content: String) -> Self { + Self { + condition: ChecklistFilterConditionPB::try_from(condition) .unwrap_or(ChecklistFilterConditionPB::IsIncomplete), } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs index 75891cea6f..01c3c9687c 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/date_filter.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::filter::ParseFilterData; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct DateFilterPB { @@ -23,19 +23,19 @@ pub struct DateFilterPB { } #[derive(Deserialize, Serialize, Default, Clone, Debug)] -pub struct DateFilterContentPB { +pub struct DateFilterContent { pub start: Option, pub end: Option, pub timestamp: Option, } -impl ToString for DateFilterContentPB { +impl ToString for DateFilterContent { fn to_string(&self) -> String { serde_json::to_string(self).unwrap() } } -impl FromStr for DateFilterContentPB { +impl FromStr for DateFilterContent { type Err = serde_json::Error; fn from_str(s: &str) -> Result { @@ -79,37 +79,17 @@ impl std::convert::TryFrom for DateFilterConditionPB { } } } -impl FromFilterString for DateFilterPB { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized, - { - let condition = DateFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(DateFilterConditionPB::DateIs); - let mut date_filter = DateFilterPB { + +impl ParseFilterData for DateFilterPB { + fn parse(condition: u8, content: String) -> Self { + let condition = + DateFilterConditionPB::try_from(condition).unwrap_or(DateFilterConditionPB::DateIs); + let mut date_filter = Self { condition, ..Default::default() }; - if let Ok(content) = DateFilterContentPB::from_str(&filter.content) { - date_filter.start = content.start; - date_filter.end = content.end; - date_filter.timestamp = content.timestamp; - }; - - date_filter - } -} -impl std::convert::From<&Filter> for DateFilterPB { - fn from(filter: &Filter) -> Self { - let condition = DateFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(DateFilterConditionPB::DateIs); - let mut date_filter = DateFilterPB { - condition, - ..Default::default() - }; - - if let Ok(content) = DateFilterContentPB::from_str(&filter.content) { + if let Ok(content) = DateFilterContent::from_str(&content) { date_filter.start = content.start; date_filter.end = content.end; date_filter.timestamp = content.timestamp; diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs index 05a0fbd4ea..06f18be289 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/filter_changeset.rs @@ -1,54 +1,22 @@ -use crate::entities::FilterPB; use flowy_derive::ProtoBuf; +use crate::entities::RepeatedFilterPB; +use crate::services::filter::Filter; + #[derive(Debug, Default, ProtoBuf)] pub struct FilterChangesetNotificationPB { #[pb(index = 1)] pub view_id: String, #[pb(index = 2)] - pub insert_filters: Vec, - - #[pb(index = 3)] - pub delete_filters: Vec, - - #[pb(index = 4)] - pub update_filters: Vec, -} - -#[derive(Debug, Default, ProtoBuf)] -pub struct UpdatedFilter { - #[pb(index = 1)] - pub filter_id: String, - - #[pb(index = 2, one_of)] - pub filter: Option, + pub filters: RepeatedFilterPB, } impl FilterChangesetNotificationPB { - pub fn from_insert(view_id: &str, filters: Vec) -> Self { + pub fn from_filters(view_id: &str, filters: &Vec) -> Self { Self { view_id: view_id.to_string(), - insert_filters: filters, - delete_filters: Default::default(), - update_filters: Default::default(), - } - } - pub fn from_delete(view_id: &str, filters: Vec) -> Self { - Self { - view_id: view_id.to_string(), - insert_filters: Default::default(), - delete_filters: filters, - update_filters: Default::default(), - } - } - - pub fn from_update(view_id: &str, filters: Vec) -> Self { - Self { - view_id: view_id.to_string(), - insert_filters: Default::default(), - delete_filters: Default::default(), - update_filters: filters, + filters: filters.into(), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs index 17f43c8640..6ea5e9ac7e 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/number_filter.rs @@ -1,7 +1,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::filter::ParseFilterData; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct NumberFilterPB { @@ -49,24 +49,12 @@ impl std::convert::TryFrom for NumberFilterConditionPB { } } -impl FromFilterString for NumberFilterPB { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized, - { +impl ParseFilterData for NumberFilterPB { + fn parse(condition: u8, content: String) -> Self { NumberFilterPB { - condition: NumberFilterConditionPB::try_from(filter.condition as u8) + condition: NumberFilterConditionPB::try_from(condition) .unwrap_or(NumberFilterConditionPB::Equal), - content: filter.content.clone(), - } - } -} -impl std::convert::From<&Filter> for NumberFilterPB { - fn from(filter: &Filter) -> Self { - NumberFilterPB { - condition: NumberFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(NumberFilterConditionPB::Equal), - content: filter.content.clone(), + content, } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs index 7c6e6ce11b..1a186eb038 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/relation_filter.rs @@ -1,6 +1,7 @@ +use collab_database::{fields::Field, rows::Cell}; use flowy_derive::ProtoBuf; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct RelationFilterPB { @@ -8,17 +9,14 @@ pub struct RelationFilterPB { pub condition: i64, } -impl FromFilterString for RelationFilterPB { - fn from_filter(_filter: &Filter) -> Self - where - Self: Sized, - { +impl ParseFilterData for RelationFilterPB { + fn parse(_condition: u8, _content: String) -> Self { RelationFilterPB { condition: 0 } } } -impl From<&Filter> for RelationFilterPB { - fn from(_filter: &Filter) -> Self { - RelationFilterPB { condition: 0 } +impl PreFillCellsWithFilter for RelationFilterPB { + fn get_compliant_cell(&self, _field: &Field) -> (Option, bool) { + (None, false) } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs index a7e8cbb60a..1643116ccb 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/select_option_filter.rs @@ -3,69 +3,58 @@ use std::str::FromStr; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use crate::services::field::SelectOptionIds; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::{field::SelectOptionIds, filter::ParseFilterData}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct SelectOptionFilterPB { #[pb(index = 1)] - pub condition: SelectOptionConditionPB, + pub condition: SelectOptionFilterConditionPB, #[pb(index = 2)] pub option_ids: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[derive(Debug, Default, Clone, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] -#[derive(Default)] -pub enum SelectOptionConditionPB { +pub enum SelectOptionFilterConditionPB { #[default] OptionIs = 0, OptionIsNot = 1, - OptionIsEmpty = 2, - OptionIsNotEmpty = 3, + OptionContains = 2, + OptionDoesNotContain = 3, + OptionIsEmpty = 4, + OptionIsNotEmpty = 5, } -impl std::convert::From for u32 { - fn from(value: SelectOptionConditionPB) -> Self { +impl From for u32 { + fn from(value: SelectOptionFilterConditionPB) -> Self { value as u32 } } -impl std::convert::TryFrom for SelectOptionConditionPB { +impl TryFrom for SelectOptionFilterConditionPB { type Error = ErrorCode; fn try_from(value: u8) -> Result { match value { - 0 => Ok(SelectOptionConditionPB::OptionIs), - 1 => Ok(SelectOptionConditionPB::OptionIsNot), - 2 => Ok(SelectOptionConditionPB::OptionIsEmpty), - 3 => Ok(SelectOptionConditionPB::OptionIsNotEmpty), + 0 => Ok(SelectOptionFilterConditionPB::OptionIs), + 1 => Ok(SelectOptionFilterConditionPB::OptionIsNot), + 2 => Ok(SelectOptionFilterConditionPB::OptionContains), + 3 => Ok(SelectOptionFilterConditionPB::OptionDoesNotContain), + 4 => Ok(SelectOptionFilterConditionPB::OptionIsEmpty), + 5 => Ok(SelectOptionFilterConditionPB::OptionIsNotEmpty), _ => Err(ErrorCode::InvalidParams), } } } -impl FromFilterString for SelectOptionFilterPB { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized, - { - let ids = SelectOptionIds::from_str(&filter.content).unwrap_or_default(); - SelectOptionFilterPB { - condition: SelectOptionConditionPB::try_from(filter.condition as u8) - .unwrap_or(SelectOptionConditionPB::OptionIs), - option_ids: ids.into_inner(), - } - } -} - -impl std::convert::From<&Filter> for SelectOptionFilterPB { - fn from(filter: &Filter) -> Self { - let ids = SelectOptionIds::from_str(&filter.content).unwrap_or_default(); - SelectOptionFilterPB { - condition: SelectOptionConditionPB::try_from(filter.condition as u8) - .unwrap_or(SelectOptionConditionPB::OptionIs), - option_ids: ids.into_inner(), +impl ParseFilterData for SelectOptionFilterPB { + fn parse(condition: u8, content: String) -> Self { + Self { + condition: SelectOptionFilterConditionPB::try_from(condition) + .unwrap_or(SelectOptionFilterConditionPB::OptionIs), + option_ids: SelectOptionIds::from_str(&content) + .unwrap_or_default() + .into_inner(), } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs index 0956fdb894..d3a2b89883 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/text_filter.rs @@ -1,7 +1,7 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; -use crate::services::filter::{Filter, FromFilterString}; +use crate::services::filter::ParseFilterData; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct TextFilterPB { @@ -12,17 +12,16 @@ pub struct TextFilterPB { pub content: String, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum)] +#[derive(Debug, Clone, Default, PartialEq, Eq, ProtoBuf_Enum)] #[repr(u8)] -#[derive(Default)] pub enum TextFilterConditionPB { #[default] - Is = 0, - IsNot = 1, - Contains = 2, - DoesNotContain = 3, - StartsWith = 4, - EndsWith = 5, + TextIs = 0, + TextIsNot = 1, + TextContains = 2, + TextDoesNotContain = 3, + TextStartsWith = 4, + TextEndsWith = 5, TextIsEmpty = 6, TextIsNotEmpty = 7, } @@ -38,12 +37,12 @@ impl std::convert::TryFrom for TextFilterConditionPB { fn try_from(value: u8) -> Result { match value { - 0 => Ok(TextFilterConditionPB::Is), - 1 => Ok(TextFilterConditionPB::IsNot), - 2 => Ok(TextFilterConditionPB::Contains), - 3 => Ok(TextFilterConditionPB::DoesNotContain), - 4 => Ok(TextFilterConditionPB::StartsWith), - 5 => Ok(TextFilterConditionPB::EndsWith), + 0 => Ok(TextFilterConditionPB::TextIs), + 1 => Ok(TextFilterConditionPB::TextIsNot), + 2 => Ok(TextFilterConditionPB::TextContains), + 3 => Ok(TextFilterConditionPB::TextDoesNotContain), + 4 => Ok(TextFilterConditionPB::TextStartsWith), + 5 => Ok(TextFilterConditionPB::TextEndsWith), 6 => Ok(TextFilterConditionPB::TextIsEmpty), 7 => Ok(TextFilterConditionPB::TextIsNotEmpty), _ => Err(ErrorCode::InvalidParams), @@ -51,25 +50,12 @@ impl std::convert::TryFrom for TextFilterConditionPB { } } -impl FromFilterString for TextFilterPB { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized, - { - TextFilterPB { - condition: TextFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(TextFilterConditionPB::Is), - content: filter.content.clone(), - } - } -} - -impl std::convert::From<&Filter> for TextFilterPB { - fn from(filter: &Filter) -> Self { - TextFilterPB { - condition: TextFilterConditionPB::try_from(filter.condition as u8) - .unwrap_or(TextFilterConditionPB::Is), - content: filter.content.clone(), +impl ParseFilterData for TextFilterPB { + fn parse(condition: u8, content: String) -> Self { + Self { + condition: TextFilterConditionPB::try_from(condition) + .unwrap_or(TextFilterConditionPB::TextIs), + content, } } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs index cdeef0401c..6d48abf0e8 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/filter_entities/util.rs @@ -1,216 +1,274 @@ use std::convert::TryInto; -use std::sync::Arc; use bytes::Bytes; -use collab_database::fields::Field; -use flowy_derive::ProtoBuf; +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use lib_infra::box_any::BoxAny; +use protobuf::ProtobufError; use validator::Validate; -use crate::entities::parser::NotEmptyStr; use crate::entities::{ - CheckboxFilterPB, ChecklistFilterPB, DateFilterContentPB, DateFilterPB, FieldType, - NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB, + CheckboxFilterPB, ChecklistFilterPB, DateFilterPB, FieldType, NumberFilterPB, RelationFilterPB, + SelectOptionFilterPB, TextFilterPB, }; -use crate::services::field::SelectOptionIds; -use crate::services::filter::Filter; +use crate::services::filter::{Filter, FilterChangeset, FilterInner}; -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +#[derive(Debug, Default, Clone, ProtoBuf_Enum, Eq, PartialEq, Copy)] +#[repr(u8)] +pub enum FilterType { + #[default] + Data = 0, + And = 1, + Or = 2, +} + +impl From<&FilterInner> for FilterType { + fn from(value: &FilterInner) -> Self { + match value { + FilterInner::And { .. } => Self::And, + FilterInner::Or { .. } => Self::Or, + FilterInner::Data { .. } => Self::Data, + } + } +} + +#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)] pub struct FilterPB { #[pb(index = 1)] pub id: String, #[pb(index = 2)] - pub field_id: String, + pub filter_type: FilterType, #[pb(index = 3)] + pub children: Vec, + + #[pb(index = 4, one_of)] + pub data: Option, +} + +#[derive(Debug, Default, Clone, ProtoBuf, Eq, PartialEq)] +pub struct FilterDataPB { + #[pb(index = 1)] + pub field_id: String, + + #[pb(index = 2)] pub field_type: FieldType, - #[pb(index = 4)] + #[pb(index = 3)] pub data: Vec, } -impl std::convert::From<&Filter> for FilterPB { +impl From<&Filter> for FilterPB { fn from(filter: &Filter) -> Self { - let bytes: Bytes = match filter.field_type { - FieldType::RichText => TextFilterPB::from(filter).try_into().unwrap(), - FieldType::Number => NumberFilterPB::from(filter).try_into().unwrap(), - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { - DateFilterPB::from(filter).try_into().unwrap() + match &filter.inner { + FilterInner::And { children } | FilterInner::Or { children } => Self { + id: filter.id.clone(), + filter_type: FilterType::from(&filter.inner), + children: children.iter().map(FilterPB::from).collect(), + data: None, + }, + FilterInner::Data { + field_id, + field_type, + condition_and_content, + } => { + let bytes: Result = match field_type { + FieldType::RichText | FieldType::URL => condition_and_content + .cloned::() + .unwrap() + .try_into(), + FieldType::Number => condition_and_content + .cloned::() + .unwrap() + .try_into(), + FieldType::DateTime | FieldType::CreatedTime | FieldType::LastEditedTime => { + condition_and_content + .cloned::() + .unwrap() + .try_into() + }, + FieldType::SingleSelect | FieldType::MultiSelect => condition_and_content + .cloned::() + .unwrap() + .try_into(), + FieldType::Checklist => condition_and_content + .cloned::() + .unwrap() + .try_into(), + FieldType::Checkbox => condition_and_content + .cloned::() + .unwrap() + .try_into(), + + FieldType::Relation => condition_and_content + .cloned::() + .unwrap() + .try_into(), + }; + + Self { + id: filter.id.clone(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: field_id.clone(), + field_type: *field_type, + data: bytes.unwrap().to_vec(), + }), + } }, - FieldType::SingleSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(), - FieldType::MultiSelect => SelectOptionFilterPB::from(filter).try_into().unwrap(), - FieldType::Checklist => ChecklistFilterPB::from(filter).try_into().unwrap(), - FieldType::Checkbox => CheckboxFilterPB::from(filter).try_into().unwrap(), - FieldType::URL => TextFilterPB::from(filter).try_into().unwrap(), - FieldType::Relation => RelationFilterPB::from(filter).try_into().unwrap(), - }; - Self { - id: filter.id.clone(), - field_id: filter.field_id.clone(), - field_type: filter.field_type, - data: bytes.to_vec(), } } } +impl TryFrom for FilterInner { + type Error = ErrorCode; + + fn try_from(value: FilterDataPB) -> Result { + let bytes: &[u8] = value.data.as_ref(); + let condition_and_content = match value.field_type { + FieldType::RichText | FieldType::URL => { + BoxAny::new(TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + FieldType::Checkbox => { + BoxAny::new(CheckboxFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + FieldType::Number => { + BoxAny::new(NumberFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + BoxAny::new(DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + FieldType::SingleSelect | FieldType::MultiSelect => { + BoxAny::new(SelectOptionFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + FieldType::Checklist => { + BoxAny::new(ChecklistFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + FieldType::Relation => { + BoxAny::new(RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?) + }, + }; + + Ok(Self::Data { + field_id: value.field_id, + field_type: value.field_type, + condition_and_content, + }) + } +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct RepeatedFilterPB { #[pb(index = 1)] pub items: Vec, } -impl std::convert::From>> for RepeatedFilterPB { - fn from(filters: Vec>) -> Self { +impl From<&Vec> for RepeatedFilterPB { + fn from(filters: &Vec) -> Self { RepeatedFilterPB { - items: filters.into_iter().map(|rev| rev.as_ref().into()).collect(), + items: filters.iter().map(|filter| filter.into()).collect(), } } } -impl std::convert::From> for RepeatedFilterPB { +impl From> for RepeatedFilterPB { fn from(items: Vec) -> Self { Self { items } } } #[derive(ProtoBuf, Debug, Default, Clone, Validate)] -pub struct DeleteFilterPayloadPB { - #[pb(index = 1)] - #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] - pub field_id: String, +pub struct InsertFilterPB { + /// If None, the filter will be the root of a new filter tree + #[pb(index = 1, one_of)] + #[validate(custom = "crate::entities::utils::validate_filter_id")] + pub parent_filter_id: Option, #[pb(index = 2)] - pub field_type: FieldType, - - #[pb(index = 3)] - #[validate(custom = "crate::entities::utils::validate_filter_id")] - pub filter_id: String, - - #[pb(index = 4)] - #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] - pub view_id: String, + pub data: FilterDataPB, } #[derive(ProtoBuf, Debug, Default, Clone, Validate)] -pub struct UpdateFilterPayloadPB { +pub struct UpdateFilterTypePB { #[pb(index = 1)] - #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] - pub field_id: String, + #[validate(custom = "crate::entities::utils::validate_filter_id")] + pub filter_id: String, #[pb(index = 2)] - pub field_type: FieldType, + pub filter_type: FilterType, +} - /// Create a new filter if the filter_id is None - #[pb(index = 3, one_of)] +#[derive(ProtoBuf, Debug, Default, Clone, Validate)] +pub struct UpdateFilterDataPB { + #[pb(index = 1)] #[validate(custom = "crate::entities::utils::validate_filter_id")] - pub filter_id: Option, + pub filter_id: String, - #[pb(index = 4)] - pub data: Vec, + #[pb(index = 2)] + pub data: FilterDataPB, +} - #[pb(index = 5)] +#[derive(ProtoBuf, Debug, Default, Clone, Validate)] +pub struct DeleteFilterPB { + #[pb(index = 1)] + #[validate(custom = "crate::entities::utils::validate_filter_id")] + pub filter_id: String, + + #[pb(index = 2)] #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] - pub view_id: String, + pub field_id: String, } -impl UpdateFilterPayloadPB { - #[allow(dead_code)] - pub fn new>( - view_id: &str, - field: &Field, - data: T, - ) -> Self { - let data = data.try_into().unwrap_or_else(|_| Bytes::new()); - let field_type = FieldType::from(field.field_type); - Self { - view_id: view_id.to_owned(), - field_id: field.id.clone(), - field_type, - filter_id: None, - data: data.to_vec(), - } - } -} - -impl TryInto for UpdateFilterPayloadPB { +impl TryFrom for FilterChangeset { type Error = ErrorCode; - fn try_into(self) -> Result { - let view_id = NotEmptyStr::parse(self.view_id) - .map_err(|_| ErrorCode::DatabaseViewIdIsEmpty)? - .0; - - let field_id = NotEmptyStr::parse(self.field_id) - .map_err(|_| ErrorCode::FieldIdIsEmpty)? - .0; - let filter_id = match self.filter_id { - None => None, - Some(filter_id) => Some( - NotEmptyStr::parse(filter_id) - .map_err(|_| ErrorCode::FilterIdIsEmpty)? - .0, - ), + fn try_from(value: InsertFilterPB) -> Result { + let changeset = Self::Insert { + parent_filter_id: value.parent_filter_id, + data: value.data.try_into()?, }; - let condition; - let mut content = "".to_string(); - let bytes: &[u8] = self.data.as_ref(); - match self.field_type { - FieldType::RichText | FieldType::URL => { - let filter = TextFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; - condition = filter.condition as u8; - content = filter.content; - }, - FieldType::Checkbox => { - let filter = CheckboxFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; - condition = filter.condition as u8; - }, - FieldType::Number => { - let filter = NumberFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; - condition = filter.condition as u8; - content = filter.content; - }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { - let filter = DateFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; - condition = filter.condition as u8; - content = DateFilterContentPB { - start: filter.start, - end: filter.end, - timestamp: filter.timestamp, - } - .to_string(); - }, - FieldType::SingleSelect | FieldType::MultiSelect | FieldType::Checklist => { - let filter = SelectOptionFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; - condition = filter.condition as u8; - content = SelectOptionIds::from(filter.option_ids).to_string(); - }, - FieldType::Relation => { - let filter = RelationFilterPB::try_from(bytes).map_err(|_| ErrorCode::ProtobufSerde)?; - condition = filter.condition as u8; - }, - } - - Ok(UpdateFilterParams { - view_id, - field_id, - filter_id, - field_type: self.field_type, - condition: condition as i64, - content, - }) + Ok(changeset) } } -#[derive(Debug)] -pub struct UpdateFilterParams { - pub view_id: String, - pub field_id: String, - /// Create a new filter if the filter_id is None - pub filter_id: Option, - pub field_type: FieldType, - pub condition: i64, - pub content: String, +impl TryFrom for FilterChangeset { + type Error = ErrorCode; + + fn try_from(value: UpdateFilterDataPB) -> Result { + let changeset = Self::UpdateData { + filter_id: value.filter_id, + data: value.data.try_into()?, + }; + + Ok(changeset) + } +} + +impl TryFrom for FilterChangeset { + type Error = ErrorCode; + + fn try_from(value: UpdateFilterTypePB) -> Result { + if matches!(value.filter_type, FilterType::Data) { + return Err(ErrorCode::InvalidParams); + } + + let changeset = Self::UpdateType { + filter_id: value.filter_id, + filter_type: value.filter_type, + }; + Ok(changeset) + } +} + +impl From for FilterChangeset { + fn from(value: DeleteFilterPB) -> Self { + Self::Delete { + filter_id: value.filter_id, + field_id: value.field_id, + } + } } diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs index 05cc0c2723..9f40685702 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group.rs @@ -76,9 +76,6 @@ pub struct GroupPB { #[pb(index = 2)] pub group_id: String, - #[pb(index = 3)] - pub group_name: String, - #[pb(index = 4)] pub rows: Vec, @@ -94,7 +91,6 @@ impl std::convert::From for GroupPB { Self { field_id: group_data.field_id, group_id: group_data.id, - group_name: group_data.name, rows: group_data.rows.into_iter().map(RowMetaPB::from).collect(), is_default: group_data.is_default, is_visible: group_data.is_visible, diff --git a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs index 59bab13169..f002e93bd2 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/group_entities/group_changeset.rs @@ -11,16 +11,13 @@ pub struct GroupRowsNotificationPB { #[pb(index = 1)] pub group_id: String, - #[pb(index = 2, one_of)] - pub group_name: Option, - - #[pb(index = 3)] + #[pb(index = 2)] pub inserted_rows: Vec, - #[pb(index = 4)] + #[pb(index = 3)] pub deleted_rows: Vec, - #[pb(index = 5)] + #[pb(index = 4)] pub updated_rows: Vec, } @@ -43,10 +40,7 @@ impl std::fmt::Display for GroupRowsNotificationPB { impl GroupRowsNotificationPB { pub fn is_empty(&self) -> bool { - self.group_name.is_none() - && self.inserted_rows.is_empty() - && self.deleted_rows.is_empty() - && self.updated_rows.is_empty() + self.inserted_rows.is_empty() && self.deleted_rows.is_empty() && self.updated_rows.is_empty() } pub fn new(group_id: String) -> Self { @@ -56,14 +50,6 @@ impl GroupRowsNotificationPB { } } - pub fn name(group_id: String, name: &str) -> Self { - Self { - group_id, - group_name: Some(name.to_owned()), - ..Default::default() - } - } - pub fn insert(group_id: String, inserted_rows: Vec) -> Self { Self { group_id, diff --git a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs index 42e9341839..597bb293cc 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/row_entities.rs @@ -1,10 +1,13 @@ use std::collections::HashMap; use collab_database::rows::{Row, RowDetail, RowId}; -use collab_database::views::{OrderObjectPosition, RowOrder}; +use collab_database::views::RowOrder; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; +use lib_infra::validator_fn::required_not_empty_str; +use serde::{Deserialize, Serialize}; +use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::position_entities::OrderObjectPositionPB; @@ -47,7 +50,7 @@ impl From for RowPB { } } -#[derive(Debug, Default, Clone, ProtoBuf)] +#[derive(Debug, Default, Clone, ProtoBuf, Serialize, Deserialize)] pub struct RowMetaPB { #[pb(index = 1)] pub id: String, @@ -335,46 +338,25 @@ impl TryInto for RowIdPB { } } -#[derive(ProtoBuf, Default)] +#[derive(ProtoBuf, Default, Validate)] pub struct CreateRowPayloadPB { #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] pub view_id: String, #[pb(index = 2)] pub row_position: OrderObjectPositionPB, #[pb(index = 3, one_of)] + #[validate(custom = "required_not_empty_str")] pub group_id: Option, - #[pb(index = 4, one_of)] - pub data: Option, -} - -#[derive(ProtoBuf, Default)] -pub struct RowDataPB { - #[pb(index = 1)] - pub cell_data_by_field_id: HashMap, + #[pb(index = 4)] + pub data: HashMap, } #[derive(Default)] pub struct CreateRowParams { - pub view_id: String, - pub row_position: OrderObjectPosition, - pub group_id: Option, - pub cell_data_by_field_id: Option>, -} - -impl TryInto for CreateRowPayloadPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let view_id = NotEmptyStr::parse(self.view_id).map_err(|_| ErrorCode::ViewIdIsInvalid)?; - let position = self.row_position.try_into()?; - Ok(CreateRowParams { - view_id: view_id.0, - row_position: position, - group_id: self.group_id, - cell_data_by_field_id: self.data.map(|data| data.cell_data_by_field_id), - }) - } + pub collab_params: collab_database::rows::CreateRowParams, + pub open_after_create: bool, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs index e1cefc07cc..2cb2f3613d 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -9,9 +9,9 @@ use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::{ - CalendarLayoutSettingPB, DeleteFilterPayloadPB, DeleteSortPayloadPB, RepeatedFieldSettingsPB, - RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB, UpdateFilterPayloadPB, UpdateGroupPB, - UpdateSortPayloadPB, + CalendarLayoutSettingPB, DeleteFilterPB, DeleteSortPayloadPB, InsertFilterPB, + RepeatedFieldSettingsPB, RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB, + UpdateFilterDataPB, UpdateFilterTypePB, UpdateGroupPB, UpdateSortPayloadPB, }; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; @@ -79,26 +79,34 @@ pub struct DatabaseSettingChangesetPB { #[pb(index = 3, one_of)] #[validate] - pub update_filter: Option, + pub insert_filter: Option, #[pb(index = 4, one_of)] #[validate] - pub delete_filter: Option, + pub update_filter_type: Option, #[pb(index = 5, one_of)] #[validate] - pub update_group: Option, + pub update_filter_data: Option, #[pb(index = 6, one_of)] #[validate] - pub update_sort: Option, + pub delete_filter: Option, #[pb(index = 7, one_of)] #[validate] - pub reorder_sort: Option, + pub update_group: Option, #[pb(index = 8, one_of)] #[validate] + pub update_sort: Option, + + #[pb(index = 9, one_of)] + #[validate] + pub reorder_sort: Option, + + #[pb(index = 10, one_of)] + #[validate] pub delete_sort: Option, } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index e14908b090..78528f255f 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -1,17 +1,15 @@ use std::sync::{Arc, Weak}; -use collab_database::database::gen_row_id; use collab_database::rows::RowId; use lib_infra::box_any::BoxAny; use tokio::sync::oneshot; +use tracing::error; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{af_spawn, data_result_ok, AFPluginData, AFPluginState, DataResult}; -use lib_infra::util::timestamp; use crate::entities::*; use crate::manager::DatabaseManager; -use crate::services::cell::CellBuilder; use crate::services::field::{ type_option_data_from_pb, ChecklistCellChangeset, DateCellChangeset, RelationCellChangeset, SelectOptionCellChangeset, @@ -91,14 +89,28 @@ pub(crate) async fn update_database_setting_handler( let params = data.try_into_inner()?; let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - if let Some(update_filter) = params.update_filter { + if let Some(payload) = params.insert_filter { database_editor - .create_or_update_filter(update_filter.try_into()?) + .modify_view_filters(¶ms.view_id, payload.try_into()?) .await?; } - if let Some(delete_filter) = params.delete_filter { - database_editor.delete_filter(delete_filter).await?; + if let Some(payload) = params.update_filter_type { + database_editor + .modify_view_filters(¶ms.view_id, payload.try_into()?) + .await?; + } + + if let Some(payload) = params.update_filter_data { + database_editor + .modify_view_filters(¶ms.view_id, payload.try_into()?) + .await?; + } + + if let Some(payload) = params.delete_filter { + database_editor + .modify_view_filters(¶ms.view_id, payload.into()) + .await?; } if let Some(update_sort) = params.update_sort { @@ -245,6 +257,20 @@ pub(crate) async fn delete_field_handler( Ok(()) } +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn clear_field_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let params: FieldIdParams = data.into_inner().try_into()?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; + database_editor + .clear_field(¶ms.view_id, ¶ms.field_id) + .await?; + Ok(()) +} + #[tracing::instrument(level = "debug", skip(data, manager), err)] pub(crate) async fn switch_to_field_handler( data: AFPluginData, @@ -379,7 +405,7 @@ pub(crate) async fn duplicate_row_handler( let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; database_editor .duplicate_row(¶ms.view_id, ¶ms.row_id) - .await; + .await?; Ok(()) } @@ -403,27 +429,12 @@ pub(crate) async fn create_row_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let params: CreateRowParams = data.into_inner().try_into()?; + let params = data.try_into_inner()?; let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; - let fields = database_editor.get_fields(¶ms.view_id, None); - let cells = - CellBuilder::with_cells(params.cell_data_by_field_id.unwrap_or_default(), &fields).build(); - let view_id = params.view_id; - let group_id = params.group_id; - let params = collab_database::rows::CreateRowParams { - id: gen_row_id(), - cells, - height: 60, - visibility: true, - row_position: params.row_position, - timestamp: timestamp(), - }; - match database_editor - .create_row(&view_id, group_id, params) - .await? - { - None => Err(FlowyError::internal().with_context("Create row fail")), + + match database_editor.create_row(params).await? { Some(row) => data_result_ok(RowMetaPB::from(row)), + None => Err(FlowyError::internal().with_context("Error creating row")), } } @@ -671,7 +682,7 @@ pub(crate) async fn update_group_handler( let (tx, rx) = oneshot::channel(); af_spawn(async move { let result = database_editor - .update_group(&view_id, vec![group_changeset].into()) + .update_group(&view_id, vec![group_changeset]) .await; let _ = tx.send(result); }); @@ -745,7 +756,22 @@ pub(crate) async fn get_databases_handler( manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; - let data = manager.get_all_databases_description().await; + let metas = manager.get_all_databases_meta().await; + + let mut items = Vec::with_capacity(metas.len()); + for meta in metas { + match manager.get_database_inline_view_id(&meta.database_id).await { + Ok(view_id) => items.push(DatabaseMetaPB { + database_id: meta.database_id, + inline_view_id: view_id, + }), + Err(err) => { + error!(?err); + }, + } + } + + let data = RepeatedDatabaseDescriptionPB { items }; data_result_ok(data) } diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index 17e9c68ff5..8b36e68946 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -14,7 +14,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .state(database_manager); plugin .event(DatabaseEvent::GetDatabase, get_database_data_handler) - .event(DatabaseEvent::OpenDatabase, get_database_data_handler) + .event(DatabaseEvent::GetDatabaseData, get_database_data_handler) .event(DatabaseEvent::GetDatabaseId, get_database_id_handler) .event(DatabaseEvent::GetDatabaseSetting, get_database_setting_handler) .event(DatabaseEvent::UpdateDatabaseSetting, update_database_setting_handler) @@ -27,6 +27,7 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::UpdateField, update_field_handler) .event(DatabaseEvent::UpdateFieldTypeOption, update_field_type_option_handler) .event(DatabaseEvent::DeleteField, delete_field_handler) + .event(DatabaseEvent::ClearField, clear_field_handler) .event(DatabaseEvent::UpdateFieldType, switch_to_field_handler) .event(DatabaseEvent::DuplicateField, duplicate_field_handler) .event(DatabaseEvent::MoveField, move_field_handler) @@ -127,7 +128,7 @@ pub enum DatabaseEvent { DeleteAllSorts = 6, #[event(input = "DatabaseViewIdPB")] - OpenDatabase = 7, + GetDatabaseData = 7, /// [GetFields] event is used to get the database's fields. /// @@ -161,6 +162,11 @@ pub enum DatabaseEvent { #[event(input = "DeleteFieldPayloadPB")] DeleteField = 14, + /// [ClearField] event is used to clear all Cells in a Field. [ClearFieldPayloadPB] is the context that + /// is used to clear the field from the Database. + #[event(input = "ClearFieldPayloadPB")] + ClearField = 15, + /// [UpdateFieldType] event is used to update the current Field's type. /// It will insert a new FieldTypeOptionData if the new FieldType doesn't exist before, otherwise /// reuse the existing FieldTypeOptionData. You could check the [DatabaseRevisionPad] for more details. diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 7dc17d9e8d..e1557d5511 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -1,19 +1,17 @@ +use anyhow::anyhow; use std::collections::HashMap; -use std::num::NonZeroUsize; use std::sync::{Arc, Weak}; -use collab::core::collab::{CollabDocState, MutexCollab}; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab_database::blocks::BlockEvent; use collab_database::database::{DatabaseData, MutexDatabase}; use collab_database::error::DatabaseError; -use collab_database::user::{ - CollabDocStateByOid, CollabFuture, DatabaseCollabService, WorkspaceDatabase, -}; use collab_database::views::{CreateDatabaseParams, CreateViewParams, DatabaseLayout}; +use collab_database::workspace_database::{ + CollabDocStateByOid, CollabFuture, DatabaseCollabService, DatabaseMeta, WorkspaceDatabase, +}; use collab_entity::CollabType; use collab_plugins::local_storage::kv::KVTransactionDB; -use futures::executor::block_on; -use lru::LruCache; use tokio::sync::{Mutex, RwLock}; use tracing::{event, instrument, trace}; @@ -24,10 +22,7 @@ use flowy_error::{internal_error, FlowyError, FlowyResult}; use lib_dispatch::prelude::af_spawn; use lib_infra::priority_task::TaskDispatcher; -use crate::entities::{ - DatabaseDescriptionPB, DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB, - RepeatedDatabaseDescriptionPB, -}; +use crate::entities::{DatabaseLayoutPB, DatabaseSnapshotPB, DidFetchRowPB}; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::database::DatabaseEditor; use crate::services::database_view::DatabaseLayoutDepsResolver; @@ -43,7 +38,7 @@ pub struct DatabaseManager { user: Arc, workspace_database: Arc>>>, task_scheduler: Arc>, - editors: Mutex>>, + editors: Mutex>>, collab_builder: Arc, cloud_service: Arc, } @@ -55,12 +50,11 @@ impl DatabaseManager { collab_builder: Arc, cloud_service: Arc, ) -> Self { - let editors = Mutex::new(LruCache::new(NonZeroUsize::new(5).unwrap())); Self { user: database_user, workspace_database: Default::default(), task_scheduler, - editors, + editors: Default::default(), collab_builder, cloud_service, } @@ -87,7 +81,7 @@ impl DatabaseManager { self.task_scheduler.write().await.clear_task(); // 2. Release all existing editors for (_, editor) in self.editors.lock().await.iter() { - editor.close().await; + editor.close_all_views().await; } self.editors.lock().await.clear(); // 3. Clear the workspace database @@ -101,7 +95,7 @@ impl DatabaseManager { }; let config = CollabPersistenceConfig::new().snapshot_per_update(100); - let mut workspace_database_doc_state = CollabDocState::default(); + let mut workspace_database_doc_state = DocStateSource::FromDisk; // If the workspace database not exist in disk, try to fetch from remote. if !self.is_collab_exist(uid, &collab_db, &workspace_database_object_id) { trace!("workspace database not exist, try to fetch from remote"); @@ -114,8 +108,13 @@ impl DatabaseManager { ) .await { - Ok(remote_doc_state) => { - workspace_database_doc_state = remote_doc_state; + Ok(doc_state) => match doc_state { + Some(doc_state) => { + workspace_database_doc_state = DocStateSource::FromDocState(doc_state); + }, + None => { + workspace_database_doc_state = DocStateSource::FromDisk; + }, }, Err(err) => { return Err(FlowyError::record_not_found().with_context(format!( @@ -139,7 +138,7 @@ impl DatabaseManager { collab_db.clone(), workspace_database_doc_state, config.clone(), - ); + )?; let workspace_database = WorkspaceDatabase::open(uid, collab, collab_db, config, collab_builder); *self.workspace_database.write().await = Some(Arc::new(workspace_database)); @@ -164,16 +163,22 @@ impl DatabaseManager { Ok(()) } - pub async fn get_all_databases_description(&self) -> RepeatedDatabaseDescriptionPB { + pub async fn get_database_inline_view_id(&self, database_id: &str) -> FlowyResult { + let wdb = self.get_workspace_database().await?; + let database_collab = wdb.get_database(database_id).await.ok_or_else(|| { + FlowyError::record_not_found().with_context(format!("The database:{} not found", database_id)) + })?; + + let lock_guard = database_collab.lock(); + Ok(lock_guard.get_inline_view_id()) + } + + pub async fn get_all_databases_meta(&self) -> Vec { let mut items = vec![]; if let Ok(wdb) = self.get_workspace_database().await { - items = wdb - .get_all_databases() - .into_iter() - .map(DatabaseDescriptionPB::from) - .collect(); + items = wdb.get_all_database_meta() } - RepeatedDatabaseDescriptionPB { items } + items } pub async fn track_database( @@ -206,11 +211,12 @@ impl DatabaseManager { if let Some(editor) = self.editors.lock().await.get(database_id).cloned() { return Ok(editor); } + // TODO(nathan): refactor the get_database that split the database creation and database opening. self.open_database(database_id).await } pub async fn open_database(&self, database_id: &str) -> FlowyResult> { - trace!("create new editor for database {}", database_id); + trace!("open database editor:{}", database_id); let database = self .get_workspace_database() .await? @@ -226,19 +232,42 @@ impl DatabaseManager { .editors .lock() .await - .put(database_id.to_string(), editor.clone()); + .insert(database_id.to_string(), editor.clone()); Ok(editor) } - #[tracing::instrument(level = "debug", skip_all)] + pub async fn open_database_view>(&self, view_id: T) -> FlowyResult<()> { + let view_id = view_id.as_ref(); + let wdb = self.get_workspace_database().await?; + if let Some(database_id) = wdb.get_database_id_with_view_id(view_id) { + if let Some(database) = wdb.open_database(&database_id) { + if let Some(lock_database) = database.try_lock() { + if let Some(lock_collab) = lock_database.get_collab().try_lock() { + trace!("{} database start init sync", view_id); + lock_collab.start_init_sync(); + } + } + } + } + Ok(()) + } + pub async fn close_database_view>(&self, view_id: T) -> FlowyResult<()> { let view_id = view_id.as_ref(); let wdb = self.get_workspace_database().await?; let database_id = wdb.get_database_id_with_view_id(view_id); if let Some(database_id) = database_id { let mut editors = self.editors.lock().await; + let mut should_remove = false; if let Some(editor) = editors.get(&database_id) { editor.close_view(view_id).await; + should_remove = editor.num_views().await == 0; + } + + if should_remove { + trace!("remove database editor:{}", database_id); + editors.remove(&database_id); + wdb.close_database(&database_id); } } @@ -415,21 +444,21 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { &self, object_id: &str, object_ty: CollabType, - ) -> CollabFuture> { + ) -> CollabFuture> { let workspace_id = self.workspace_id.clone(); let object_id = object_id.to_string(); let weak_cloud_service = Arc::downgrade(&self.cloud_service); Box::pin(async move { match weak_cloud_service.upgrade() { - None => { - tracing::warn!("Cloud service is dropped"); - Ok(vec![]) - }, + None => Err(DatabaseError::Internal(anyhow!("Cloud service is dropped"))), Some(cloud_service) => { - let updates = cloud_service + let doc_state = cloud_service .get_database_object_doc_state(&object_id, object_ty, &workspace_id) .await?; - Ok(updates) + match doc_state { + None => Ok(DocStateSource::FromDisk), + Some(doc_state) => Ok(DocStateSource::FromDocState(doc_state)), + } }, } }) @@ -464,18 +493,18 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { object_id: &str, object_type: CollabType, collab_db: Weak, - collab_raw_data: CollabDocState, + collab_raw_data: DocStateSource, persistence_config: CollabPersistenceConfig, - ) -> Arc { - block_on(self.collab_builder.build_with_config( + ) -> Result, DatabaseError> { + let collab = self.collab_builder.build_with_config( uid, object_id, - object_type, - collab_db, + object_type.clone(), + collab_db.clone(), collab_raw_data, persistence_config, CollabBuilderConfig::default().sync_enable(true), - )) - .unwrap() + )?; + Ok(collab) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs index ba12d76ab9..3ba34eb4b6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs @@ -36,8 +36,8 @@ impl CalculationsService { let mut sum = 0.0; let mut len = 0.0; let field_type = FieldType::from(field.field_type); - if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None) - .get_type_option_cell_data_handler(&field_type) + if let Some(handler) = + TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type) { for row_cell in row_cells { if let Some(cell) = &row_cell.cell { @@ -131,8 +131,8 @@ impl CalculationsService { fn calculate_count_empty(&self, field: &Field, row_cells: Vec>) -> String { let field_type = FieldType::from(field.field_type); - if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None) - .get_type_option_cell_data_handler(&field_type) + if let Some(handler) = + TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type) { if !row_cells.is_empty() { return format!( @@ -154,8 +154,8 @@ impl CalculationsService { fn calculate_count_non_empty(&self, field: &Field, row_cells: Vec>) -> String { let field_type = FieldType::from(field.field_type); - if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None) - .get_type_option_cell_data_handler(&field_type) + if let Some(handler) = + TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type) { if !row_cells.is_empty() { return format!( @@ -183,8 +183,8 @@ impl CalculationsService { let mut values = vec![]; let field_type = FieldType::from(field.field_type); - if let Some(handler) = TypeOptionCellExt::new_with_cell_data_cache(field, None) - .get_type_option_cell_data_handler(&field_type) + if let Some(handler) = + TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(&field_type) { for row_cell in row_cells { if let Some(cell) = &row_cell.cell { diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs index 30e61dd098..07864351d4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_data_cache.rs @@ -4,4 +4,3 @@ use std::sync::Arc; use crate::utils::cache::AnyTypeCache; pub type CellCache = Arc>>; -pub type CellFilterCache = Arc>>; diff --git a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs index 4caafafef0..4d9140c2b6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/cell/cell_operation.rs @@ -75,7 +75,7 @@ pub fn apply_cell_changeset( cell_data_cache: Option, ) -> Result { let field_type = FieldType::from(field.field_type); - match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) + match TypeOptionCellExt::new(field, cell_data_cache) .get_type_option_cell_data_handler(&field_type) { None => Ok(Cell::default()), @@ -128,7 +128,7 @@ pub fn try_decode_cell_to_cell_protobuf( field: &Field, cell_data_cache: Option, ) -> FlowyResult { - match TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) + match TypeOptionCellExt::new(field, cell_data_cache) .get_type_option_cell_data_handler(to_field_type) { None => Ok(CellProtobufBlob::default()), @@ -143,7 +143,7 @@ pub fn try_decode_cell_to_cell_data( field: &Field, cell_data_cache: Option, ) -> Option { - let handler = TypeOptionCellExt::new_with_cell_data_cache(field, cell_data_cache) + let handler = TypeOptionCellExt::new(field, cell_data_cache) .get_type_option_cell_data_handler(to_field_type)?; handler .get_cell_data(cell, from_field_type, field) @@ -152,8 +152,8 @@ pub fn try_decode_cell_to_cell_data( } /// Returns a string that represents the current field_type's cell data. -/// For example, The string of the Multi-Select cell will be a list of the option's name -/// separated by a comma. +/// For example, a Multi-Select cell will be represented by a list of the options' names +/// separated by commas. /// /// # Arguments /// @@ -162,16 +162,13 @@ pub fn try_decode_cell_to_cell_data( /// * `from_field_type`: the original field type of the passed-in cell data. /// * `field`: used to get the corresponding TypeOption for the specified field type. /// -/// returns: String pub fn stringify_cell_data( cell: &Cell, to_field_type: &FieldType, from_field_type: &FieldType, field: &Field, ) -> String { - match TypeOptionCellExt::new_with_cell_data_cache(field, None) - .get_type_option_cell_data_handler(from_field_type) - { + match TypeOptionCellExt::new(field, None).get_type_option_cell_data_handler(from_field_type) { None => "".to_string(), Some(handler) => handler.handle_stringify_cell(cell, to_field_type, field), } diff --git a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs index 7e79d74291..fc1c8c5923 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database/database_editor.rs @@ -1,20 +1,3 @@ -use std::collections::HashMap; -use std::sync::Arc; - -use collab_database::database::MutexDatabase; -use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Cells, CreateRowParams, Row, RowCell, RowDetail, RowId}; -use collab_database::views::{DatabaseLayout, DatabaseView, LayoutSetting, OrderObjectPosition}; -use futures::StreamExt; -use lib_infra::box_any::BoxAny; -use tokio::sync::{broadcast, RwLock}; -use tracing::{event, warn}; - -use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; -use lib_dispatch::prelude::af_spawn; -use lib_infra::future::{to_fut, Fut, FutureResult}; -use lib_infra::priority_task::TaskDispatcher; - use crate::entities::*; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::calculations::Calculation; @@ -32,11 +15,27 @@ use crate::services::field::{ use crate::services::field_settings::{ default_field_settings_by_layout_map, FieldSettings, FieldSettingsChangesetParams, }; -use crate::services::filter::Filter; -use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset}; +use crate::services::filter::{Filter, FilterChangeset}; +use crate::services::group::{default_group_setting, GroupChangeset, GroupSetting, RowChangeset}; use crate::services::share::csv::{CSVExport, CSVFormat}; use crate::services::sort::Sort; use crate::utils::cache::AnyTypeCache; +use collab_database::database::MutexDatabase; +use collab_database::fields::{Field, TypeOptionData}; +use collab_database::rows::{Cell, Cells, Row, RowCell, RowDetail, RowId}; +use collab_database::views::{ + DatabaseLayout, DatabaseView, FilterMap, LayoutSetting, OrderObjectPosition, +}; +use flowy_error::{internal_error, ErrorCode, FlowyError, FlowyResult}; +use futures::StreamExt; +use lib_dispatch::prelude::af_spawn; +use lib_infra::box_any::BoxAny; +use lib_infra::future::{to_fut, Fut, FutureResult}; +use lib_infra::priority_task::TaskDispatcher; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::{broadcast, RwLock}; +use tracing::{event, warn}; #[derive(Clone)] pub struct DatabaseEditor { @@ -113,24 +112,16 @@ impl DatabaseEditor { }) } - /// Returns bool value indicating whether the database is empty. - /// - #[tracing::instrument(level = "debug", skip_all)] - pub async fn close_view(&self, view_id: &str) -> bool { - // If the database is empty, flush the database to the disk. - if self.database_views.editors().await.len() == 1 { - if let Some(database) = self.database.try_lock() { - let _ = database.flush(); - } - } - self.database_views.close_view(view_id).await + pub async fn close_view(&self, view_id: &str) { + self.database_views.close_view(view_id).await; + } + + pub async fn num_views(&self) -> usize { + self.database_views.num_editors().await } #[tracing::instrument(level = "debug", skip_all)] - pub async fn close(&self) { - if let Some(database) = self.database.try_lock() { - let _ = database.flush(); - } + pub async fn close_all_views(&self) { for view in self.database_views.editors().await { view.close().await; } @@ -208,22 +199,23 @@ impl DatabaseEditor { Ok(self.database.lock().delete_view(view_id)) } - pub async fn update_group(&self, view_id: &str, changesets: GroupChangesets) -> FlowyResult<()> { + pub async fn update_group( + &self, + view_id: &str, + changesets: Vec, + ) -> FlowyResult<()> { let view_editor = self.database_views.get_view_editor(view_id).await?; view_editor.v_update_group(changesets).await?; Ok(()) } - #[tracing::instrument(level = "trace", skip_all, err)] - pub async fn create_or_update_filter(&self, params: UpdateFilterParams) -> FlowyResult<()> { - let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; - view_editor.v_insert_filter(params).await?; - Ok(()) - } - - pub async fn delete_filter(&self, params: DeleteFilterPayloadPB) -> FlowyResult<()> { - let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; - view_editor.v_delete_filter(params).await?; + pub async fn modify_view_filters( + &self, + view_id: &str, + changeset: FilterChangeset, + ) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(view_id).await?; + view_editor.v_modify_filters(changeset).await?; Ok(()) } @@ -267,7 +259,8 @@ impl DatabaseEditor { pub async fn get_all_filters(&self, view_id: &str) -> RepeatedFilterPB { if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { - view_editor.v_get_all_filters().await.into() + let filters = view_editor.v_get_all_filters().await; + RepeatedFilterPB::from(&filters) } else { RepeatedFilterPB { items: vec![] } } @@ -357,6 +350,30 @@ impl DatabaseEditor { Ok(()) } + pub async fn clear_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { + let field_type: FieldType = self + .get_field(field_id) + .map(|field| field.field_type.into()) + .unwrap_or_default(); + + if matches!( + field_type, + FieldType::LastEditedTime | FieldType::CreatedTime + ) { + return Err(FlowyError::new( + ErrorCode::Internal, + "Can not clear the field type of Last Edited Time or Created Time.", + )); + } + + let cells: Vec = self.get_cells_for_field(view_id, field_id).await; + for row_cell in cells { + self.clear_cell(view_id, row_cell.row_id, field_id).await?; + } + + Ok(()) + } + /// Update the field type option data. /// Do nothing if the [TypeOptionData] is empty. pub async fn update_field_type_option( @@ -457,20 +474,33 @@ impl DatabaseEditor { Ok(()) } - // consider returning a result. But most of the time, it should be fine to just ignore the error. - pub async fn duplicate_row(&self, view_id: &str, row_id: &RowId) { - let params = self.database.lock().duplicate_row(row_id); - match params { - None => warn!("Failed to duplicate row: {}", row_id), - Some(params) => { - let result = self.create_row(view_id, None, params).await; - if let Some(row_detail) = result.unwrap_or(None) { - for view in self.database_views.editors().await { - view.v_did_duplicate_row(&row_detail).await; - } - } - }, + pub async fn duplicate_row(&self, view_id: &str, row_id: &RowId) -> FlowyResult<()> { + let (row_detail, index) = { + let database = self.database.lock(); + + let params = database + .duplicate_row(row_id) + .ok_or_else(|| FlowyError::internal().with_context("error while copying row"))?; + + let (index, row_order) = database + .create_row_in_view(view_id, params) + .ok_or_else(|| { + FlowyError::internal().with_context("error while inserting duplicated row") + })?; + + tracing::trace!("duplicated row: {:?} at {}", row_order, index); + let row_detail = database.get_row_detail(&row_order.id); + + (row_detail, index) + }; + + if let Some(row_detail) = row_detail { + for view in self.database_views.editors().await { + view.v_did_create_row(&row_detail, index).await; + } } + + Ok(()) } pub async fn move_row( @@ -506,18 +536,21 @@ impl DatabaseEditor { Ok(()) } - pub async fn create_row( - &self, - view_id: &str, - group_id: Option, - mut params: CreateRowParams, - ) -> FlowyResult> { - for view in self.database_views.editors().await { - view.v_will_create_row(&mut params.cells, &group_id).await; - } - let result = self.database.lock().create_row_in_view(view_id, params); + pub async fn create_row(&self, params: CreateRowPayloadPB) -> FlowyResult> { + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + + let CreateRowParams { + collab_params, + open_after_create: _, + } = view_editor.v_will_create_row(params).await?; + + let result = self + .database + .lock() + .create_row_in_view(&view_editor.view_id, collab_params); + if let Some((index, row_order)) = result { - tracing::trace!("create row: {:?} at {}", row_order, index); + tracing::trace!("created row: {:?} at {}", row_order, index); let row_detail = self.database.lock().get_row_detail(&row_order.id); if let Some(row_detail) = row_detail { for view in self.database_views.editors().await { @@ -761,6 +794,7 @@ impl DatabaseEditor { }?; (field, database.get_cell(field_id, &row_id).cell) }; + let new_cell = apply_cell_changeset(cell_changeset, cell, &field, Some(self.cell_cache.clone()))?; self.update_cell(view_id, row_id, field_id, new_cell).await @@ -784,6 +818,37 @@ impl DatabaseEditor { }); }); + self + .did_update_row(view_id, row_id, field_id, old_row) + .await; + + Ok(()) + } + + pub async fn clear_cell(&self, view_id: &str, row_id: RowId, field_id: &str) -> FlowyResult<()> { + // Get the old row before updating the cell. It would be better to get the old cell + let old_row = { self.get_row_detail(view_id, &row_id) }; + + self.database.lock().update_row(&row_id, |row_update| { + row_update.update_cells(|cell_update| { + cell_update.clear(field_id); + }); + }); + + self + .did_update_row(view_id, row_id, field_id, old_row) + .await; + + Ok(()) + } + + async fn did_update_row( + &self, + view_id: &str, + row_id: RowId, + field_id: &str, + old_row: Option, + ) { let option_row = self.get_row_detail(view_id, &row_id); if let Some(new_row_detail) = option_row { for view in self.database_views.editors().await { @@ -801,8 +866,6 @@ impl DatabaseEditor { self .notify_update_row(view_id, row_id, vec![changeset]) .await; - - Ok(()) } pub fn get_auto_updated_fields_changesets( @@ -1058,7 +1121,7 @@ impl DatabaseEditor { pub async fn group_by_field(&self, view_id: &str, field_id: &str) -> FlowyResult<()> { let view = self.database_views.get_view_editor(view_id).await?; - view.v_grouping_by_field(field_id).await?; + view.v_group_by_field(field_id).await?; Ok(()) } @@ -1259,10 +1322,9 @@ impl DatabaseEditor { row_ids: Option<&Vec>, ) -> FlowyResult> { let primary_field = self.database.lock().fields.get_primary_field().unwrap(); - let handler = - TypeOptionCellExt::new_with_cell_data_cache(&primary_field, Some(self.cell_cache.clone())) - .get_type_option_cell_data_handler(&FieldType::RichText) - .ok_or(FlowyError::internal())?; + let handler = TypeOptionCellExt::new(&primary_field, Some(self.cell_cache.clone())) + .get_type_option_cell_data_handler(&FieldType::RichText) + .ok_or(FlowyError::internal())?; let row_data = { let database = self.database.lock(); @@ -1381,9 +1443,9 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { to_fut(async move { view }) } - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut> { let fields = self.database.lock().get_fields_in_view(view_id, field_ids); - to_fut(async move { fields.into_iter().map(Arc::new).collect() }) + to_fut(async move { fields }) } fn get_field(&self, field_id: &str) -> Option { @@ -1566,13 +1628,12 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { .get_calculation::(view_id, field_id) } - fn get_all_filters(&self, view_id: &str) -> Vec> { + fn get_all_filters(&self, view_id: &str) -> Vec { self .database .lock() .get_all_filters(view_id) .into_iter() - .map(Arc::new) .collect() } @@ -1581,7 +1642,14 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { } fn insert_filter(&self, view_id: &str, filter: Filter) { - self.database.lock().insert_filter(view_id, filter); + self.database.lock().insert_filter(view_id, &filter); + } + + fn save_filters(&self, view_id: &str, filters: &[Filter]) { + self + .database + .lock() + .save_filters::(view_id, filters); } fn get_filter(&self, view_id: &str, filter_id: &str) -> Option { @@ -1591,15 +1659,6 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { .get_filter::(view_id, filter_id) } - fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option { - self - .database - .lock() - .get_all_filters::(view_id) - .into_iter() - .find(|filter| filter.field_id == field_id) - } - fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option { self.database.lock().get_layout_setting(view_id, layout_ty) } @@ -1632,7 +1691,7 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { field: &Field, field_type: &FieldType, ) -> Option> { - TypeOptionCellExt::new_with_cell_data_cache(field, Some(self.cell_cache.clone())) + TypeOptionCellExt::new(field, Some(self.cell_cache.clone())) .get_type_option_cell_data_handler(field_type) } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs index 9a1cbecf98..478aa2c5a4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/notifier.rs @@ -1,12 +1,12 @@ #![allow(clippy::while_let_loop)] use crate::entities::{ CalculationChangesetNotificationPB, DatabaseViewSettingPB, FilterChangesetNotificationPB, - GroupChangesPB, GroupRowsNotificationPB, ReorderAllRowsPB, ReorderSingleRowPB, - RowsVisibilityChangePB, SortChangesetNotificationPB, + GroupChangesPB, GroupRowsNotificationPB, InsertedRowPB, ReorderAllRowsPB, ReorderSingleRowPB, + RowMetaPB, RowsChangePB, RowsVisibilityChangePB, SortChangesetNotificationPB, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::filter::FilterResultNotification; -use crate::services::sort::{InsertSortedRowResult, ReorderAllRowsResult, ReorderSingleRowResult}; +use crate::services::sort::{InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult}; use async_stream::stream; use futures::stream::StreamExt; use tokio::sync::broadcast; @@ -16,7 +16,7 @@ pub enum DatabaseViewChanged { FilterNotification(FilterResultNotification), ReorderAllRowsNotification(ReorderAllRowsResult), ReorderSingleRowNotification(ReorderSingleRowResult), - InsertSortedRowNotification(InsertSortedRowResult), + InsertRowNotification(InsertRowResult), CalculationValueNotification(CalculationChangesetNotificationPB), } @@ -79,7 +79,17 @@ impl DatabaseViewChangedReceiverRunner { .payload(reorder_row) .send() }, - DatabaseViewChanged::InsertSortedRowNotification(_result) => {}, + DatabaseViewChanged::InsertRowNotification(result) => { + let inserted_row = InsertedRowPB { + row_meta: RowMetaPB::from(result.row), + index: Some(result.index as i32), + is_new: true, + }; + let changes = RowsChangePB::from_insert(inserted_row); + send_notification(&result.view_id, DatabaseNotification::DidUpdateViewRows) + .payload(changes) + .send(); + }, DatabaseViewChanged::CalculationValueNotification(notification) => send_notification( ¬ification.view_id, DatabaseNotification::DidUpdateCalculation, diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs index 61ee522a0e..6cef8ccc45 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_editor.rs @@ -2,12 +2,11 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::{ - gen_database_calculation_id, gen_database_filter_id, gen_database_sort_id, -}; -use collab_database::fields::{Field, TypeOptionData}; +use collab_database::database::{gen_database_calculation_id, gen_database_sort_id, gen_row_id}; +use collab_database::fields::Field; use collab_database::rows::{Cells, Row, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView}; +use lib_infra::util::timestamp; use tokio::sync::{broadcast, RwLock}; use tracing::instrument; @@ -15,19 +14,19 @@ use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::af_spawn; use crate::entities::{ - CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterPayloadPB, - DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, - LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, ReorderSortPayloadPB, - RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, - UpdateFilterParams, UpdateSortPayloadPB, + CalendarEventPB, CreateRowParams, CreateRowPayloadPB, DatabaseLayoutMetaPB, + DatabaseLayoutSettingPB, DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, + GroupPB, LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, + ReorderSortPayloadPB, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, + UpdateCalculationChangesetPB, UpdateSortPayloadPB, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController}; -use crate::services::cell::CellCache; +use crate::services::cell::{CellBuilder, CellCache}; use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow}; use crate::services::database_view::view_filter::make_filter_controller; use crate::services::database_view::view_group::{ - get_cell_for_row, get_cells_for_field, new_group_controller, new_group_controller_with_field, + get_cell_for_row, get_cells_for_field, new_group_controller, }; use crate::services::database_view::view_operation::DatabaseViewOperation; use crate::services::database_view::view_sort::make_sort_controller; @@ -37,10 +36,8 @@ use crate::services::database_view::{ DatabaseViewChangedNotifier, DatabaseViewChangedReceiverRunner, }; use crate::services::field_settings::FieldSettings; -use crate::services::filter::{ - Filter, FilterChangeset, FilterContext, FilterController, UpdatedFilter, -}; -use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowContext, RowChangeset}; +use crate::services::filter::{Filter, FilterChangeset, FilterController}; +use crate::services::group::{GroupChangeset, GroupController, MoveGroupRowContext, RowChangeset}; use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{Sort, SortChangeset, SortController}; @@ -71,10 +68,6 @@ impl DatabaseViewEditor { ) -> FlowyResult { let (notifier, _) = broadcast::channel(100); af_spawn(DatabaseViewChangedReceiverRunner(Some(notifier.subscribe())).run()); - // Group - let group_controller = Arc::new(RwLock::new( - new_group_controller(view_id.clone(), delegate.clone()).await?, - )); // Filter let filter_controller = make_filter_controller( @@ -95,6 +88,17 @@ impl DatabaseViewEditor { ) .await; + // Group + let group_controller = Arc::new(RwLock::new( + new_group_controller( + view_id.clone(), + delegate.clone(), + filter_controller.clone(), + None, + ) + .await?, + )); + // Calculations let calculations_controller = make_calculations_controller(&view_id, delegate.clone(), notifier.clone()).await; @@ -120,17 +124,44 @@ impl DatabaseViewEditor { self.delegate.get_view(&self.view_id).await } - pub async fn v_will_create_row(&self, cells: &mut Cells, group_id: &Option) { - if group_id.is_none() { - return; + pub async fn v_will_create_row( + &self, + params: CreateRowPayloadPB, + ) -> FlowyResult { + let mut result = CreateRowParams { + collab_params: collab_database::rows::CreateRowParams { + id: gen_row_id(), + cells: Cells::new(), + height: 60, + visibility: true, + row_position: params.row_position.try_into()?, + timestamp: timestamp(), + }, + open_after_create: false, + }; + + // fill in cells from the frontend + let fields = self.delegate.get_fields(¶ms.view_id, None).await; + let mut cells = CellBuilder::with_cells(params.data, &fields).build(); + + // fill in cells according to group_id if supplied + if let Some(group_id) = params.group_id { + if let Some(controller) = self.group_controller.read().await.as_ref() { + let field = self + .delegate + .get_field(controller.get_grouping_field_id()) + .ok_or_else(|| FlowyError::internal().with_context("Failed to get grouping field"))?; + controller.will_create_row(&mut cells, &field, &group_id); + } } - let group_id = group_id.as_ref().unwrap(); - let _ = self - .mut_group_controller(|group_controller, field| { - group_controller.will_create_row(cells, &field, group_id); - Ok(()) - }) - .await; + + // fill in cells according to active filters + let filter_controller = self.filter_controller.clone(); + filter_controller.fill_cells(&mut cells).await; + + result.collab_params.cells = cells; + + Ok(result) } pub async fn v_did_update_row_meta(&self, row_id: &RowId, row_detail: &RowDetail) { @@ -144,31 +175,20 @@ impl DatabaseViewEditor { pub async fn v_did_create_row(&self, row_detail: &RowDetail, index: usize) { // Send the group notification if the current view has groups if let Some(controller) = self.group_controller.write().await.as_mut() { - let changesets = controller.did_create_row(row_detail, index); + let mut row_details = vec![Arc::new(row_detail.clone())]; + self.v_filter_rows(&mut row_details).await; - for changeset in changesets { - notify_did_update_group_rows(changeset).await; + if let Some(row_detail) = row_details.pop() { + let changesets = controller.did_create_row(&row_detail, index); + + for changeset in changesets { + notify_did_update_group_rows(changeset).await; + } } } - let inserted_row = InsertedRowPB { - row_meta: RowMetaPB::from(row_detail), - index: Some(index as i32), - is_new: true, - }; - let changes = RowsChangePB::from_insert(inserted_row); - send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) - .payload(changes) - .send(); self - .gen_did_create_row_view_tasks(row_detail.row.clone()) - .await; - } - - pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) { - self - .calculations_controller - .did_receive_row_changed(row_detail.row.clone()) + .gen_did_create_row_view_tasks(index, row_detail.clone()) .await; } @@ -222,34 +242,41 @@ impl DatabaseViewEditor { row_detail: &RowDetail, field_id: String, ) { - let result = self - .mut_group_controller(|group_controller, field| { - Ok(group_controller.did_update_group_row(old_row, row_detail, &field)) - }) - .await; + if let Some(controller) = self.group_controller.write().await.as_mut() { + let field = self.delegate.get_field(controller.get_grouping_field_id()); - if let Some(Ok(result)) = result { - let mut group_changes = GroupChangesPB { - view_id: self.view_id.clone(), - ..Default::default() - }; - if let Some(inserted_group) = result.inserted_group { - tracing::trace!("Create group after editing the row: {:?}", inserted_group); - group_changes.inserted_groups.push(inserted_group); - } - if let Some(delete_group) = result.deleted_group { - tracing::trace!("Delete group after editing the row: {:?}", delete_group); - group_changes.deleted_groups.push(delete_group.group_id); - } + if let Some(field) = field { + let mut row_details = vec![Arc::new(row_detail.clone())]; + self.v_filter_rows(&mut row_details).await; - if !group_changes.is_empty() { - notify_did_update_num_of_groups(&self.view_id, group_changes).await; - } + if let Some(row_detail) = row_details.pop() { + let result = controller.did_update_group_row(old_row, &row_detail, &field); - for changeset in result.row_changesets { - if !changeset.is_empty() { - tracing::trace!("Group change after editing the row: {:?}", changeset); - notify_did_update_group_rows(changeset).await; + if let Ok(result) = result { + let mut group_changes = GroupChangesPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + if let Some(inserted_group) = result.inserted_group { + tracing::trace!("Create group after editing the row: {:?}", inserted_group); + group_changes.inserted_groups.push(inserted_group); + } + if let Some(delete_group) = result.deleted_group { + tracing::trace!("Delete group after editing the row: {:?}", delete_group); + group_changes.deleted_groups.push(delete_group.group_id); + } + + if !group_changes.is_empty() { + notify_did_update_num_of_groups(&self.view_id, group_changes).await; + } + + for changeset in result.row_changesets { + if !changeset.is_empty() { + tracing::trace!("Group change after editing the row: {:?}", changeset); + notify_did_update_group_rows(changeset).await; + } + } + } } } } @@ -359,7 +386,7 @@ impl DatabaseViewEditor { pub async fn is_grouping_field(&self, field_id: &str) -> bool { match self.group_controller.read().await.as_ref() { - Some(group_controller) => group_controller.field_id() == field_id, + Some(group_controller) => group_controller.get_grouping_field_id() == field_id, None => false, } } @@ -368,7 +395,7 @@ impl DatabaseViewEditor { pub async fn v_initialize_new_group(&self, field_id: &str) -> FlowyResult<()> { let is_grouping_field = self.is_grouping_field(field_id).await; if !is_grouping_field { - self.v_grouping_by_field(field_id).await?; + self.v_group_by_field(field_id).await?; if let Some(view) = self.delegate.get_view(&self.view_id).await { let setting = database_view_setting_pb_from_view(view); @@ -382,7 +409,7 @@ impl DatabaseViewEditor { let mut old_field: Option = None; let result = if let Some(controller) = self.group_controller.write().await.as_mut() { let create_group_results = controller.create_group(name.to_string())?; - old_field = self.delegate.get_field(controller.field_id()); + old_field = self.delegate.get_field(controller.get_grouping_field_id()); create_group_results } else { (None, None) @@ -415,7 +442,7 @@ impl DatabaseViewEditor { None => return Ok(RowsChangePB::default()), }; - let old_field = self.delegate.get_field(controller.field_id()); + let old_field = self.delegate.get_field(controller.get_grouping_field_id()); let (row_ids, type_option_data) = controller.delete_group(group_id)?; drop(group_controller); @@ -444,22 +471,24 @@ impl DatabaseViewEditor { Ok(changes) } - pub async fn v_update_group(&self, changeset: GroupChangesets) -> FlowyResult<()> { - let mut type_option_data = TypeOptionData::new(); - let (old_field, updated_groups) = if let Some(controller) = - self.group_controller.write().await.as_mut() - { - let old_field = self.delegate.get_field(controller.field_id()); - let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset).await?; - type_option_data.extend(new_type_option); + pub async fn v_update_group(&self, changeset: Vec) -> FlowyResult<()> { + let mut type_option_data = None; + let (old_field, updated_groups) = + if let Some(controller) = self.group_controller.write().await.as_mut() { + let old_field = self.delegate.get_field(controller.get_grouping_field_id()); + let (updated_groups, new_type_option) = controller.apply_group_changeset(&changeset)?; - (old_field, updated_groups) - } else { - (None, vec![]) - }; + if new_type_option.is_some() { + type_option_data = new_type_option; + } + + (old_field, updated_groups) + } else { + (None, vec![]) + }; if let Some(old_field) = old_field { - if !type_option_data.is_empty() { + if let Some(type_option_data) = type_option_data { self .delegate .update_field(type_option_data, old_field) @@ -618,73 +647,33 @@ impl DatabaseViewEditor { Ok(()) } - pub async fn v_get_all_filters(&self) -> Vec> { + pub async fn v_get_all_filters(&self) -> Vec { self.delegate.get_all_filters(&self.view_id) } - #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn v_insert_filter(&self, params: UpdateFilterParams) -> FlowyResult<()> { - let is_exist = params.filter_id.is_some(); - let filter_id = match params.filter_id { - None => gen_database_filter_id(), - Some(filter_id) => filter_id, - }; - let filter = Filter { - id: filter_id.clone(), - field_id: params.field_id.clone(), - field_type: params.field_type, - condition: params.condition, - content: params.content, - }; - let filter_controller = self.filter_controller.clone(); - let changeset = if is_exist { - let old_filter = self.delegate.get_filter(&self.view_id, &filter.id); - - self.delegate.insert_filter(&self.view_id, filter.clone()); - filter_controller - .did_receive_changes(FilterChangeset::from_update(UpdatedFilter::new( - old_filter, filter, - ))) - .await - } else { - self.delegate.insert_filter(&self.view_id, filter.clone()); - filter_controller - .did_receive_changes(FilterChangeset::from_insert(filter)) - .await - }; - drop(filter_controller); - - if let Some(changeset) = changeset { - notify_did_update_filter(changeset).await; - } - Ok(()) - } - - #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn v_delete_filter(&self, params: DeleteFilterPayloadPB) -> FlowyResult<()> { - let filter_context = FilterContext { - filter_id: params.filter_id.clone(), - field_id: params.field_id.clone(), - field_type: params.field_type, - }; - let changeset = self - .filter_controller - .did_receive_changes(FilterChangeset::from_delete(filter_context.clone())) - .await; - - self - .delegate - .delete_filter(&self.view_id, ¶ms.filter_id); - if changeset.is_some() { - notify_did_update_filter(changeset.unwrap()).await; - } - Ok(()) - } - pub async fn v_get_filter(&self, filter_id: &str) -> Option { self.delegate.get_filter(&self.view_id, filter_id) } + #[tracing::instrument(level = "trace", skip(self), err)] + pub async fn v_modify_filters(&self, changeset: FilterChangeset) -> FlowyResult<()> { + let notification = self.filter_controller.apply_changeset(changeset).await; + + notify_did_update_filter(notification).await; + + let group_controller_read_guard = self.group_controller.read().await; + let grouping_field_id = group_controller_read_guard + .as_ref() + .map(|controller| controller.get_grouping_field_id().to_string()); + drop(group_controller_read_guard); + + if let Some(field_id) = grouping_field_id { + self.v_group_by_field(&field_id).await?; + } + + Ok(()) + } + /// Returns the current calendar settings #[tracing::instrument(level = "trace", skip(self))] pub async fn v_get_layout_settings(&self, layout_ty: &DatabaseLayout) -> LayoutSettingParams { @@ -769,6 +758,12 @@ impl DatabaseViewEditor { } pub async fn v_did_delete_field(&self, deleted_field_id: &str) { + let changeset = FilterChangeset::DeleteAllWithFieldId { + field_id: deleted_field_id.to_string(), + }; + let notification = self.filter_controller.apply_changeset(changeset).await; + notify_did_update_filter(notification).await; + let sorts = self.delegate.get_all_sorts(&self.view_id); if let Some(sort) = sorts.iter().find(|sort| sort.field_id == deleted_field_id) { @@ -809,11 +804,6 @@ impl DatabaseViewEditor { #[tracing::instrument(level = "trace", skip_all, err)] pub async fn v_did_update_field_type_option(&self, old_field: &Field) -> FlowyResult<()> { let field_id = &old_field.id; - // If the id of the grouping field is equal to the updated field's id, then we need to - // update the group setting - if self.is_grouping_field(field_id).await { - self.v_grouping_by_field(field_id).await?; - } if let Some(field) = self.delegate.get_field(field_id) { self @@ -823,68 +813,66 @@ impl DatabaseViewEditor { .did_update_field_type_option(&field) .await; - self - .mut_group_controller(|group_controller, _| { - group_controller.did_update_field_type_option(&field); - Ok(()) - }) - .await; - - if let Some(filter) = self - .delegate - .get_filter_by_field_id(&self.view_id, field_id) - { - let old = Filter { - field_type: FieldType::from(old_field.field_type), - ..filter.clone() + if old_field.field_type != field.field_type { + let changeset = FilterChangeset::DeleteAllWithFieldId { + field_id: field.id.clone(), }; - let updated_filter = UpdatedFilter::new(Some(old), filter); - let filter_changeset = FilterChangeset::from_update(updated_filter); - let filter_controller = self.filter_controller.clone(); - af_spawn(async move { - if let Some(notification) = filter_controller - .did_receive_changes(filter_changeset) - .await - { - notify_did_update_filter(notification).await; - } - }); + let notification = self.filter_controller.apply_changeset(changeset).await; + notify_did_update_filter(notification).await; } } + + // If the id of the grouping field is equal to the updated field's id, then we need to + // update the group setting + if self.is_grouping_field(field_id).await { + self.v_group_by_field(field_id).await?; + } + Ok(()) } /// Called when a grouping field is updated. #[tracing::instrument(level = "debug", skip_all, err)] - pub async fn v_grouping_by_field(&self, field_id: &str) -> FlowyResult<()> { + pub async fn v_group_by_field(&self, field_id: &str) -> FlowyResult<()> { if let Some(field) = self.delegate.get_field(field_id) { - let new_group_controller = new_group_controller_with_field( + tracing::trace!("create new group controller"); + + let new_group_controller = new_group_controller( self.view_id.clone(), self.delegate.clone(), - Arc::new(field), + self.filter_controller.clone(), + Some(field), ) .await?; - let new_groups = new_group_controller - .get_all_groups() - .into_iter() - .map(|group| GroupPB::from(group.clone())) - .collect(); + if let Some(controller) = &new_group_controller { + let new_groups = controller + .get_all_groups() + .into_iter() + .map(|group| GroupPB::from(group.clone())) + .collect(); - *self.group_controller.write().await = Some(new_group_controller); - let changeset = GroupChangesPB { - view_id: self.view_id.clone(), - initial_groups: new_groups, - ..Default::default() - }; + let changeset = GroupChangesPB { + view_id: self.view_id.clone(), + initial_groups: new_groups, + ..Default::default() + }; + tracing::trace!("notify did group by field1"); - debug_assert!(!changeset.is_empty()); - if !changeset.is_empty() { - send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField) - .payload(changeset) - .send(); + debug_assert!(!changeset.is_empty()); + if !changeset.is_empty() { + send_notification(&changeset.view_id, DatabaseNotification::DidGroupByField) + .payload(changeset) + .send(); + } } + tracing::trace!("notify did group by field2"); + + *self.group_controller.write().await = new_group_controller; + + tracing::trace!("did write group_controller to cache"); } + Ok(()) } @@ -1005,8 +993,13 @@ impl DatabaseViewEditor { } // initialize the group controller if the current layout support grouping - *self.group_controller.write().await = - new_group_controller(self.view_id.clone(), self.delegate.clone()).await?; + *self.group_controller.write().await = new_group_controller( + self.view_id.clone(), + self.delegate.clone(), + self.filter_controller.clone(), + None, + ) + .await?; let payload = DatabaseLayoutMetaPB { view_id: self.view_id.clone(), @@ -1066,7 +1059,7 @@ impl DatabaseViewEditor { .read() .await .as_ref() - .map(|group| group.field_id().to_owned())?; + .map(|controller| controller.get_grouping_field_id().to_owned())?; let field = self.delegate.get_field(&group_field_id)?; let mut write_guard = self.group_controller.write().await; if let Some(group_controller) = &mut *write_guard { @@ -1101,7 +1094,7 @@ impl DatabaseViewEditor { }); } - async fn gen_did_create_row_view_tasks(&self, row: Row) { + async fn gen_did_create_row_view_tasks(&self, preliminary_index: usize, row_detail: RowDetail) { let weak_sort_controller = Arc::downgrade(&self.sort_controller); let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); af_spawn(async move { @@ -1109,12 +1102,14 @@ impl DatabaseViewEditor { sort_controller .read() .await - .did_create_row(row.id.clone()) + .did_create_row(preliminary_index, &row_detail) .await; } if let Some(calculations_controller) = weak_calculations_controller.upgrade() { - calculations_controller.did_receive_row_changed(row).await; + calculations_controller + .did_receive_row_changed(row_detail.row.clone()) + .await; } }); } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs index 75f31212d9..f710144e60 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_filter.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use collab_database::fields::Field; use collab_database::rows::{RowDetail, RowId}; -use lib_infra::future::{to_fut, Fut}; +use lib_infra::future::Fut; use crate::services::cell::CellCache; use crate::services::database_view::{ @@ -17,7 +17,6 @@ pub async fn make_filter_controller( notifier: DatabaseViewChangedNotifier, cell_cache: CellCache, ) -> Arc { - let filters = delegate.get_all_filters(view_id); let task_scheduler = delegate.get_task_scheduler(); let filter_delegate = DatabaseViewFilterDelegateImpl(delegate.clone()); @@ -27,7 +26,6 @@ pub async fn make_filter_controller( &handler_id, filter_delegate, task_scheduler.clone(), - filters, cell_cache, notifier, ) @@ -46,16 +44,11 @@ pub async fn make_filter_controller( struct DatabaseViewFilterDelegateImpl(Arc); impl FilterDelegate for DatabaseViewFilterDelegateImpl { - fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>> { - let filter = self.0.get_filter(view_id, filter_id).map(Arc::new); - to_fut(async move { filter }) - } - fn get_field(&self, field_id: &str) -> Option { self.0.get_field(field_id) } - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut> { self.0.get_fields(view_id, field_ids) } @@ -66,4 +59,12 @@ impl FilterDelegate for DatabaseViewFilterDelegateImpl { fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>> { self.0.get_row(view_id, rows_id) } + + fn get_all_filters(&self, view_id: &str) -> Vec { + self.0.get_all_filters(view_id) + } + + fn save_filters(&self, view_id: &str, filters: &[Filter]) { + self.0.save_filters(view_id, filters) + } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs index 963e8a45dd..9f7e3da4ff 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_group.rs @@ -1,8 +1,7 @@ use std::sync::Arc; -use async_trait::async_trait; use collab_database::fields::Field; -use collab_database::rows::{Cell, RowId}; +use collab_database::rows::{RowDetail, RowId}; use flowy_error::FlowyResult; use lib_infra::future::{to_fut, Fut}; @@ -10,80 +9,61 @@ use lib_infra::future::{to_fut, Fut}; use crate::entities::FieldType; use crate::services::database_view::DatabaseViewOperation; use crate::services::field::RowSingleCellData; +use crate::services::filter::FilterController; use crate::services::group::{ - find_new_grouping_field, make_group_controller, GroupController, GroupSetting, - GroupSettingReader, GroupSettingWriter, GroupTypeOptionCellOperation, + make_group_controller, GroupContextDelegate, GroupController, GroupControllerDelegate, + GroupSetting, }; -pub async fn new_group_controller_with_field( - view_id: String, - delegate: Arc, - grouping_field: Arc, -) -> FlowyResult> { - let setting_reader = GroupSettingReaderImpl(delegate.clone()); - let rows = delegate.get_rows(&view_id).await; - let setting_writer = GroupSettingWriterImpl(delegate.clone()); - let type_option_writer = GroupTypeOptionCellWriterImpl(delegate.clone()); - make_group_controller( - view_id, - grouping_field, - rows, - setting_reader, - setting_writer, - type_option_writer, - ) - .await -} - pub async fn new_group_controller( view_id: String, delegate: Arc, + filter_controller: Arc, + grouping_field: Option, ) -> FlowyResult>> { - let fields = delegate.get_fields(&view_id, None).await; - let setting_reader = GroupSettingReaderImpl(delegate.clone()); - - // Read the grouping field or find a new grouping field - let mut grouping_field = setting_reader - .get_group_setting(&view_id) - .await - .and_then(|setting| { - fields - .iter() - .find(|field| field.id == setting.field_id) - .cloned() - }); - - let layout = delegate.get_layout_for_view(&view_id); - // If the view is a board and the grouping field is empty, we need to find a new grouping field - if layout.is_board() && grouping_field.is_none() { - grouping_field = find_new_grouping_field(&fields, &layout); + if !delegate.get_layout_for_view(&view_id).is_board() { + return Ok(None); } - if let Some(grouping_field) = grouping_field { - let rows = delegate.get_rows(&view_id).await; - let setting_writer = GroupSettingWriterImpl(delegate.clone()); - let type_option_writer = GroupTypeOptionCellWriterImpl(delegate.clone()); - Ok(Some( - make_group_controller( - view_id, - grouping_field, - rows, - setting_reader, - setting_writer, - type_option_writer, - ) - .await?, - )) - } else { - Ok(None) - } + let controller_delegate = GroupControllerDelegateImpl { + delegate: delegate.clone(), + filter_controller: filter_controller.clone(), + }; + + let grouping_field = match grouping_field { + Some(field) => Some(field), + None => { + let group_setting = controller_delegate.get_group_setting(&view_id).await; + + let fields = delegate.get_fields(&view_id, None).await; + + group_setting + .and_then(|setting| { + fields + .iter() + .find(|field| field.id == setting.field_id) + .cloned() + }) + .or_else(|| find_suitable_grouping_field(&fields)) + }, + }; + + let controller = match grouping_field { + Some(field) => Some(make_group_controller(&view_id, field, controller_delegate).await?), + None => None, + }; + + Ok(controller) } -pub(crate) struct GroupSettingReaderImpl(pub Arc); +pub(crate) struct GroupControllerDelegateImpl { + delegate: Arc, + filter_controller: Arc, +} -impl GroupSettingReader for GroupSettingReaderImpl { +impl GroupContextDelegate for GroupControllerDelegateImpl { fn get_group_setting(&self, view_id: &str) -> Fut>> { - let mut settings = self.0.get_group_setting(view_id); + let mut settings = self.delegate.get_group_setting(view_id); to_fut(async move { if settings.is_empty() { None @@ -96,9 +76,31 @@ impl GroupSettingReader for GroupSettingReaderImpl { fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut> { let field_id = field_id.to_owned(); let view_id = view_id.to_owned(); - let delegate = self.0.clone(); + let delegate = self.delegate.clone(); to_fut(async move { get_cells_for_field(delegate, &view_id, &field_id).await }) } + + fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut> { + self.delegate.insert_group_setting(view_id, group_setting); + to_fut(async move { Ok(()) }) + } +} + +impl GroupControllerDelegate for GroupControllerDelegateImpl { + fn get_field(&self, field_id: &str) -> Option { + self.delegate.get_field(field_id) + } + + fn get_all_rows(&self, view_id: &str) -> Fut>> { + let view_id = view_id.to_string(); + let delegate = self.delegate.clone(); + let filter_controller = self.filter_controller.clone(); + to_fut(async move { + let mut row_details = delegate.get_rows(&view_id).await; + filter_controller.filter_rows(&mut row_details).await; + row_details + }) + } } pub(crate) async fn get_cell_for_row( @@ -154,30 +156,14 @@ pub(crate) async fn get_cells_for_field( vec![] } -struct GroupSettingWriterImpl(Arc); -impl GroupSettingWriter for GroupSettingWriterImpl { - fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut> { - self.0.insert_group_setting(view_id, group_setting); - to_fut(async move { Ok(()) }) - } -} - -struct GroupTypeOptionCellWriterImpl(Arc); - -#[async_trait] -impl GroupTypeOptionCellOperation for GroupTypeOptionCellWriterImpl { - async fn get_cell(&self, _row_id: &RowId, _field_id: &str) -> FlowyResult> { - todo!() - } - - #[tracing::instrument(level = "trace", skip_all, err)] - async fn update_cell( - &self, - _view_id: &str, - _row_id: &RowId, - _field_id: &str, - _cell: Cell, - ) -> FlowyResult<()> { - todo!() +fn find_suitable_grouping_field(fields: &[Field]) -> Option { + let groupable_field = fields + .iter() + .find(|field| FieldType::from(field.field_type).can_be_group()); + + if let Some(field) = groupable_field { + Some(field.clone()) + } else { + fields.iter().find(|field| field.is_primary).cloned() } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs index 40ad9778b5..e64d9b494e 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_operation.rs @@ -27,7 +27,7 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { /// Get the view of the database with the view_id fn get_view(&self, view_id: &str) -> Fut>; /// If the field_ids is None, then it will return all the field revisions - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; /// Returns the field with the field_id fn get_field(&self, field_id: &str) -> Option; @@ -91,15 +91,15 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { fn remove_calculation(&self, view_id: &str, calculation_id: &str); - fn get_all_filters(&self, view_id: &str) -> Vec>; + fn get_all_filters(&self, view_id: &str) -> Vec; + + fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; fn delete_filter(&self, view_id: &str, filter_id: &str); fn insert_filter(&self, view_id: &str, filter: Filter); - fn get_filter(&self, view_id: &str, filter_id: &str) -> Option; - - fn get_filter_by_field_id(&self, view_id: &str, field_id: &str) -> Option; + fn save_filters(&self, view_id: &str, filters: &[Filter]); fn get_layout_setting(&self, view_id: &str, layout_ty: &DatabaseLayout) -> Option; diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs index 6587d9ea0e..0397526b66 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_sort.rs @@ -70,11 +70,21 @@ impl SortDelegate for DatabaseViewSortDelegateImpl { }) } + fn filter_row(&self, row_detail: &RowDetail) -> Fut { + let filter_controller = self.filter_controller.clone(); + let row_detail = row_detail.clone(); + to_fut(async move { + let mut row_details = vec![Arc::new(row_detail)]; + filter_controller.filter_rows(&mut row_details).await; + !row_details.is_empty() + }) + } + fn get_field(&self, field_id: &str) -> Option { self.delegate.get_field(field_id) } - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>> { + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut> { self.delegate.get_fields(view_id, field_ids) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs index 005cb48443..ca5697a6d9 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/views.rs @@ -2,17 +2,14 @@ use std::collections::HashMap; use std::sync::Arc; use collab_database::database::MutexDatabase; -use collab_database::rows::{RowDetail, RowId}; use nanoid::nanoid; use tokio::sync::{broadcast, RwLock}; use flowy_error::{FlowyError, FlowyResult}; -use lib_infra::future::Fut; use crate::services::cell::CellCache; use crate::services::database::DatabaseRowEvent; use crate::services::database_view::{DatabaseViewEditor, DatabaseViewOperation}; -use crate::services::group::RowChangeset; pub type RowEventSender = broadcast::Sender; pub type RowEventReceiver = broadcast::Receiver; @@ -23,7 +20,7 @@ pub struct DatabaseViews { database: Arc, cell_cache: CellCache, view_operation: Arc, - editor_by_view_id: Arc>, + view_editors: Arc>, } impl DatabaseViews { @@ -31,65 +28,38 @@ impl DatabaseViews { database: Arc, cell_cache: CellCache, view_operation: Arc, - editor_by_view_id: Arc>, + view_editors: Arc>, ) -> FlowyResult { Ok(Self { database, view_operation, cell_cache, - editor_by_view_id, + view_editors, }) } - pub async fn close_view(&self, view_id: &str) -> bool { - let mut editor_map = self.editor_by_view_id.write().await; - if let Some(view) = editor_map.remove(view_id) { + pub async fn close_view(&self, view_id: &str) { + let mut lock_guard = self.view_editors.write().await; + if let Some(view) = lock_guard.remove(view_id) { view.close().await; } - editor_map.is_empty() + } + + pub async fn num_editors(&self) -> usize { + self.view_editors.read().await.len() } pub async fn editors(&self) -> Vec> { - self - .editor_by_view_id - .read() - .await - .values() - .cloned() - .collect() - } - - /// It may generate a RowChangeset when the Row was moved from one group to another. - /// The return value, [RowChangeset], contains the changes made by the groups. - /// - pub async fn move_group_row( - &self, - view_id: &str, - row_detail: Arc, - to_group_id: String, - to_row_id: Option, - recv_row_changeset: impl FnOnce(RowChangeset) -> Fut<()>, - ) -> FlowyResult<()> { - let view_editor = self.get_view_editor(view_id).await?; - let mut row_changeset = RowChangeset::new(row_detail.row.id.clone()); - view_editor - .v_move_group_row(&row_detail, &mut row_changeset, &to_group_id, to_row_id) - .await; - - if !row_changeset.is_empty() { - recv_row_changeset(row_changeset).await; - } - - Ok(()) + self.view_editors.read().await.values().cloned().collect() } pub async fn get_view_editor(&self, view_id: &str) -> FlowyResult> { debug_assert!(!view_id.is_empty()); - if let Some(editor) = self.editor_by_view_id.read().await.get(view_id) { + if let Some(editor) = self.view_editors.read().await.get(view_id) { return Ok(editor.clone()); } - let mut editor_map = self.editor_by_view_id.try_write().map_err(|err| { + let mut editor_map = self.view_editors.try_write().map_err(|err| { FlowyError::internal().with_context(format!( "fail to acquire the lock of editor_by_view_id: {}", err diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs index 9a1e2812e1..e2aa56de94 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_filter.rs @@ -1,4 +1,8 @@ +use collab_database::{fields::Field, rows::Cell}; + use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB}; +use crate::services::cell::insert_checkbox_cell; +use crate::services::filter::PreFillCellsWithFilter; impl CheckboxFilterPB { pub fn is_visible(&self, cell_data: &CheckboxCellDataPB) -> bool { @@ -9,6 +13,20 @@ impl CheckboxFilterPB { } } +impl PreFillCellsWithFilter for CheckboxFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let is_checked = match self.condition { + CheckboxFilterConditionPB::IsChecked => Some(true), + CheckboxFilterConditionPB::IsUnChecked => None, + }; + + ( + is_checked.map(|is_checked| insert_checkbox_cell(is_checked, field)), + false, + ) + } +} + #[cfg(test)] mod tests { use crate::entities::{CheckboxCellDataPB, CheckboxFilterConditionPB, CheckboxFilterPB}; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs index 84773b5cd3..91768a5cf3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist_filter.rs @@ -1,5 +1,9 @@ +use collab_database::fields::Field; +use collab_database::rows::Cell; + use crate::entities::{ChecklistFilterConditionPB, ChecklistFilterPB}; use crate::services::field::SelectOption; +use crate::services::filter::PreFillCellsWithFilter; impl ChecklistFilterPB { pub fn is_visible( @@ -37,3 +41,9 @@ impl ChecklistFilterPB { } } } + +impl PreFillCellsWithFilter for ChecklistFilterPB { + fn get_compliant_cell(&self, _field: &Field) -> (Option, bool) { + (None, true) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs index 1eed418c86..42a0300e18 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_filter.rs @@ -1,8 +1,11 @@ use crate::entities::{DateFilterConditionPB, DateFilterPB}; +use crate::services::cell::insert_date_cell; +use crate::services::field::DateCellData; +use crate::services::filter::PreFillCellsWithFilter; -use chrono::{NaiveDate, NaiveDateTime}; - -use super::DateCellData; +use chrono::{Duration, NaiveDate, NaiveDateTime}; +use collab_database::fields::Field; +use collab_database::rows::Cell; impl DateFilterPB { /// Returns `None` if the DateFilterPB doesn't have the necessary data for @@ -95,6 +98,39 @@ impl DateFilterStrategy { } } +impl PreFillCellsWithFilter for DateFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let timestamp = match self.condition { + DateFilterConditionPB::DateIs + | DateFilterConditionPB::DateOnOrBefore + | DateFilterConditionPB::DateOnOrAfter => self.timestamp, + DateFilterConditionPB::DateBefore => self + .timestamp + .and_then(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0)) + .map(|date_time| { + let answer = date_time - Duration::days(1); + answer.timestamp() + }), + DateFilterConditionPB::DateAfter => self + .timestamp + .and_then(|timestamp| NaiveDateTime::from_timestamp_opt(timestamp, 0)) + .map(|date_time| { + let answer = date_time + Duration::days(1); + answer.timestamp() + }), + DateFilterConditionPB::DateWithIn => self.start, + _ => None, + }; + + let open_after_create = matches!(self.condition, DateFilterConditionPB::DateIsNotEmpty); + + ( + timestamp.map(|timestamp| insert_date_cell(timestamp, None, None, field)), + open_after_create, + ) + } +} + #[cfg(test)] mod tests { use crate::entities::{DateFilterConditionPB, DateFilterPB}; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs index 9832d05009..ba95dd8843 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_filter.rs @@ -1,23 +1,30 @@ -use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; -use crate::services::field::NumberCellFormat; -use rust_decimal::prelude::Zero; -use rust_decimal::Decimal; use std::str::FromStr; +use collab_database::fields::Field; +use collab_database::rows::Cell; +use rust_decimal::Decimal; + +use crate::entities::{NumberFilterConditionPB, NumberFilterPB}; +use crate::services::cell::insert_text_cell; +use crate::services::field::NumberCellFormat; +use crate::services::filter::PreFillCellsWithFilter; + impl NumberFilterPB { pub fn is_visible(&self, cell_data: &NumberCellFormat) -> Option { - let expected_decimal = Decimal::from_str(&self.content).unwrap_or_else(|_| Decimal::zero()); + let expected_decimal = || Decimal::from_str(&self.content).ok(); let strategy = match self.condition { - NumberFilterConditionPB::Equal => NumberFilterStrategy::Equal(expected_decimal), - NumberFilterConditionPB::NotEqual => NumberFilterStrategy::NotEqual(expected_decimal), - NumberFilterConditionPB::GreaterThan => NumberFilterStrategy::GreaterThan(expected_decimal), - NumberFilterConditionPB::LessThan => NumberFilterStrategy::LessThan(expected_decimal), + NumberFilterConditionPB::Equal => NumberFilterStrategy::Equal(expected_decimal()?), + NumberFilterConditionPB::NotEqual => NumberFilterStrategy::NotEqual(expected_decimal()?), + NumberFilterConditionPB::GreaterThan => { + NumberFilterStrategy::GreaterThan(expected_decimal()?) + }, + NumberFilterConditionPB::LessThan => NumberFilterStrategy::LessThan(expected_decimal()?), NumberFilterConditionPB::GreaterThanOrEqualTo => { - NumberFilterStrategy::GreaterThanOrEqualTo(expected_decimal) + NumberFilterStrategy::GreaterThanOrEqualTo(expected_decimal()?) }, NumberFilterConditionPB::LessThanOrEqualTo => { - NumberFilterStrategy::LessThanOrEqualTo(expected_decimal) + NumberFilterStrategy::LessThanOrEqualTo(expected_decimal()?) }, NumberFilterConditionPB::NumberIsEmpty => NumberFilterStrategy::Empty, NumberFilterConditionPB::NumberIsNotEmpty => NumberFilterStrategy::NotEmpty, @@ -27,6 +34,39 @@ impl NumberFilterPB { } } +impl PreFillCellsWithFilter for NumberFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let expected_decimal = || Decimal::from_str(&self.content).ok(); + + let text = match self.condition { + NumberFilterConditionPB::Equal + | NumberFilterConditionPB::GreaterThanOrEqualTo + | NumberFilterConditionPB::LessThanOrEqualTo + if !self.content.is_empty() => + { + Some(self.content.clone()) + }, + NumberFilterConditionPB::GreaterThan if !self.content.is_empty() => { + expected_decimal().map(|value| { + let answer = value + Decimal::from_f32_retain(1.0).unwrap(); + answer.to_string() + }) + }, + NumberFilterConditionPB::LessThan if !self.content.is_empty() => { + expected_decimal().map(|value| { + let answer = value - Decimal::from_f32_retain(1.0).unwrap(); + answer.to_string() + }) + }, + _ => None, + }; + + let open_after_create = matches!(self.condition, NumberFilterConditionPB::NumberIsNotEmpty); + + // use `insert_text_cell` because self.content might not be a parsable i64. + (text.map(|s| insert_text_cell(s, field)), open_after_create) + } +} enum NumberFilterStrategy { Equal(Decimal), NotEqual(Decimal), diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs index 572bfc5021..8ebd0d1db4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/multi_select_type_option.rs @@ -128,7 +128,7 @@ impl TypeOptionCellDataFilter for MultiSelectTypeOption { cell_data: &::CellData, ) -> bool { let selected_options = self.get_selected_options(cell_data.clone()).select_options; - filter.is_visible(&selected_options, FieldType::MultiSelect) + filter.is_visible(&selected_options).unwrap_or(true) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs index 148cef5f75..a0e1ce096b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_filter.rs @@ -1,106 +1,149 @@ -#![allow(clippy::needless_collect)] +use collab_database::fields::Field; +use collab_database::rows::Cell; -use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB}; -use crate::services::field::SelectOption; +use crate::entities::{SelectOptionFilterConditionPB, SelectOptionFilterPB}; +use crate::services::cell::insert_select_option_cell; +use crate::services::field::{select_type_option_from_field, SelectOption}; +use crate::services::filter::PreFillCellsWithFilter; impl SelectOptionFilterPB { - pub fn is_visible(&self, selected_options: &[SelectOption], field_type: FieldType) -> bool { - let selected_option_ids: Vec<&String> = - selected_options.iter().map(|option| &option.id).collect(); - match self.condition { - SelectOptionConditionPB::OptionIs => match field_type { - FieldType::SingleSelect => { - if self.option_ids.is_empty() { - return true; - } + pub fn is_visible(&self, selected_options: &[SelectOption]) -> Option { + let selected_option_ids = selected_options + .iter() + .map(|option| &option.id) + .collect::>(); - if selected_options.is_empty() { - return false; - } + let get_non_empty_expected_options = + || (!self.option_ids.is_empty()).then(|| self.option_ids.clone()); - let required_options = self - .option_ids - .iter() - .filter(|id| selected_option_ids.contains(id)) - .collect::>(); - - !required_options.is_empty() - }, - FieldType::MultiSelect => { - if self.option_ids.is_empty() { - return true; - } - - let required_options = self - .option_ids - .iter() - .filter(|id| selected_option_ids.contains(id)) - .collect::>(); - - !required_options.is_empty() - }, - _ => false, + let strategy = match self.condition { + SelectOptionFilterConditionPB::OptionIs => { + SelectOptionFilterStrategy::Is(get_non_empty_expected_options()?) }, - SelectOptionConditionPB::OptionIsNot => match field_type { - FieldType::SingleSelect => { - if self.option_ids.is_empty() { - return true; - } - - if selected_options.is_empty() { - return false; - } - - let required_options = self - .option_ids - .iter() - .filter(|id| selected_option_ids.contains(id)) - .collect::>(); - - required_options.is_empty() - }, - FieldType::MultiSelect => { - let required_options = self - .option_ids - .iter() - .filter(|id| selected_option_ids.contains(id)) - .collect::>(); - - required_options.is_empty() - }, - _ => false, + SelectOptionFilterConditionPB::OptionIsNot => { + SelectOptionFilterStrategy::IsNot(get_non_empty_expected_options()?) }, - SelectOptionConditionPB::OptionIsEmpty => selected_option_ids.is_empty(), - SelectOptionConditionPB::OptionIsNotEmpty => !selected_option_ids.is_empty(), + SelectOptionFilterConditionPB::OptionContains => { + SelectOptionFilterStrategy::Contains(get_non_empty_expected_options()?) + }, + SelectOptionFilterConditionPB::OptionDoesNotContain => { + SelectOptionFilterStrategy::DoesNotContain(get_non_empty_expected_options()?) + }, + SelectOptionFilterConditionPB::OptionIsEmpty => SelectOptionFilterStrategy::IsEmpty, + SelectOptionFilterConditionPB::OptionIsNotEmpty => SelectOptionFilterStrategy::IsNotEmpty, + }; + + Some(strategy.filter(&selected_option_ids)) + } +} + +enum SelectOptionFilterStrategy { + Is(Vec), + IsNot(Vec), + Contains(Vec), + DoesNotContain(Vec), + IsEmpty, + IsNotEmpty, +} + +impl SelectOptionFilterStrategy { + fn filter(self, selected_option_ids: &[&String]) -> bool { + match self { + SelectOptionFilterStrategy::Is(option_ids) => { + if selected_option_ids.is_empty() { + return false; + } + + selected_option_ids.len() == option_ids.len() + && selected_option_ids.iter().all(|id| option_ids.contains(id)) + }, + SelectOptionFilterStrategy::IsNot(option_ids) => { + if selected_option_ids.is_empty() { + return true; + } + + selected_option_ids.len() != option_ids.len() + || !selected_option_ids.iter().all(|id| option_ids.contains(id)) + }, + SelectOptionFilterStrategy::Contains(option_ids) => { + if selected_option_ids.is_empty() { + return false; + } + + let required_options = option_ids + .into_iter() + .filter(|id| selected_option_ids.contains(&id)) + .collect::>(); + + !required_options.is_empty() + }, + SelectOptionFilterStrategy::DoesNotContain(option_ids) => { + if selected_option_ids.is_empty() { + return true; + } + + let required_options = option_ids + .into_iter() + .filter(|id| selected_option_ids.contains(&id)) + .collect::>(); + + required_options.is_empty() + }, + SelectOptionFilterStrategy::IsEmpty => selected_option_ids.is_empty(), + SelectOptionFilterStrategy::IsNotEmpty => !selected_option_ids.is_empty(), } } } +impl PreFillCellsWithFilter for SelectOptionFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let get_non_empty_expected_options = || { + if !self.option_ids.is_empty() { + Some(self.option_ids.clone()) + } else { + None + } + }; + + let option_ids = match self.condition { + SelectOptionFilterConditionPB::OptionIs => get_non_empty_expected_options(), + SelectOptionFilterConditionPB::OptionContains => { + get_non_empty_expected_options().map(|mut options| vec![options.swap_remove(0)]) + }, + SelectOptionFilterConditionPB::OptionIsNotEmpty => select_type_option_from_field(field) + .ok() + .map(|mut type_option| { + let options = type_option.mut_options(); + if options.is_empty() { + vec![] + } else { + vec![options.swap_remove(0).id] + } + }), + _ => None, + }; + + ( + option_ids.map(|ids| insert_select_option_cell(ids, field)), + false, + ) + } +} #[cfg(test)] mod tests { - #![allow(clippy::all)] - use crate::entities::{FieldType, SelectOptionConditionPB, SelectOptionFilterPB}; + use crate::entities::{SelectOptionFilterConditionPB, SelectOptionFilterPB}; use crate::services::field::SelectOption; #[test] fn select_option_filter_is_empty_test() { let option = SelectOption::new("A"); let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIsEmpty, + condition: SelectOptionFilterConditionPB::OptionIsEmpty, option_ids: vec![], }; - assert_eq!(filter.is_visible(&vec![], FieldType::SingleSelect), true); - assert_eq!( - filter.is_visible(&vec![option.clone()], FieldType::SingleSelect), - false, - ); - - assert_eq!(filter.is_visible(&vec![], FieldType::MultiSelect), true); - assert_eq!( - filter.is_visible(&vec![option], FieldType::MultiSelect), - false, - ); + assert_eq!(filter.is_visible(&[]), Some(true)); + assert_eq!(filter.is_visible(&[option.clone()]), Some(false)); } #[test] @@ -108,157 +151,227 @@ mod tests { let option_1 = SelectOption::new("A"); let option_2 = SelectOption::new("B"); let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIsNotEmpty, + condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, option_ids: vec![option_1.id.clone(), option_2.id.clone()], }; - assert_eq!( - filter.is_visible(&vec![option_1.clone()], FieldType::SingleSelect), - true - ); - assert_eq!(filter.is_visible(&vec![], FieldType::SingleSelect), false,); - - assert_eq!( - filter.is_visible(&vec![option_1.clone()], FieldType::MultiSelect), - true - ); - assert_eq!(filter.is_visible(&vec![], FieldType::MultiSelect), false,); + assert_eq!(filter.is_visible(&[]), Some(false)); + assert_eq!(filter.is_visible(&[option_1.clone()]), Some(true)); } #[test] - fn single_select_option_filter_is_not_test() { + fn select_option_filter_is_test() { let option_1 = SelectOption::new("A"); let option_2 = SelectOption::new("B"); let option_3 = SelectOption::new("C"); + + // no expected options let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIsNot, - option_ids: vec![option_1.id.clone(), option_2.id.clone()], + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![], }; - - for (options, is_visible) in vec![ - (vec![option_2.clone()], false), - (vec![option_1.clone()], false), - (vec![option_3.clone()], true), - (vec![option_1.clone(), option_2.clone()], false), + for (options, is_visible) in [ + (vec![], None), + (vec![option_1.clone()], None), + (vec![option_1.clone(), option_2.clone()], None), ] { - assert_eq!( - filter.is_visible(&options, FieldType::SingleSelect), - is_visible - ); + assert_eq!(filter.is_visible(&options), is_visible); } - } - - #[test] - fn single_select_option_filter_is_test() { - let option_1 = SelectOption::new("A"); - let option_2 = SelectOption::new("B"); - let option_3 = SelectOption::new("c"); + // one expected option let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIs, + condition: SelectOptionFilterConditionPB::OptionIs, option_ids: vec![option_1.id.clone()], }; - for (options, is_visible) in vec![ - (vec![option_1.clone()], true), - (vec![option_2.clone()], false), - (vec![option_3.clone()], false), - (vec![option_1.clone(), option_2.clone()], true), + for (options, is_visible) in [ + (vec![], Some(false)), + (vec![option_1.clone()], Some(true)), + (vec![option_2.clone()], Some(false)), + (vec![option_3.clone()], Some(false)), + (vec![option_1.clone(), option_2.clone()], Some(false)), ] { - assert_eq!( - filter.is_visible(&options, FieldType::SingleSelect), - is_visible - ); + assert_eq!(filter.is_visible(&options), is_visible); } - } - - #[test] - fn single_select_option_filter_is_test2() { - let option_1 = SelectOption::new("A"); - let option_2 = SelectOption::new("B"); + // multiple expected options let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIs, - option_ids: vec![], - }; - for (options, is_visible) in vec![ - (vec![option_1.clone()], true), - (vec![option_2.clone()], true), - (vec![option_1.clone(), option_2.clone()], true), - ] { - assert_eq!( - filter.is_visible(&options, FieldType::SingleSelect), - is_visible - ); - } - } - - #[test] - fn multi_select_option_filter_not_contains_test() { - let option_1 = SelectOption::new("A"); - let option_2 = SelectOption::new("B"); - let option_3 = SelectOption::new("C"); - let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIsNot, + condition: SelectOptionFilterConditionPB::OptionIs, option_ids: vec![option_1.id.clone(), option_2.id.clone()], }; - - for (options, is_visible) in vec![ - (vec![option_1.clone(), option_2.clone()], false), - (vec![option_1.clone()], false), - (vec![option_2.clone()], false), - (vec![option_3.clone()], true), + for (options, is_visible) in [ + (vec![], Some(false)), + (vec![option_1.clone()], Some(false)), + (vec![option_1.clone(), option_2.clone()], Some(true)), ( vec![option_1.clone(), option_2.clone(), option_3.clone()], - false, + Some(false), ), - (vec![], true), ] { - assert_eq!( - filter.is_visible(&options, FieldType::MultiSelect), - is_visible - ); + assert_eq!(filter.is_visible(&options), is_visible); } } + #[test] - fn multi_select_option_filter_contains_test() { + fn select_option_filter_is_not_test() { let option_1 = SelectOption::new("A"); let option_2 = SelectOption::new("B"); let option_3 = SelectOption::new("C"); + // no expected options let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIs, + condition: SelectOptionFilterConditionPB::OptionIsNot, + option_ids: vec![], + }; + for (options, is_visible) in [ + (vec![], None), + (vec![option_1.clone()], None), + (vec![option_1.clone(), option_2.clone()], None), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + + // one expected option + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsNot, + option_ids: vec![option_1.id.clone()], + }; + for (options, is_visible) in [ + (vec![], Some(true)), + (vec![option_1.clone()], Some(false)), + (vec![option_2.clone()], Some(true)), + (vec![option_3.clone()], Some(true)), + (vec![option_1.clone(), option_2.clone()], Some(true)), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + + // multiple expected options + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsNot, option_ids: vec![option_1.id.clone(), option_2.id.clone()], }; - for (options, is_visible) in vec![ + for (options, is_visible) in [ + (vec![], Some(true)), + (vec![option_1.clone()], Some(true)), + (vec![option_1.clone(), option_2.clone()], Some(false)), ( vec![option_1.clone(), option_2.clone(), option_3.clone()], - true, + Some(true), ), - (vec![option_2.clone(), option_1.clone()], true), - (vec![option_2.clone()], true), - (vec![option_1.clone(), option_3.clone()], true), - (vec![option_3.clone()], false), ] { - assert_eq!( - filter.is_visible(&options, FieldType::MultiSelect), - is_visible - ); + assert_eq!(filter.is_visible(&options), is_visible); } } #[test] - fn multi_select_option_filter_contains_test2() { + fn select_option_filter_contains_test() { let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let option_3 = SelectOption::new("C"); + let option_4 = SelectOption::new("D"); + // no expected options let filter = SelectOptionFilterPB { - condition: SelectOptionConditionPB::OptionIs, + condition: SelectOptionFilterConditionPB::OptionContains, option_ids: vec![], }; - for (options, is_visible) in vec![(vec![option_1.clone()], true), (vec![], true)] { - assert_eq!( - filter.is_visible(&options, FieldType::MultiSelect), - is_visible - ); + for (options, is_visible) in [ + (vec![], None), + (vec![option_1.clone()], None), + (vec![option_1.clone(), option_2.clone()], None), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + + // one expected option + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: vec![option_1.id.clone()], + }; + for (options, is_visible) in [ + (vec![], Some(false)), + (vec![option_1.clone()], Some(true)), + (vec![option_2.clone()], Some(false)), + (vec![option_1.clone(), option_2.clone()], Some(true)), + (vec![option_3.clone(), option_4.clone()], Some(false)), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + + // multiple expected options + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: vec![option_1.id.clone(), option_2.id.clone()], + }; + for (options, is_visible) in [ + (vec![], Some(false)), + (vec![option_1.clone()], Some(true)), + (vec![option_3.clone()], Some(false)), + (vec![option_1.clone(), option_2.clone()], Some(true)), + (vec![option_1.clone(), option_3.clone()], Some(true)), + (vec![option_3.clone(), option_4.clone()], Some(false)), + ( + vec![option_1.clone(), option_3.clone(), option_4.clone()], + Some(true), + ), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + } + + #[test] + fn select_option_filter_does_not_contain_test() { + let option_1 = SelectOption::new("A"); + let option_2 = SelectOption::new("B"); + let option_3 = SelectOption::new("C"); + let option_4 = SelectOption::new("D"); + + // no expected options + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionDoesNotContain, + option_ids: vec![], + }; + for (options, is_visible) in [ + (vec![], None), + (vec![option_1.clone()], None), + (vec![option_1.clone(), option_2.clone()], None), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + + // one expected option + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionDoesNotContain, + option_ids: vec![option_1.id.clone()], + }; + for (options, is_visible) in [ + (vec![], Some(true)), + (vec![option_1.clone()], Some(false)), + (vec![option_2.clone()], Some(true)), + (vec![option_1.clone(), option_2.clone()], Some(false)), + (vec![option_3.clone(), option_4.clone()], Some(true)), + ] { + assert_eq!(filter.is_visible(&options), is_visible); + } + + // multiple expected options + let filter = SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionDoesNotContain, + option_ids: vec![option_1.id.clone(), option_2.id.clone()], + }; + for (options, is_visible) in [ + (vec![], Some(true)), + (vec![option_1.clone()], Some(false)), + (vec![option_3.clone()], Some(true)), + (vec![option_1.clone(), option_2.clone()], Some(false)), + (vec![option_1.clone(), option_3.clone()], Some(false)), + (vec![option_3.clone(), option_4.clone()], Some(true)), + ( + vec![option_1.clone(), option_3.clone(), option_4.clone()], + Some(false), + ), + ] { + assert_eq!(filter.is_visible(&options), is_visible); } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs index 2925dc04ef..fa0745133b 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/single_select_type_option.rs @@ -119,7 +119,7 @@ impl TypeOptionCellDataFilter for SingleSelectTypeOption { cell_data: &::CellData, ) -> bool { let selected_options = self.get_selected_options(cell_data.clone()).select_options; - filter.is_visible(&selected_options, FieldType::SingleSelect) + filter.is_visible(&selected_options).unwrap_or(true) } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs index f684dcc56b..8f090f5802 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_filter.rs @@ -1,22 +1,46 @@ +use collab_database::{fields::Field, rows::Cell}; + use crate::entities::{TextFilterConditionPB, TextFilterPB}; +use crate::services::cell::insert_text_cell; +use crate::services::filter::PreFillCellsWithFilter; impl TextFilterPB { pub fn is_visible>(&self, cell_data: T) -> bool { let cell_data = cell_data.as_ref().to_lowercase(); let content = &self.content.to_lowercase(); match self.condition { - TextFilterConditionPB::Is => &cell_data == content, - TextFilterConditionPB::IsNot => &cell_data != content, - TextFilterConditionPB::Contains => cell_data.contains(content), - TextFilterConditionPB::DoesNotContain => !cell_data.contains(content), - TextFilterConditionPB::StartsWith => cell_data.starts_with(content), - TextFilterConditionPB::EndsWith => cell_data.ends_with(content), + TextFilterConditionPB::TextIs => &cell_data == content, + TextFilterConditionPB::TextIsNot => &cell_data != content, + TextFilterConditionPB::TextContains => cell_data.contains(content), + TextFilterConditionPB::TextDoesNotContain => !cell_data.contains(content), + TextFilterConditionPB::TextStartsWith => cell_data.starts_with(content), + TextFilterConditionPB::TextEndsWith => cell_data.ends_with(content), TextFilterConditionPB::TextIsEmpty => cell_data.is_empty(), TextFilterConditionPB::TextIsNotEmpty => !cell_data.is_empty(), } } } +impl PreFillCellsWithFilter for TextFilterPB { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool) { + let text = match self.condition { + TextFilterConditionPB::TextIs + | TextFilterConditionPB::TextContains + | TextFilterConditionPB::TextStartsWith + | TextFilterConditionPB::TextEndsWith + if !self.content.is_empty() => + { + Some(self.content.clone()) + }, + _ => None, + }; + + let open_after_create = matches!(self.condition, TextFilterConditionPB::TextIsNotEmpty); + + (text.map(|s| insert_text_cell(s, field)), open_after_create) + } +} + #[cfg(test)] mod tests { #![allow(clippy::all)] @@ -25,7 +49,7 @@ mod tests { #[test] fn text_filter_equal_test() { let text_filter = TextFilterPB { - condition: TextFilterConditionPB::Is, + condition: TextFilterConditionPB::TextIs, content: "appflowy".to_owned(), }; @@ -37,7 +61,7 @@ mod tests { #[test] fn text_filter_start_with_test() { let text_filter = TextFilterPB { - condition: TextFilterConditionPB::StartsWith, + condition: TextFilterConditionPB::TextStartsWith, content: "appflowy".to_owned(), }; @@ -49,7 +73,7 @@ mod tests { #[test] fn text_filter_end_with_test() { let text_filter = TextFilterPB { - condition: TextFilterConditionPB::EndsWith, + condition: TextFilterConditionPB::TextEndsWith, content: "appflowy".to_owned(), }; @@ -70,7 +94,7 @@ mod tests { #[test] fn text_filter_contain_test() { let text_filter = TextFilterPB { - condition: TextFilterConditionPB::Contains, + condition: TextFilterConditionPB::TextContains, content: "appflowy".to_owned(), }; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs index 01eb06835f..f3def99718 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/timestamp_type_option/timestamp_type_option.rs @@ -177,7 +177,7 @@ impl TypeOptionCellDataFilter for TimestampTypeOption { _filter: &::CellFilter, _cell_data: &::CellData, ) -> bool { - false + true } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs index 172b192728..0cb3d1ca65 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option.rs @@ -19,10 +19,10 @@ use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, RichTextTypeOption, SingleSelectTypeOption, TimestampTypeOption, URLTypeOption, }; -use crate::services::filter::FromFilterString; +use crate::services::filter::{ParseFilterData, PreFillCellsWithFilter}; use crate::services::sort::SortCondition; -pub trait TypeOption { +pub trait TypeOption: From + Into { /// `CellData` represents the decoded model for the current type option. Each of them must /// implement the From<&Cell> trait. If the `Cell` cannot be decoded into this type, the default /// value will be returned. @@ -58,7 +58,7 @@ pub trait TypeOption { type CellProtobufType: TryInto + Debug; /// Represents the filter configuration for this type option. - type CellFilter: FromFilterString + Send + Sync + 'static; + type CellFilter: ParseFilterData + PreFillCellsWithFilter + Clone + Send + Sync + 'static; } /// This trait providing serialization and deserialization methods for cell data. /// diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs index 5492d92194..0c2b8a73da 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/type_option_cell.rs @@ -9,9 +9,7 @@ use flowy_error::FlowyResult; use lib_infra::box_any::BoxAny; use crate::entities::FieldType; -use crate::services::cell::{ - CellCache, CellDataChangeset, CellDataDecoder, CellFilterCache, CellProtobufBlob, -}; +use crate::services::cell::{CellCache, CellDataChangeset, CellDataDecoder, CellProtobufBlob}; use crate::services::field::checklist_type_option::ChecklistTypeOption; use crate::services::field::{ CheckboxTypeOption, DateTypeOption, MultiSelectTypeOption, NumberTypeOption, RelationTypeOption, @@ -55,7 +53,7 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static { sort_condition: SortCondition, ) -> Ordering; - fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool; + fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool; /// Format the cell to string using the passed-in [FieldType] and [Field]. /// The [Cell] is generic, so we need to know the [FieldType] and [Field] to format the cell. @@ -100,7 +98,6 @@ impl AsRef for CellDataCacheKey { struct TypeOptionCellDataHandlerImpl { inner: T, cell_data_cache: Option, - cell_filter_cache: Option, } impl TypeOptionCellDataHandlerImpl @@ -122,13 +119,11 @@ where pub fn new_with_boxed( inner: T, - cell_filter_cache: Option, cell_data_cache: Option, ) -> Box { Self { inner, cell_data_cache, - cell_filter_cache, } .into_boxed() } @@ -143,7 +138,7 @@ where cell: &Cell, decoded_field_type: &FieldType, field: &Field, - ) -> FlowyResult<::CellData> { + ) -> FlowyResult { let key = CellDataCacheKey::new(field, *decoded_field_type, cell); if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { let read_guard = cell_data_cache.read(); @@ -173,12 +168,7 @@ where Ok(cell_data) } - fn set_decoded_cell_data( - &self, - cell: &Cell, - cell_data: ::CellData, - field: &Field, - ) { + fn set_decoded_cell_data(&self, cell: &Cell, cell_data: T::CellData, field: &Field) { if let Some(cell_data_cache) = self.cell_data_cache.as_ref() { let field_type = FieldType::from(field.field_type); let key = CellDataCacheKey::new(field, field_type, cell); @@ -201,16 +191,6 @@ impl std::ops::Deref for TypeOptionCellDataHandlerImpl { } } -impl TypeOption for TypeOptionCellDataHandlerImpl -where - T: TypeOption + Send + Sync, -{ - type CellData = T::CellData; - type CellChangeset = T::CellChangeset; - type CellProtobufType = T::CellProtobufType; - type CellFilter = T::CellFilter; -} - impl TypeOptionCellDataHandler for TypeOptionCellDataHandlerImpl where T: TypeOption @@ -232,7 +212,7 @@ where ) -> FlowyResult { let cell_data = self .get_cell_data(cell, decoded_field_type, field_rev)? - .unbox_or_default::<::CellData>(); + .unbox_or_default::(); CellProtobufBlob::from(self.protobuf_encode(cell_data)) } @@ -243,7 +223,7 @@ where old_cell: Option, field: &Field, ) -> FlowyResult { - let changeset = cell_changeset.unbox_or_error::<::CellChangeset>()?; + let changeset = cell_changeset.unbox_or_error::()?; let (cell, cell_data) = self.apply_changeset(changeset, old_cell)?; self.set_decoded_cell_data(&cell, cell_data, field); Ok(cell) @@ -308,11 +288,11 @@ where } } - fn handle_cell_filter(&self, field_type: &FieldType, field: &Field, cell: &Cell) -> bool { + fn handle_cell_filter(&self, field: &Field, cell: &Cell, filter: &BoxAny) -> bool { let perform_filter = || { - let filter_cache = self.cell_filter_cache.as_ref()?.read(); - let cell_filter = filter_cache.get::<::CellFilter>(&field.id)?; - let cell_data = self.get_decoded_cell_data(cell, field_type, field).ok()?; + let field_type = FieldType::from(field.field_type); + let cell_filter = filter.downcast_ref::()?; + let cell_data = self.get_decoded_cell_data(cell, &field_type, field).ok()?; Some(self.apply_filter(cell_filter, &cell_data)) }; @@ -362,28 +342,16 @@ where pub struct TypeOptionCellExt<'a> { field: &'a Field, cell_data_cache: Option, - cell_filter_cache: Option, } impl<'a> TypeOptionCellExt<'a> { - pub fn new_with_cell_data_cache(field: &'a Field, cell_data_cache: Option) -> Self { + pub fn new(field: &'a Field, cell_data_cache: Option) -> Self { Self { field, cell_data_cache, - cell_filter_cache: None, } } - pub fn new( - field: &'a Field, - cell_data_cache: Option, - cell_filter_cache: Option, - ) -> Self { - let mut this = Self::new_with_cell_data_cache(field, cell_data_cache); - this.cell_filter_cache = cell_filter_cache; - this - } - pub fn get_cells(&self) -> Vec { let field_type = FieldType::from(self.field.field_type); match self.get_type_option_cell_data_handler(&field_type) { @@ -403,103 +371,63 @@ impl<'a> TypeOptionCellExt<'a> { .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::Number => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::DateTime => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::LastEditedTime | FieldType::CreatedTime => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::SingleSelect => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::MultiSelect => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::Checkbox => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::URL => { self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }) }, FieldType::Checklist => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), FieldType::Relation => self .field .get_type_option::(field_type) .map(|type_option| { - TypeOptionCellDataHandlerImpl::new_with_boxed( - type_option, - self.cell_filter_cache.clone(), - self.cell_data_cache.clone(), - ) + TypeOptionCellDataHandlerImpl::new_with_boxed(type_option, self.cell_data_cache.clone()) }), } } diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs index facc4bfd2e..e378990146 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option_entities.rs @@ -71,7 +71,7 @@ impl From for URLCellData { impl AsRef for URLCellData { fn as_ref(&self) -> &str { - &self.url + &self.data } } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs index 122b714ee6..6f700ca7c0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -2,8 +2,9 @@ use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; +use collab_database::database::gen_database_filter_id; use collab_database::fields::Field; -use collab_database::rows::{Cell, Row, RowDetail, RowId}; +use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; use dashmap::DashMap; use serde::{Deserialize, Serialize}; use tokio::sync::RwLock; @@ -14,33 +15,31 @@ use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatch use crate::entities::filter_entities::*; use crate::entities::{FieldType, InsertedRowPB, RowMetaPB}; -use crate::services::cell::{CellCache, CellFilterCache}; +use crate::services::cell::CellCache; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; -use crate::services::field::*; -use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification}; -use crate::utils::cache::AnyTypeCache; +use crate::services::field::TypeOptionCellExt; +use crate::services::filter::{Filter, FilterChangeset, FilterInner, FilterResultNotification}; pub trait FilterDelegate: Send + Sync + 'static { - fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>>; fn get_field(&self, field_id: &str) -> Option; - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; fn get_rows(&self, view_id: &str) -> Fut>>; fn get_row(&self, view_id: &str, rows_id: &RowId) -> Fut)>>; + fn get_all_filters(&self, view_id: &str) -> Vec; + fn save_filters(&self, view_id: &str, filters: &[Filter]); } -pub trait FromFilterString { - fn from_filter(filter: &Filter) -> Self - where - Self: Sized; +pub trait PreFillCellsWithFilter { + fn get_compliant_cell(&self, field: &Field) -> (Option, bool); } pub struct FilterController { view_id: String, handler_id: String, delegate: Box, - result_by_row_id: DashMap, + result_by_row_id: DashMap, cell_cache: CellCache, - cell_filter_cache: CellFilterCache, + filters: RwLock>, task_scheduler: Arc>, notifier: DatabaseViewChangedNotifier, } @@ -57,26 +56,56 @@ impl FilterController { handler_id: &str, delegate: T, task_scheduler: Arc>, - filters: Vec>, cell_cache: CellCache, notifier: DatabaseViewChangedNotifier, ) -> Self where T: FilterDelegate + 'static, { - let this = Self { + // ensure every filter is valid + let field_ids = delegate + .get_fields(view_id, None) + .await + .into_iter() + .map(|field| field.id) + .collect::>(); + + let mut need_save = false; + + let mut filters = delegate.get_all_filters(view_id); + let mut filtering_field_ids: HashMap> = HashMap::new(); + + for filter in filters.iter() { + filter.get_all_filtering_field_ids(&mut filtering_field_ids); + } + + let mut delete_filter_ids = vec![]; + + for (field_id, filter_ids) in &filtering_field_ids { + if !field_ids.contains(field_id) { + need_save = true; + delete_filter_ids.extend(filter_ids); + } + } + + for filter_id in delete_filter_ids { + Self::delete_filter(&mut filters, filter_id); + } + + if need_save { + delegate.save_filters(view_id, &filters); + } + + Self { view_id: view_id.to_string(), handler_id: handler_id.to_string(), delegate: Box::new(delegate), result_by_row_id: DashMap::default(), cell_cache, - // Cache by field_id - cell_filter_cache: AnyTypeCache::::new(), + filters: RwLock::new(filters), task_scheduler, notifier, - }; - this.refresh_filters(filters).await; - this + } } pub async fn close(&self) { @@ -100,7 +129,9 @@ impl FilterController { } pub async fn filter_rows(&self, rows: &mut Vec>) { - if self.cell_filter_cache.read().is_empty() { + let filters = self.filters.read().await; + + if filters.is_empty() { return; } let field_by_field_id = self.get_field_map().await; @@ -110,7 +141,7 @@ impl FilterController { &self.result_by_row_id, &field_by_field_id, &self.cell_cache, - &self.cell_filter_cache, + &filters, ); }); @@ -118,19 +149,175 @@ impl FilterController { self .result_by_row_id .get(&row_detail.row.id) - .map(|result| result.is_visible()) + .map(|result| *result) .unwrap_or(false) }); } - async fn get_field_map(&self) -> HashMap> { + pub async fn did_receive_row_changed(&self, row_id: RowId) { + if !self.filters.read().await.is_empty() { + self + .gen_task( + FilterEvent::RowDidChanged(row_id), + QualityOfService::UserInteractive, + ) + .await + } + } + + #[tracing::instrument(level = "trace", skip(self))] + pub async fn apply_changeset(&self, changeset: FilterChangeset) -> FilterChangesetNotificationPB { + let mut filters = self.filters.write().await; + + match changeset { + FilterChangeset::Insert { + parent_filter_id, + data, + } => { + let new_filter = Filter { + id: gen_database_filter_id(), + inner: data, + }; + match parent_filter_id { + Some(parent_filter_id) => { + if let Some(parent_filter) = filters + .iter_mut() + .find_map(|filter| filter.find_filter(&parent_filter_id)) + { + // TODO(RS): error handling for inserting filters + let _result = parent_filter.insert_filter(new_filter); + } + }, + None => { + filters.push(new_filter); + }, + } + }, + FilterChangeset::UpdateType { + filter_id, + filter_type, + } => { + for filter in filters.iter_mut() { + let filter = filter.find_filter(&filter_id); + if let Some(filter) = filter { + let result = filter.convert_to_and_or_filter_type(filter_type); + if result.is_ok() { + break; + } + } + } + }, + FilterChangeset::UpdateData { filter_id, data } => { + if let Some(filter) = filters + .iter_mut() + .find_map(|filter| filter.find_filter(&filter_id)) + { + // TODO(RS): error handling for updating filter data + let _result = filter.update_filter_data(data); + } + }, + FilterChangeset::Delete { + filter_id, + field_id: _, + } => Self::delete_filter(&mut filters, &filter_id), + FilterChangeset::DeleteAllWithFieldId { field_id } => { + let mut filter_ids = vec![]; + for filter in filters.iter() { + filter.find_all_filters_with_field_id(&field_id, &mut filter_ids); + } + for filter_id in filter_ids { + Self::delete_filter(&mut filters, &filter_id) + } + }, + } + + self.delegate.save_filters(&self.view_id, &filters); + self - .delegate - .get_fields(&self.view_id, None) - .await - .into_iter() - .map(|field| (field.id.clone(), field)) - .collect::>>() + .gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background) + .await; + + FilterChangesetNotificationPB::from_filters(&self.view_id, &filters) + } + + pub async fn fill_cells(&self, cells: &mut Cells) -> bool { + let filters = self.filters.read().await; + + let mut open_after_create = false; + + let mut min_required_filters: Vec<&FilterInner> = vec![]; + for filter in filters.iter() { + filter.get_min_effective_filters(&mut min_required_filters); + } + + let field_map = self.get_field_map().await; + + while let Some(current_inner) = min_required_filters.pop() { + if let FilterInner::Data { + field_id, + field_type, + condition_and_content, + } = ¤t_inner + { + if min_required_filters.iter().any( + |inner| matches!(inner, FilterInner::Data { field_id: other_id, .. } if other_id == field_id), + ) { + min_required_filters.retain( + |inner| matches!(inner, FilterInner::Data { field_id: other_id, .. } if other_id != field_id), + ); + open_after_create = true; + continue; + } + + if let Some(field) = field_map.get(field_id) { + let (cell, flag) = match field_type { + FieldType::RichText | FieldType::URL => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::Number => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::DateTime => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::SingleSelect => { + let filter = condition_and_content + .cloned::() + .unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::MultiSelect => { + let filter = condition_and_content + .cloned::() + .unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::Checkbox => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + FieldType::Checklist => { + let filter = condition_and_content.cloned::().unwrap(); + filter.get_compliant_cell(field) + }, + _ => (None, false), + }; + + if let Some(cell) = cell { + cells.insert(field_id.clone(), cell); + } + + if flag { + open_after_create = flag; + } + } + } + } + + open_after_create } #[tracing::instrument( @@ -143,22 +330,24 @@ impl FilterController { pub async fn process(&self, predicate: &str) -> FlowyResult<()> { let event_type = FilterEvent::from_str(predicate).unwrap(); match event_type { - FilterEvent::FilterDidChanged => self.filter_all_rows().await?, - FilterEvent::RowDidChanged(row_id) => self.filter_row(row_id).await?, + FilterEvent::FilterDidChanged => self.filter_all_rows_handler().await?, + FilterEvent::RowDidChanged(row_id) => self.filter_single_row_handler(row_id).await?, } Ok(()) } - async fn filter_row(&self, row_id: RowId) -> FlowyResult<()> { + async fn filter_single_row_handler(&self, row_id: RowId) -> FlowyResult<()> { + let filters = self.filters.read().await; + if let Some((_, row_detail)) = self.delegate.get_row(&self.view_id, &row_id).await { let field_by_field_id = self.get_field_map().await; let mut notification = FilterResultNotification::new(self.view_id.clone()); - if let Some((row_id, is_visible)) = filter_row( + if let Some(is_visible) = filter_row( &row_detail.row, &self.result_by_row_id, &field_by_field_id, &self.cell_cache, - &self.cell_filter_cache, + &filters, ) { if is_visible { if let Some((index, _row)) = self.delegate.get_row(&self.view_id, &row_id).await { @@ -178,7 +367,9 @@ impl FilterController { Ok(()) } - async fn filter_all_rows(&self) -> FlowyResult<()> { + async fn filter_all_rows_handler(&self) -> FlowyResult<()> { + let filters = self.filters.read().await; + let field_by_field_id = self.get_field_map().await; let mut visible_rows = vec![]; let mut invisible_rows = vec![]; @@ -190,18 +381,18 @@ impl FilterController { .into_iter() .enumerate() { - if let Some((row_id, is_visible)) = filter_row( + if let Some(is_visible) = filter_row( &row_detail.row, &self.result_by_row_id, &field_by_field_id, &self.cell_cache, - &self.cell_filter_cache, + &filters, ) { if is_visible { let row_meta = RowMetaPB::from(row_detail.as_ref()); visible_rows.push(InsertedRowPB::new(row_meta).with_index(index as i32)) } else { - invisible_rows.push(row_id); + invisible_rows.push(row_detail.row.id.clone()); } } } @@ -211,225 +402,142 @@ impl FilterController { invisible_rows, visible_rows, }; - tracing::Span::current().record("filter_result", format!("{:?}", ¬ification).as_str()); + tracing::trace!("filter result {:?}", filters); let _ = self .notifier .send(DatabaseViewChanged::FilterNotification(notification)); + Ok(()) } - pub async fn did_receive_row_changed(&self, row_id: RowId) { - if !self.cell_filter_cache.read().is_empty() { - self - .gen_task( - FilterEvent::RowDidChanged(row_id), - QualityOfService::UserInteractive, - ) - .await - } - } - - #[tracing::instrument(level = "trace", skip(self))] - pub async fn did_receive_changes( - &self, - changeset: FilterChangeset, - ) -> Option { - let mut notification: Option = None; - - if let Some(filter_type) = &changeset.insert_filter { - if let Some(filter) = self.filter_from_filter_id(&filter_type.id).await { - notification = Some(FilterChangesetNotificationPB::from_insert( - &self.view_id, - vec![filter], - )); - } - if let Some(filter) = self - .delegate - .get_filter(&self.view_id, &filter_type.id) - .await - { - self.refresh_filters(vec![filter]).await; - } - } - - if let Some(updated_filter_type) = changeset.update_filter { - if let Some(old_filter_type) = updated_filter_type.old { - let new_filter = self - .filter_from_filter_id(&updated_filter_type.new.id) - .await; - let old_filter = self.filter_from_filter_id(&old_filter_type.id).await; - - // Get the filter id - let mut filter_id = old_filter.map(|filter| filter.id); - if filter_id.is_none() { - filter_id = new_filter.as_ref().map(|filter| filter.id.clone()); - } - - if let Some(filter_id) = filter_id { - // Update the corresponding filter in the cache - if let Some(filter) = self.delegate.get_filter(&self.view_id, &filter_id).await { - self.refresh_filters(vec![filter]).await; - } - - notification = Some(FilterChangesetNotificationPB::from_update( - &self.view_id, - vec![UpdatedFilter { - filter_id, - filter: new_filter, - }], - )); - } - } - } - - if let Some(filter_context) = &changeset.delete_filter { - if let Some(filter) = self.filter_from_filter_id(&filter_context.filter_id).await { - notification = Some(FilterChangesetNotificationPB::from_delete( - &self.view_id, - vec![filter], - )); - } - self - .cell_filter_cache - .write() - .remove(&filter_context.field_id); - } - - self - .gen_task(FilterEvent::FilterDidChanged, QualityOfService::Background) - .await; - tracing::trace!("{:?}", notification); - notification - } - - async fn filter_from_filter_id(&self, filter_id: &str) -> Option { + async fn get_field_map(&self) -> HashMap { self .delegate - .get_filter(&self.view_id, filter_id) + .get_fields(&self.view_id, None) .await - .map(|filter| FilterPB::from(filter.as_ref())) + .into_iter() + .map(|field| (field.id.clone(), field)) + .collect::>() } - #[tracing::instrument(level = "trace", skip_all)] - async fn refresh_filters(&self, filters: Vec>) { - for filter in filters { - let field_id = &filter.field_id; - tracing::trace!("Create filter with type: {:?}", filter.field_type); - match &filter.field_type { - FieldType::RichText => { - self - .cell_filter_cache - .write() - .insert(field_id, TextFilterPB::from_filter(filter.as_ref())); - }, - FieldType::Number => { - self - .cell_filter_cache - .write() - .insert(field_id, NumberFilterPB::from_filter(filter.as_ref())); - }, - FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { - self - .cell_filter_cache - .write() - .insert(field_id, DateFilterPB::from_filter(filter.as_ref())); - }, - FieldType::SingleSelect | FieldType::MultiSelect => { - self - .cell_filter_cache - .write() - .insert(field_id, SelectOptionFilterPB::from_filter(filter.as_ref())); - }, - FieldType::Checkbox => { - self - .cell_filter_cache - .write() - .insert(field_id, CheckboxFilterPB::from_filter(filter.as_ref())); - }, - FieldType::URL => { - self - .cell_filter_cache - .write() - .insert(field_id, TextFilterPB::from_filter(filter.as_ref())); - }, - FieldType::Checklist => { - self - .cell_filter_cache - .write() - .insert(field_id, ChecklistFilterPB::from_filter(filter.as_ref())); - }, - FieldType::Relation => { - self - .cell_filter_cache - .write() - .insert(field_id, RelationFilterPB::from_filter(filter.as_ref())); - }, + fn delete_filter(filters: &mut Vec, filter_id: &str) { + let mut find_root_filter: Option = None; + let mut find_parent_of_non_root_filter: Option<&mut Filter> = None; + + for (position, filter) in filters.iter_mut().enumerate() { + if filter.id == filter_id { + find_root_filter = Some(position); + break; + } + if let Some(filter) = filter.find_parent_of_filter(filter_id) { + find_parent_of_non_root_filter = Some(filter); + break; + } + } + + if let Some(pos) = find_root_filter { + filters.remove(pos); + } else if let Some(filter) = find_parent_of_non_root_filter { + if let Err(err) = filter.delete_filter(filter_id) { + tracing::error!("error while deleting filter: {}", err); } } } } -/// Returns None if there is no change in this row after applying the filter +/// Returns `Some` if the visibility of the row changed after applying the filter and `None` +/// otherwise #[tracing::instrument(level = "trace", skip_all)] fn filter_row( row: &Row, - result_by_row_id: &DashMap, - field_by_field_id: &HashMap>, + result_by_row_id: &DashMap, + field_by_field_id: &HashMap, cell_data_cache: &CellCache, - cell_filter_cache: &CellFilterCache, -) -> Option<(RowId, bool)> { - // Create a filter result cache if it's not exist - let mut filter_result = result_by_row_id.entry(row.id.clone()).or_default(); - let old_is_visible = filter_result.is_visible(); + filters: &Vec, +) -> Option { + // Create a filter result cache if it doesn't exist + let mut filter_result = result_by_row_id.entry(row.id.clone()).or_insert(true); + let old_is_visible = *filter_result; - // Iterate each cell of the row to check its visibility - for (field_id, field) in field_by_field_id { - if !cell_filter_cache.read().contains(field_id) { - filter_result.visible_by_field_id.remove(field_id); - continue; - } + let mut new_is_visible = true; - let cell = row.cells.get(field_id).cloned(); - let field_type = FieldType::from(field.field_type); - // if the visibility of the cell_rew is changed, which means the visibility of the - // row is changed too. - if let Some(is_visible) = - filter_cell(&field_type, field, cell, cell_data_cache, cell_filter_cache) - { - filter_result - .visible_by_field_id - .insert(field_id.to_string(), is_visible); + for filter in filters { + if let Some(is_visible) = apply_filter(row, field_by_field_id, cell_data_cache, filter) { + new_is_visible = new_is_visible && is_visible; + + // short-circuit as soon as one filter tree returns false + if !new_is_visible { + break; + } } } - let is_visible = filter_result.is_visible(); - if old_is_visible != is_visible { - Some((row.id.clone(), is_visible)) + *filter_result = new_is_visible; + + if old_is_visible != new_is_visible { + Some(new_is_visible) } else { None } } -// Returns None if there is no change in this cell after applying the filter -// Returns Some if the visibility of the cell is changed - -#[tracing::instrument(level = "trace", skip_all, fields(cell_content))] -fn filter_cell( - field_type: &FieldType, - field: &Arc, - cell: Option, +/// Recursively applies a `Filter` to a `Row`'s cells. +fn apply_filter( + row: &Row, + field_by_field_id: &HashMap, cell_data_cache: &CellCache, - cell_filter_cache: &CellFilterCache, + filter: &Filter, ) -> Option { - let handler = TypeOptionCellExt::new( - field.as_ref(), - Some(cell_data_cache.clone()), - Some(cell_filter_cache.clone()), - ) - .get_type_option_cell_data_handler(field_type)?; - let is_visible = - handler.handle_cell_filter(field_type, field.as_ref(), &cell.unwrap_or_default()); - Some(is_visible) + match &filter.inner { + FilterInner::And { children } => { + if children.is_empty() { + return None; + } + for child_filter in children.iter() { + if let Some(false) = apply_filter(row, field_by_field_id, cell_data_cache, child_filter) { + return Some(false); + } + } + Some(true) + }, + FilterInner::Or { children } => { + if children.is_empty() { + return None; + } + for child_filter in children.iter() { + if let Some(true) = apply_filter(row, field_by_field_id, cell_data_cache, child_filter) { + return Some(true); + } + } + Some(false) + }, + FilterInner::Data { + field_id, + field_type, + condition_and_content, + } => { + let field = match field_by_field_id.get(field_id) { + Some(field) => field, + None => { + tracing::error!("cannot find field"); + return Some(false); + }, + }; + if *field_type != FieldType::from(field.field_type) { + tracing::error!("field type of filter doesn't match field type of field"); + return Some(false); + } + let cell = row.cells.get(field_id).cloned(); + let field_type = FieldType::from(field.field_type); + if let Some(handler) = TypeOptionCellExt::new(field, Some(cell_data_cache.clone())) + .get_type_option_cell_data_handler(&field_type) + { + Some(handler.handle_cell_filter(field, &cell.unwrap_or_default(), condition_and_content)) + } else { + Some(true) + } + }, + } } #[derive(Serialize, Deserialize, Clone, Debug)] @@ -446,6 +554,7 @@ impl ToString for FilterEvent { impl FromStr for FilterEvent { type Err = serde_json::Error; + fn from_str(s: &str) -> Result { serde_json::from_str(s) } diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs index 27e220b0c2..f12bc415d4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -1,125 +1,450 @@ +use std::collections::HashMap; +use std::mem; + use anyhow::bail; use collab::core::any_map::AnyMapExtension; +use collab_database::database::gen_database_filter_id; use collab_database::rows::RowId; use collab_database::views::{FilterMap, FilterMapBuilder}; +use flowy_error::{FlowyError, FlowyResult}; +use lib_infra::box_any::BoxAny; -use crate::entities::{FieldType, FilterPB, InsertedRowPB}; +use crate::entities::{ + CheckboxFilterPB, ChecklistFilterPB, DateFilterContent, DateFilterPB, FieldType, FilterType, + InsertedRowPB, NumberFilterPB, RelationFilterPB, SelectOptionFilterPB, TextFilterPB, +}; +use crate::services::field::SelectOptionIds; -#[derive(Debug, Clone)] +pub trait ParseFilterData { + fn parse(condition: u8, content: String) -> Self; +} + +#[derive(Debug)] pub struct Filter { pub id: String, - pub field_id: String, - pub field_type: FieldType, - pub condition: i64, - pub content: String, + pub inner: FilterInner, +} + +impl Filter { + /// Recursively determine whether there are any data filters in the filter tree. A tree that has + /// multiple AND/OR filters but no Data filters is considered "empty". + pub fn is_empty(&self) -> bool { + match &self.inner { + FilterInner::And { children } | FilterInner::Or { children } => children + .iter() + .map(|filter| filter.is_empty()) + .all(|is_empty| is_empty), + FilterInner::Data { .. } => false, + } + } + + /// Recursively find a filter based on `filter_id`. Returns `None` if the filter cannot be found. + pub fn find_filter(&mut self, filter_id: &str) -> Option<&mut Self> { + if self.id == filter_id { + return Some(self); + } + match &mut self.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + for child_filter in children.iter_mut() { + let result = child_filter.find_filter(filter_id); + if result.is_some() { + return result; + } + } + None + }, + FilterInner::Data { .. } => None, + } + } + + /// Recursively find the parent of a filter whose id is `filter_id`. Returns `None` if the filter + /// cannot be found. + pub fn find_parent_of_filter(&mut self, filter_id: &str) -> Option<&mut Self> { + if self.id == filter_id { + return None; + } + match &mut self.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + for child_filter in children.iter_mut() { + if child_filter.id == filter_id { + return Some(child_filter); + } + let result = child_filter.find_parent_of_filter(filter_id); + if result.is_some() { + return result; + } + } + None + }, + FilterInner::Data { .. } => None, + } + } + + /// Converts a filter from And/Or/Data to And/Or. If the current type of the filter is Data, + /// return the FilterInner after the conversion. + pub fn convert_to_and_or_filter_type( + &mut self, + filter_type: FilterType, + ) -> FlowyResult> { + match (&mut self.inner, filter_type) { + (FilterInner::And { children }, FilterType::Or) => { + self.inner = FilterInner::Or { + children: mem::take(children), + }; + Ok(None) + }, + (FilterInner::Or { children }, FilterType::And) => { + self.inner = FilterInner::And { + children: mem::take(children), + }; + Ok(None) + }, + (FilterInner::Data { .. }, FilterType::And) => { + let mut inner = FilterInner::And { children: vec![] }; + mem::swap(&mut self.inner, &mut inner); + Ok(Some(inner)) + }, + (FilterInner::Data { .. }, FilterType::Or) => { + let mut inner = FilterInner::Or { children: vec![] }; + mem::swap(&mut self.inner, &mut inner); + Ok(Some(inner)) + }, + (_, FilterType::Data) => { + // from And/Or to Data + Err(FlowyError::internal().with_context(format!( + "conversion from {:?} to FilterType::Data not supported", + FilterType::from(&self.inner) + ))) + }, + _ => { + tracing::warn!("conversion to the same filter type"); + Ok(None) + }, + } + } + + /// Insert a filter into the current filter in the filter tree. If the current filter + /// is an AND/OR filter, then the filter is appended to its children. Otherwise, the current + /// filter is converted to an AND filter, after which the current data filter and the new filter + /// are added to the AND filter's children. + pub fn insert_filter(&mut self, filter: Filter) -> FlowyResult<()> { + match &mut self.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + children.push(filter); + }, + FilterInner::Data { .. } => { + // convert to FilterInner::And by default + let old_filter = self + .convert_to_and_or_filter_type(FilterType::And) + .and_then(|result| { + result.ok_or_else(|| FlowyError::internal().with_context("failed to convert filter")) + })?; + self.insert_filter(Filter { + id: gen_database_filter_id(), + inner: old_filter, + })?; + self.insert_filter(filter)?; + }, + } + + Ok(()) + } + + /// Update the criteria of a data filter. Return an error if the current filter is an AND/OR + /// filter. + pub fn update_filter_data(&mut self, filter_data: FilterInner) -> FlowyResult<()> { + match &self.inner { + FilterInner::And { .. } | FilterInner::Or { .. } => Err(FlowyError::internal().with_context( + format!("unexpected filter type {:?}", FilterType::from(&self.inner)), + )), + _ => { + self.inner = filter_data; + Ok(()) + }, + } + } + + /// Delete a filter based on `filter_id`. The current filter must be the parent of the filter + /// whose id is `filter_id`. Returns an error if the current filter is a Data filter (which + /// cannot have children), or the filter to be deleted cannot be found. + pub fn delete_filter(&mut self, filter_id: &str) -> FlowyResult<()> { + match &mut self.inner { + FilterInner::And { children } | FilterInner::Or { children } => children + .iter() + .position(|filter| filter.id == filter_id) + .map(|position| { + children.remove(position); + }) + .ok_or_else(|| { + FlowyError::internal() + .with_context(format!("filter with filter_id {:?} not found", filter_id)) + }), + FilterInner::Data { .. } => Err( + FlowyError::internal().with_context("unexpected parent filter type of FilterInner::Data"), + ), + } + } + + /// Recursively finds any Data filter whose `field_id` is equal to `matching_field_id`. Any found + /// filters' id is appended to the `ids` vector. + pub fn find_all_filters_with_field_id(&self, matching_field_id: &str, ids: &mut Vec) { + match &self.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + for child_filter in children.iter() { + child_filter.find_all_filters_with_field_id(matching_field_id, ids); + } + }, + FilterInner::Data { field_id, .. } => { + if field_id == matching_field_id { + ids.push(self.id.clone()); + } + }, + } + } + + /// Recursively determine the smallest set of filters that loosely represents the filter tree. The + /// filters are appended to the `min_effective_filters` vector. The following rules are followed + /// when determining if a filter should get included. If the current filter is: + /// + /// 1. a Data filter, then it should be included. + /// 2. an AND filter, then all of its effective children should be + /// included. + /// 3. an OR filter, then only the first child should be included. + pub fn get_min_effective_filters<'a>(&'a self, min_effective_filters: &mut Vec<&'a FilterInner>) { + match &self.inner { + FilterInner::And { children } => { + for filter in children.iter() { + filter.get_min_effective_filters(min_effective_filters); + } + }, + FilterInner::Or { children } => { + if let Some(filter) = children.first() { + filter.get_min_effective_filters(min_effective_filters); + } + }, + FilterInner::Data { .. } => min_effective_filters.push(&self.inner), + } + } + + /// Recursively get all of the filtering field ids and the associated filter_ids + pub fn get_all_filtering_field_ids(&self, field_ids: &mut HashMap>) { + match &self.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + for child in children.iter() { + child.get_all_filtering_field_ids(field_ids); + } + }, + FilterInner::Data { field_id, .. } => { + field_ids + .entry(field_id.clone()) + .and_modify(|filter_ids| filter_ids.push(self.id.clone())) + .or_insert_with(|| vec![self.id.clone()]); + }, + } + } +} + +#[derive(Debug)] +pub enum FilterInner { + And { + children: Vec, + }, + Or { + children: Vec, + }, + Data { + field_id: String, + field_type: FieldType, + condition_and_content: BoxAny, + }, +} + +impl FilterInner { + pub fn new_data( + field_id: String, + field_type: FieldType, + condition: i64, + content: String, + ) -> Self { + let condition_and_content = match field_type { + FieldType::RichText | FieldType::URL => { + BoxAny::new(TextFilterPB::parse(condition as u8, content)) + }, + FieldType::Number => BoxAny::new(NumberFilterPB::parse(condition as u8, content)), + FieldType::DateTime | FieldType::CreatedTime | FieldType::LastEditedTime => { + BoxAny::new(DateFilterPB::parse(condition as u8, content)) + }, + FieldType::SingleSelect | FieldType::MultiSelect => { + BoxAny::new(SelectOptionFilterPB::parse(condition as u8, content)) + }, + FieldType::Checklist => BoxAny::new(ChecklistFilterPB::parse(condition as u8, content)), + FieldType::Checkbox => BoxAny::new(CheckboxFilterPB::parse(condition as u8, content)), + FieldType::Relation => BoxAny::new(RelationFilterPB::parse(condition as u8, content)), + }; + + FilterInner::Data { + field_id, + field_type, + condition_and_content, + } + } + + pub fn get_int_repr(&self) -> i64 { + match self { + FilterInner::And { .. } => FILTER_AND_INDEX, + FilterInner::Or { .. } => FILTER_OR_INDEX, + FilterInner::Data { .. } => FILTER_DATA_INDEX, + } + } } const FILTER_ID: &str = "id"; +const FILTER_TYPE: &str = "filter_type"; const FIELD_ID: &str = "field_id"; const FIELD_TYPE: &str = "ty"; const FILTER_CONDITION: &str = "condition"; const FILTER_CONTENT: &str = "content"; +const FILTER_CHILDREN: &str = "children"; -impl From for FilterMap { - fn from(data: Filter) -> Self { - FilterMapBuilder::new() - .insert_str_value(FILTER_ID, data.id) - .insert_str_value(FIELD_ID, data.field_id) - .insert_str_value(FILTER_CONTENT, data.content) - .insert_i64_value(FIELD_TYPE, data.field_type.into()) - .insert_i64_value(FILTER_CONDITION, data.condition) - .build() +const FILTER_AND_INDEX: i64 = 0; +const FILTER_OR_INDEX: i64 = 1; +const FILTER_DATA_INDEX: i64 = 2; + +impl<'a> From<&'a Filter> for FilterMap { + fn from(filter: &'a Filter) -> Self { + let mut builder = FilterMapBuilder::new() + .insert_str_value(FILTER_ID, &filter.id) + .insert_i64_value(FILTER_TYPE, filter.inner.get_int_repr()); + + builder = match &filter.inner { + FilterInner::And { children } | FilterInner::Or { children } => { + builder.insert_maps(FILTER_CHILDREN, children.iter().collect::>()) + }, + FilterInner::Data { + field_id, + field_type, + condition_and_content, + } => { + let get_raw_condition_and_content = || -> Option<(u8, String)> { + let (condition, content) = match field_type { + FieldType::RichText | FieldType::URL => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, filter.content) + }, + FieldType::Number => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, filter.content) + }, + FieldType::DateTime | FieldType::LastEditedTime | FieldType::CreatedTime => { + let filter = condition_and_content.cloned::()?; + let content = DateFilterContent { + start: filter.start, + end: filter.end, + timestamp: filter.timestamp, + } + .to_string(); + (filter.condition as u8, content) + }, + FieldType::SingleSelect | FieldType::MultiSelect => { + let filter = condition_and_content.cloned::()?; + let content = SelectOptionIds::from(filter.option_ids).to_string(); + (filter.condition as u8, content) + }, + FieldType::Checkbox => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, "".to_string()) + }, + FieldType::Checklist => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, "".to_string()) + }, + FieldType::Relation => { + let filter = condition_and_content.cloned::()?; + (filter.condition as u8, "".to_string()) + }, + }; + Some((condition, content)) + }; + + let (condition, content) = get_raw_condition_and_content().unwrap_or_else(|| { + tracing::error!("cannot deserialize filter condition and content filter properly"); + Default::default() + }); + + builder + .insert_str_value(FIELD_ID, field_id) + .insert_i64_value(FIELD_TYPE, field_type.into()) + .insert_i64_value(FILTER_CONDITION, condition as i64) + .insert_str_value(FILTER_CONTENT, content) + }, + }; + + builder.build() } } impl TryFrom for Filter { type Error = anyhow::Error; - fn try_from(filter: FilterMap) -> Result { - match ( - filter.get_str_value(FILTER_ID), - filter.get_str_value(FIELD_ID), - ) { - (Some(id), Some(field_id)) => { - let condition = filter.get_i64_value(FILTER_CONDITION).unwrap_or(0); - let content = filter.get_str_value(FILTER_CONTENT).unwrap_or_default(); - let field_type = filter - .get_i64_value(FIELD_TYPE) - .map(FieldType::from) - .unwrap_or_default(); - Ok(Filter { - id, - field_id, - field_type, - condition, - content, - }) + fn try_from(filter_map: FilterMap) -> Result { + let filter_id = filter_map + .get_str_value(FILTER_ID) + .ok_or_else(|| anyhow::anyhow!("invalid filter data"))?; + let filter_type = filter_map + .get_i64_value(FILTER_TYPE) + .unwrap_or(FILTER_DATA_INDEX); + + let filter = Filter { + id: filter_id, + inner: match filter_type { + FILTER_AND_INDEX => FilterInner::And { + children: filter_map.try_get_array(FILTER_CHILDREN), + }, + FILTER_OR_INDEX => FilterInner::Or { + children: filter_map.try_get_array(FILTER_CHILDREN), + }, + FILTER_DATA_INDEX => { + let field_id = filter_map + .get_str_value(FIELD_ID) + .ok_or_else(|| anyhow::anyhow!("invalid filter data"))?; + let field_type = filter_map + .get_i64_value(FIELD_TYPE) + .map(FieldType::from) + .unwrap_or_default(); + let condition = filter_map.get_i64_value(FILTER_CONDITION).unwrap_or(0); + let content = filter_map.get_str_value(FILTER_CONTENT).unwrap_or_default(); + + FilterInner::new_data(field_id, field_type, condition, content) + }, + _ => bail!("Unsupported filter type"), }, - _ => { - bail!("Invalid filter data") - }, - } + }; + + Ok(filter) } } -#[derive(Debug)] -pub struct FilterChangeset { - pub(crate) insert_filter: Option, - pub(crate) update_filter: Option, - pub(crate) delete_filter: Option, -} #[derive(Debug)] -pub struct UpdatedFilter { - pub old: Option, - pub new: Filter, -} - -impl UpdatedFilter { - pub fn new(old: Option, new: Filter) -> UpdatedFilter { - Self { old, new } - } -} - -impl FilterChangeset { - pub fn from_insert(filter: Filter) -> Self { - Self { - insert_filter: Some(filter), - update_filter: None, - delete_filter: None, - } - } - - pub fn from_update(filter: UpdatedFilter) -> Self { - Self { - insert_filter: None, - update_filter: Some(filter), - delete_filter: None, - } - } - pub fn from_delete(filter_context: FilterContext) -> Self { - Self { - insert_filter: None, - update_filter: None, - delete_filter: Some(filter_context), - } - } -} - -#[derive(Debug, Clone)] -pub struct FilterContext { - pub filter_id: String, - pub field_id: String, - pub field_type: FieldType, -} - -impl From<&FilterPB> for FilterContext { - fn from(filter: &FilterPB) -> Self { - Self { - filter_id: filter.id.clone(), - field_id: filter.field_id.clone(), - field_type: filter.field_type, - } - } +pub enum FilterChangeset { + Insert { + parent_filter_id: Option, + data: FilterInner, + }, + UpdateType { + filter_id: String, + filter_type: FilterType, + }, + UpdateData { + filter_id: String, + data: FilterInner, + }, + Delete { + filter_id: String, + field_id: String, + }, + DeleteAllWithFieldId { + field_id: String, + }, } #[derive(Clone, Debug)] diff --git a/frontend/rust-lib/flowy-database2/src/services/filter/task.rs b/frontend/rust-lib/flowy-database2/src/services/filter/task.rs index dbf55776df..03ed453f89 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/task.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/task.rs @@ -1,7 +1,6 @@ use crate::services::filter::FilterController; use lib_infra::future::BoxResultFuture; use lib_infra::priority_task::{TaskContent, TaskHandler}; -use std::collections::HashMap; use std::sync::Arc; pub struct FilterTaskHandler { @@ -40,21 +39,3 @@ impl TaskHandler for FilterTaskHandler { }) } } -/// Refresh the filter according to the field id. -#[derive(Default)] -pub(crate) struct FilterResult { - pub(crate) visible_by_field_id: HashMap, -} - -impl FilterResult { - pub(crate) fn is_visible(&self) -> bool { - let mut is_visible = true; - for visible in self.visible_by_field_id.values() { - if !is_visible { - break; - } - is_visible = *visible; - } - is_visible - } -} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/action.rs b/frontend/rust-lib/flowy-database2/src/services/group/action.rs index 11bd169591..b540fb5fa3 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/action.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/action.rs @@ -1,16 +1,15 @@ -use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; -use collab_database::rows::{Cell, Row, RowDetail, RowId}; +use collab_database::rows::{Cell, Cells, Row, RowDetail, RowId}; use flowy_error::FlowyResult; use crate::entities::{GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; use crate::services::field::TypeOption; -use crate::services::group::{GroupChangesets, GroupData, MoveGroupRowContext}; +use crate::services::group::{GroupChangeset, GroupData, MoveGroupRowContext}; -/// Using polymorphism to provides the customs action for different group controller. -/// -/// For example, the `CheckboxGroupController` implements this trait to provide custom behavior. +/// [GroupCustomize] is implemented by parameterized `BaseGroupController`s to provide different +/// behaviors. This allows the BaseGroupController to call these actions indescriminantly using +/// polymorphism. /// pub trait GroupCustomize: Send + Sync { type GroupTypeOption: TypeOption; @@ -57,11 +56,7 @@ pub trait GroupCustomize: Send + Sync { ) -> (Option, Vec); /// Move row from one group to another - fn move_row( - &mut self, - cell_data: &::CellProtobufType, - context: MoveGroupRowContext, - ) -> Vec; + fn move_row(&mut self, context: MoveGroupRowContext) -> Vec; /// Returns None if there is no need to delete the group when corresponding row get removed fn delete_group_when_move_row( @@ -72,21 +67,38 @@ pub trait GroupCustomize: Send + Sync { None } - fn generate_new_group( + fn create_group( &mut self, _name: String, ) -> FlowyResult<(Option, Option)> { Ok((None, None)) } - fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult>; + fn delete_group(&mut self, group_id: &str) -> FlowyResult>; + + fn update_type_option_when_update_group( + &mut self, + _changeset: &GroupChangeset, + _type_option: &Self::GroupTypeOption, + ) -> Option { + None + } + + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str); } -/// Defines the shared actions any group controller can perform. -#[async_trait] -pub trait GroupControllerOperation: Send + Sync { +/// The `GroupController` trait defines the behavior of the group controller when performing any +/// group-related tasks, such as managing rows within a group, transferring rows between groups, +/// manipulating groups themselves, and even pre-filling a row's cells before it is created. +/// +/// Depending on the type of the field that is being grouped, a parameterized `BaseGroupController` +/// or a `DefaultGroupController` may be the actual object that provides the functionality of +/// this trait. For example, a `Single-Select` group controller will be a `BaseGroupController`, +/// while a `URL` group controller will be a `DefaultGroupController`. +/// +pub trait GroupController: Send + Sync { /// Returns the id of field that is being used to group the rows - fn field_id(&self) -> &str; + fn get_grouping_field_id(&self) -> &str; /// Returns all of the groups currently managed by the controller fn get_all_groups(&self) -> Vec<&GroupData>; @@ -175,10 +187,13 @@ pub trait GroupControllerOperation: Send + Sync { /// in the field type option data. /// /// * `changesets`: list of changesets to be made to one or more groups - async fn apply_group_changeset( + fn apply_group_changeset( &mut self, - changesets: &GroupChangesets, - ) -> FlowyResult<(Vec, TypeOptionData)>; + changesets: &[GroupChangeset], + ) -> FlowyResult<(Vec, Option)>; + + /// Called before the row was created. + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str); } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs index 128dd30543..6134b7d265 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/configuration.rs @@ -1,11 +1,8 @@ -use std::collections::HashMap; use std::fmt::Formatter; use std::marker::PhantomData; use std::sync::Arc; -use async_trait::async_trait; use collab_database::fields::Field; -use collab_database::rows::{Cell, RowId}; use indexmap::IndexMap; use serde::de::DeserializeOwned; use serde::Serialize; @@ -21,28 +18,15 @@ use crate::services::group::{ default_group_setting, GeneratedGroups, Group, GroupChangeset, GroupData, GroupSetting, }; -pub trait GroupSettingReader: Send + Sync + 'static { +pub trait GroupContextDelegate: Send + Sync + 'static { fn get_group_setting(&self, view_id: &str) -> Fut>>; - fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut>; -} -pub trait GroupSettingWriter: Send + Sync + 'static { + fn get_configuration_cells(&self, view_id: &str, field_id: &str) -> Fut>; + fn save_configuration(&self, view_id: &str, group_setting: GroupSetting) -> Fut>; } -#[async_trait] -pub trait GroupTypeOptionCellOperation: Send + Sync + 'static { - async fn get_cell(&self, row_id: &RowId, field_id: &str) -> FlowyResult>; - async fn update_cell( - &self, - view_id: &str, - row_id: &RowId, - field_id: &str, - cell: Cell, - ) -> FlowyResult<()>; -} - -impl std::fmt::Display for GroupContext { +impl std::fmt::Display for GroupControllerContext { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { self.group_by_id.iter().for_each(|(_, group)| { let _ = f.write_fmt(format_args!( @@ -56,12 +40,12 @@ impl std::fmt::Display for GroupContext { } } -/// A [GroupContext] represents as the groups memory cache -/// Each [GenericGroupController] has its own [GroupContext], the `context` has its own configuration +/// A [GroupControllerContext] represents as the groups memory cache +/// Each [GenericGroupController] has its own [GroupControllerContext], the `context` has its own configuration /// that is restored from the disk. /// /// The `context` contains a list of [GroupData]s and the grouping [Field] -pub struct GroupContext { +pub struct GroupControllerContext { pub view_id: String, /// The group configuration restored from the disk. /// @@ -70,39 +54,32 @@ pub struct GroupContext { configuration_phantom: PhantomData, - /// The grouping field - field: Arc, + /// The grouping field id + field_id: String, /// Cache all the groups. Cache the group by its id. /// We use the id of the [Field] as the [No Status] group id. group_by_id: IndexMap, - /// A reader that implement the [GroupSettingReader] trait - /// - reader: Arc, - - /// A writer that implement the [GroupSettingWriter] trait is used to save the - /// configuration to disk - /// - writer: Arc, + /// delegate that reads and writes data to and from disk + delegate: Arc, } -impl GroupContext +impl GroupControllerContext where C: Serialize + DeserializeOwned, { #[tracing::instrument(level = "trace", skip_all, err)] pub async fn new( view_id: String, - field: Arc, - reader: Arc, - writer: Arc, + field: Field, + delegate: Arc, ) -> FlowyResult { - event!(tracing::Level::TRACE, "GroupContext::new"); - let setting = match reader.get_group_setting(&view_id).await { + event!(tracing::Level::TRACE, "GroupControllerContext::new"); + let setting = match delegate.get_group_setting(&view_id).await { None => { let default_configuration = default_group_setting(&field); - writer + delegate .save_configuration(&view_id, default_configuration.clone()) .await?; Arc::new(default_configuration) @@ -112,10 +89,9 @@ where Ok(Self { view_id, - field, + field_id: field.id, group_by_id: IndexMap::new(), - reader, - writer, + delegate, setting, configuration_phantom: PhantomData, }) @@ -126,11 +102,11 @@ where /// We take the `id` of the `field` as the no status group id #[allow(dead_code)] pub(crate) fn get_no_status_group(&self) -> Option<&GroupData> { - self.group_by_id.get(&self.field.id) + self.group_by_id.get(&self.field_id) } pub(crate) fn get_mut_no_status_group(&mut self) -> Option<&mut GroupData> { - self.group_by_id.get_mut(&self.field.id) + self.group_by_id.get_mut(&self.field_id) } pub(crate) fn groups(&self) -> Vec<&GroupData> { @@ -155,7 +131,7 @@ where /// Iterate mut the groups without `No status` group pub(crate) fn iter_mut_status_groups(&mut self, mut each: impl FnMut(&mut GroupData)) { self.group_by_id.iter_mut().for_each(|(_, group)| { - if group.id != self.field.id { + if group.id != self.field_id { each(group); } }); @@ -168,13 +144,7 @@ where } #[tracing::instrument(level = "trace", skip(self), err)] pub(crate) fn add_new_group(&mut self, group: Group) -> FlowyResult { - let group_data = GroupData::new( - group.id.clone(), - self.field.id.clone(), - group.name.clone(), - group.id.clone(), - group.visible, - ); + let group_data = GroupData::new(group.id.clone(), self.field_id.clone(), group.visible); self.group_by_id.insert(group.id.clone(), group_data); let (index, group_data) = self.get_group(&group.id).unwrap(); let insert_group = InsertedGroupPB { @@ -232,7 +202,7 @@ where configuration .groups .iter() - .map(|group| group.name.clone()) + .map(|group| group.id.clone()) .collect::>() .join(",") ); @@ -268,22 +238,12 @@ where ) -> FlowyResult> { let GeneratedGroups { no_status_group, - group_configs, + groups, } = generated_groups; - let mut new_groups = vec![]; - let mut filter_content_map = HashMap::new(); - group_configs.into_iter().for_each(|generate_group| { - filter_content_map.insert( - generate_group.group.id.clone(), - generate_group.filter_content, - ); - new_groups.push(generate_group.group); - }); - let mut old_groups = self.setting.groups.clone(); // clear all the groups if grouping by a new field - if self.setting.field_id != self.field.id { + if self.setting.field_id != self.field_id { old_groups.clear(); } @@ -292,7 +252,7 @@ where mut all_groups, new_groups, deleted_groups, - } = merge_groups(no_status_group, old_groups, new_groups); + } = merge_groups(no_status_group, old_groups, groups); let deleted_group_ids = deleted_groups .into_iter() @@ -321,12 +281,10 @@ where Some(pos) => { let old_group = configuration.groups.get_mut(pos).unwrap(); // Take the old group setting - group.visible = old_group.visible; - if !is_changed { - is_changed = is_group_changed(group, old_group); + if group.visible != old_group.visible { + is_changed = true; } - // Consider the the name of the `group_rev` as the newest. - old_group.name = group.name.clone(); + group.visible = old_group.visible; }, } } @@ -335,31 +293,14 @@ where // Update the memory cache of the groups all_groups.into_iter().for_each(|group| { - let filter_content = filter_content_map - .get(&group.id) - .cloned() - .unwrap_or_else(|| "".to_owned()); - let group = GroupData::new( - group.id, - self.field.id.clone(), - group.name, - filter_content, - group.visible, - ); + let group = GroupData::new(group.id, self.field_id.clone(), group.visible); self.group_by_id.insert(group.id.clone(), group); }); let initial_groups = new_groups .into_iter() .flat_map(|group_rev| { - let filter_content = filter_content_map.get(&group_rev.id)?; - let group = GroupData::new( - group_rev.id, - self.field.id.clone(), - group_rev.name, - filter_content.clone(), - group_rev.visible, - ); + let group = GroupData::new(group_rev.id, self.field_id.clone(), group_rev.visible); Some(GroupPB::from(group)) }) .collect(); @@ -385,14 +326,10 @@ where if let Some(visible) = group_changeset.visible { group.visible = visible; } - if let Some(name) = &group_changeset.name { - group.name = name.clone(); - } })?; if let Some(group) = update_group { if let Some(group_data) = self.group_by_id.get_mut(&group.id) { - group_data.name = group.name.clone(); group_data.is_visible = group.visible; }; } @@ -401,8 +338,8 @@ where pub(crate) async fn get_all_cells(&self) -> Vec { self - .reader - .get_configuration_cells(&self.view_id, &self.field.id) + .delegate + .get_configuration_cells(&self.view_id, &self.field_id) .await } @@ -423,10 +360,10 @@ where let is_changed = mut_configuration_fn(configuration); if is_changed { let configuration = (*self.setting).clone(); - let writer = self.writer.clone(); + let delegate = self.delegate.clone(); let view_id = self.view_id.clone(); af_spawn(async move { - match writer.save_configuration(&view_id, configuration).await { + match delegate.save_configuration(&view_id, configuration).await { Ok(_) => {}, Err(e) => { tracing::error!("Save group configuration failed: {}", e); @@ -504,13 +441,6 @@ fn merge_groups( merge_result } -fn is_group_changed(new: &Group, old: &Group) -> bool { - if new.name != old.name { - return true; - } - false -} - struct MergeGroupResult { // Contains the new groups and the updated groups all_groups: Vec, @@ -545,13 +475,13 @@ mod tests { exp_deleted_groups: Vec<&'a str>, } - let new_group = |name: &str| Group::new(name.to_string(), name.to_string()); + let new_group = |name: &str| Group::new(name.to_string()); let groups_from_strings = |strings: Vec<&str>| strings.iter().map(|s| new_group(s)).collect::>(); let group_stringify = |groups: Vec| { groups .iter() - .map(|group| group.name.clone()) + .map(|group| group.id.clone()) .collect::>() .join(",") }; diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs index acc240a9e3..a918e7f7c2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller.rs @@ -1,14 +1,14 @@ use std::marker::PhantomData; use std::sync::Arc; -use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cells, Row, RowDetail, RowId}; use futures::executor::block_on; +use lib_infra::future::Fut; use serde::de::DeserializeOwned; use serde::Serialize; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use crate::entities::{ FieldType, GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, @@ -17,67 +17,45 @@ use crate::entities::{ use crate::services::cell::{get_cell_protobuf, CellProtobufBlobParser}; use crate::services::field::{default_type_option_data_from_type, TypeOption, TypeOptionCellData}; use crate::services::group::action::{ - DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, GroupCustomize, + DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController, GroupCustomize, }; -use crate::services::group::configuration::GroupContext; +use crate::services::group::configuration::GroupControllerContext; use crate::services::group::entities::GroupData; -use crate::services::group::{GroupChangeset, GroupChangesets, GroupsBuilder, MoveGroupRowContext}; +use crate::services::group::{GroupChangeset, GroupsBuilder, MoveGroupRowContext}; -// use collab_database::views::Group; +pub trait GroupControllerDelegate: Send + Sync + 'static { + fn get_field(&self, field_id: &str) -> Option; -/// The [GroupController] trait defines the group actions, including create/delete/move items -/// For example, the group will insert a item if the one of the new [RowRevision]'s [CellRevision]s -/// content match the group filter. -/// -/// Different [FieldType] has a different controller that implements the [GroupController] trait. -/// If the [FieldType] doesn't implement its group controller, then the [DefaultGroupController] will -/// be used. + fn get_all_rows(&self, view_id: &str) -> Fut>>; +} + +/// [BaseGroupController] is a generic group controller that provides customized implementations +/// of the `GroupController` trait for different field types. /// -pub trait GroupController: GroupControllerOperation + Send + Sync { - /// Called when the type option of the [Field] was updated. - fn did_update_field_type_option(&mut self, field: &Field); - - /// Called before the row was created. - fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str); -} - -#[async_trait] -pub trait GroupOperationInterceptor { - type GroupTypeOption: TypeOption; - async fn type_option_from_group_changeset( - &self, - _changeset: &GroupChangeset, - _type_option: &Self::GroupTypeOption, - _view_id: &str, - ) -> Option { - None - } -} - -/// C: represents the group configuration that impl [GroupConfigurationSerde] -/// T: the type-option data deserializer that impl [TypeOptionDataDeserializer] -/// G: the group generator, [GroupsBuilder] -/// P: the parser that impl [CellProtobufBlobParser] for the CellBytes -pub struct BaseGroupController { +/// - `C`: represents the group configuration that impl [GroupConfigurationSerde] +/// - `G`: group generator, [GroupsBuilder] +/// - `P`: parser that impl [CellProtobufBlobParser] for the CellBytes +/// +/// See also: [DefaultGroupController] which contains the most basic implementation of +/// `GroupController` that only has one group. +pub struct BaseGroupController { pub grouping_field_id: String, - pub type_option: T, - pub context: GroupContext, + pub context: GroupControllerContext, group_builder_phantom: PhantomData, cell_parser_phantom: PhantomData

, - pub operation_interceptor: I, + pub delegate: Arc, } -impl BaseGroupController +impl BaseGroupController where C: Serialize + DeserializeOwned, - T: TypeOption + From + Send + Sync, - G: GroupsBuilder, GroupTypeOption = T>, - I: GroupOperationInterceptor + Send + Sync, + T: TypeOption + Send + Sync, + G: GroupsBuilder, GroupTypeOption = T>, { pub async fn new( - grouping_field: &Arc, - mut configuration: GroupContext, - operation_interceptor: I, + grouping_field: &Field, + mut configuration: GroupControllerContext, + delegate: Arc, ) -> FlowyResult { let field_type = FieldType::from(grouping_field.field_type); let type_option = grouping_field @@ -90,14 +68,20 @@ where Ok(Self { grouping_field_id: grouping_field.id.clone(), - type_option, context: configuration, group_builder_phantom: PhantomData, cell_parser_phantom: PhantomData, - operation_interceptor, + delegate, }) } + pub fn get_grouping_field_type_option(&self) -> Option { + self + .delegate + .get_field(&self.grouping_field_id) + .and_then(|field| field.get_type_option::(FieldType::from(field.field_type))) + } + fn update_no_status_group( &mut self, row_detail: &RowDetail, @@ -170,17 +154,15 @@ where } } -#[async_trait] -impl GroupControllerOperation for BaseGroupController +impl GroupController for BaseGroupController where P: CellProtobufBlobParser::CellProtobufType>, C: Serialize + DeserializeOwned + Sync + Send, - T: TypeOption + From + Send + Sync, - G: GroupsBuilder, GroupTypeOption = T>, - I: GroupOperationInterceptor + Send + Sync, + T: TypeOption + Send + Sync, + G: GroupsBuilder, GroupTypeOption = T>, Self: GroupCustomize, { - fn field_id(&self) -> &str { + fn get_grouping_field_id(&self) -> &str { &self.grouping_field_id } @@ -205,7 +187,7 @@ where let mut grouped_rows: Vec = vec![]; let cell_data = ::CellData::from(&cell); for group in self.context.groups() { - if self.can_group(&group.filter_content, &cell_data) { + if self.can_group(&group.id, &cell_data) { grouped_rows.push(GroupedRow { row_detail: (*row_detail).clone(), group_id: group.id.clone(), @@ -237,7 +219,7 @@ where &mut self, name: String, ) -> FlowyResult<(Option, Option)> { - self.generate_new_group(name) + ::create_group(self, name) } fn move_group(&mut self, from_group_id: &str, to_group_id: &str) -> FlowyResult<()> { @@ -249,24 +231,25 @@ where row_detail: &RowDetail, index: usize, ) -> Vec { + let mut changesets: Vec = vec![]; + let cell = match row_detail.row.cells.get(&self.grouping_field_id) { None => self.placeholder_cell(), Some(cell) => Some(cell.clone()), }; - let mut changesets: Vec = vec![]; if let Some(cell) = cell { let cell_data = ::CellData::from(&cell); let mut suitable_group_ids = vec![]; for group in self.get_all_groups() { - if self.can_group(&group.filter_content, &cell_data) { + if self.can_group(&group.id, &cell_data) { suitable_group_ids.push(group.id.clone()); let changeset = GroupRowsNotificationPB::insert( group.id.clone(), vec![InsertedRowPB { - row_meta: row_detail.into(), + row_meta: (*row_detail).clone().into(), index: Some(index as i32), is_new: true, }], @@ -277,15 +260,15 @@ where if !suitable_group_ids.is_empty() { for group_id in suitable_group_ids.iter() { if let Some(group) = self.context.get_mut_group(group_id) { - group.add_row(row_detail.clone()); + group.add_row((*row_detail).clone()); } } } else if let Some(no_status_group) = self.context.get_mut_no_status_group() { - no_status_group.add_row(row_detail.clone()); + no_status_group.add_row((*row_detail).clone()); let changeset = GroupRowsNotificationPB::insert( no_status_group.id.clone(), vec![InsertedRowPB { - row_meta: row_detail.into(), + row_meta: (*row_detail).clone().into(), index: Some(index as i32), is_new: true, }], @@ -303,18 +286,12 @@ where row_detail: &RowDetail, field: &Field, ) -> FlowyResult { - // let cell_data = row_rev.cells.get(&self.field_id).and_then(|cell_rev| { - // let cell_data: Option

= get_type_cell_data(cell_rev, field_rev, None); - // cell_data - // }); let mut result = DidUpdateGroupRowResult { inserted_group: None, deleted_group: None, row_changesets: vec![], }; - if let Some(cell_data) = get_cell_data_from_row::

(Some(&row_detail.row), field) { - let _old_row = old_row_detail.as_ref(); let old_cell_data = get_cell_data_from_row::

(old_row_detail.as_ref().map(|detail| &detail.row), field); if let Ok((insert, delete)) = self.create_or_delete_group_when_cell_changed( @@ -385,7 +362,7 @@ where let cell_bytes = get_cell_protobuf(&cell, context.field, None); let cell_data = cell_bytes.parser::

()?; result.deleted_group = self.delete_group_when_move_row(&context.row_detail.row, &cell_data); - result.row_changesets = self.move_row(&cell_data, context); + result.row_changesets = self.move_row(context); } else { tracing::warn!("Unexpected moving group row, changes should not be empty"); } @@ -397,7 +374,7 @@ where } fn delete_group(&mut self, group_id: &str) -> FlowyResult<(Vec, Option)> { - let group = if group_id != self.field_id() { + let group = if group_id != self.get_grouping_field_id() { self.get_group(group_id) } else { None @@ -410,32 +387,39 @@ where .iter() .map(|row| row.row.id.clone()) .collect(); - let type_option_data = self.delete_group_custom(group_id)?; + let type_option_data = ::delete_group(self, group_id)?; Ok((row_ids, type_option_data)) }, None => Ok((vec![], None)), } } - async fn apply_group_changeset( + fn apply_group_changeset( &mut self, - changeset: &GroupChangesets, - ) -> FlowyResult<(Vec, TypeOptionData)> { - for group_changeset in changeset.changesets.iter() { + changeset: &[GroupChangeset], + ) -> FlowyResult<(Vec, Option)> { + // update group visibility + for group_changeset in changeset.iter() { self.context.update_group(group_changeset)?; } - let mut type_option_data = TypeOptionData::new(); - for group_changeset in changeset.changesets.iter() { - if let Some(new_type_option_data) = self - .operation_interceptor - .type_option_from_group_changeset(group_changeset, &self.type_option, &self.context.view_id) - .await + + // update group name + let type_option = self.get_grouping_field_type_option().ok_or_else(|| { + FlowyError::internal().with_context("Failed to get grouping field type option") + })?; + + let mut updated_type_option = None; + + for group_changeset in changeset.iter() { + if let Some(type_option) = + self.update_type_option_when_update_group(group_changeset, &type_option) { - type_option_data.extend(new_type_option_data); + updated_type_option = Some(type_option); + break; } } + let updated_groups = changeset - .changesets .iter() .filter_map(|changeset| { self @@ -443,7 +427,15 @@ where .map(|(_, group)| GroupPB::from(group)) }) .collect::>(); - Ok((updated_groups, type_option_data)) + + Ok(( + updated_groups, + updated_type_option.map(|type_option| type_option.into()), + )) + } + + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { + ::will_create_row(self, cells, field, group_id); } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs index e6e9fdeabd..a3057b24a0 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/checkbox_controller.rs @@ -10,11 +10,10 @@ use crate::services::field::{ CheckboxCellDataParser, CheckboxTypeOption, TypeOption, CHECK, UNCHECK, }; use crate::services::group::action::GroupCustomize; -use crate::services::group::configuration::GroupContext; -use crate::services::group::controller::{BaseGroupController, GroupController}; +use crate::services::group::configuration::GroupControllerContext; +use crate::services::group::controller::BaseGroupController; use crate::services::group::{ - move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, GroupOperationInterceptor, - GroupsBuilder, MoveGroupRowContext, + move_group_row, GeneratedGroups, Group, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -22,16 +21,10 @@ pub struct CheckboxGroupConfiguration { pub hide_empty: bool, } -pub type CheckboxGroupController = BaseGroupController< - CheckboxGroupConfiguration, - CheckboxTypeOption, - CheckboxGroupBuilder, - CheckboxCellDataParser, - CheckboxGroupOperationInterceptorImpl, ->; - -pub type CheckboxGroupContext = GroupContext; +pub type CheckboxGroupController = + BaseGroupController; +pub type CheckboxGroupControllerContext = GroupControllerContext; impl GroupCustomize for CheckboxGroupController { type GroupTypeOption = CheckboxTypeOption; fn placeholder_cell(&self) -> Option { @@ -126,11 +119,7 @@ impl GroupCustomize for CheckboxGroupController { (None, changesets) } - fn move_row( - &mut self, - _cell_data: &::CellProtobufType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.context.iter_mut_groups(|group| { if let Some(changeset) = move_group_row(group, &mut context) { @@ -140,17 +129,11 @@ impl GroupCustomize for CheckboxGroupController { group_changeset } - fn delete_group_custom(&mut self, _group_id: &str) -> FlowyResult> { + fn delete_group(&mut self, _group_id: &str) -> FlowyResult> { Ok(None) } -} -impl GroupController for CheckboxGroupController { - fn did_update_field_type_option(&mut self, _field: &Field) { - // Do nothing - } - - fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { None => tracing::warn!("Can not find the group: {}", group_id), Some((_, group)) => { @@ -165,7 +148,7 @@ impl GroupController for CheckboxGroupController { pub struct CheckboxGroupBuilder(); #[async_trait] impl GroupsBuilder for CheckboxGroupBuilder { - type Context = CheckboxGroupContext; + type Context = CheckboxGroupControllerContext; type GroupTypeOption = CheckboxTypeOption; async fn build( @@ -173,26 +156,12 @@ impl GroupsBuilder for CheckboxGroupBuilder { _context: &Self::Context, _type_option: &Self::GroupTypeOption, ) -> GeneratedGroups { - let check_group = GeneratedGroupConfig { - group: Group::new(CHECK.to_string(), "".to_string()), - filter_content: CHECK.to_string(), - }; - - let uncheck_group = GeneratedGroupConfig { - group: Group::new(UNCHECK.to_string(), "".to_string()), - filter_content: UNCHECK.to_string(), - }; + let check_group = Group::new(CHECK.to_string()); + let uncheck_group = Group::new(UNCHECK.to_string()); GeneratedGroups { no_status_group: None, - group_configs: vec![check_group, uncheck_group], + groups: vec![check_group, uncheck_group], } } } - -pub struct CheckboxGroupOperationInterceptorImpl {} - -#[async_trait] -impl GroupOperationInterceptor for CheckboxGroupOperationInterceptorImpl { - type GroupTypeOption = CheckboxTypeOption; -} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs index 1d947d66e3..8a2827a107 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/date_controller.rs @@ -1,7 +1,5 @@ -use std::format; - use async_trait::async_trait; -use chrono::{DateTime, Datelike, Days, Duration, Local, NaiveDate, NaiveDateTime}; +use chrono::{DateTime, Datelike, Days, Duration, Local, NaiveDateTime}; use collab_database::database::timestamp; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; @@ -16,28 +14,24 @@ use crate::entities::{ use crate::services::cell::insert_date_cell; use crate::services::field::{DateCellData, DateCellDataParser, DateTypeOption, TypeOption}; use crate::services::group::action::GroupCustomize; -use crate::services::group::configuration::GroupContext; -use crate::services::group::controller::{BaseGroupController, GroupController}; +use crate::services::group::configuration::GroupControllerContext; +use crate::services::group::controller::BaseGroupController; use crate::services::group::{ - make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, - GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, + make_no_status_group, move_group_row, GeneratedGroups, Group, GroupsBuilder, MoveGroupRowContext, }; -pub trait GroupConfigurationContentSerde: Sized + Send + Sync { - fn from_json(s: &str) -> Result; - fn to_json(&self) -> Result; -} - #[derive(Default, Serialize, Deserialize)] pub struct DateGroupConfiguration { pub hide_empty: bool, pub condition: DateCondition, } -impl GroupConfigurationContentSerde for DateGroupConfiguration { +impl DateGroupConfiguration { fn from_json(s: &str) -> Result { serde_json::from_str(s) } + + #[allow(dead_code)] fn to_json(&self) -> Result { serde_json::to_string(self) } @@ -54,15 +48,10 @@ pub enum DateCondition { Year = 4, } -pub type DateGroupController = BaseGroupController< - DateGroupConfiguration, - DateTypeOption, - DateGroupBuilder, - DateCellDataParser, - DateGroupOperationInterceptorImpl, ->; +pub type DateGroupController = + BaseGroupController; -pub type DateGroupContext = GroupContext; +pub type DateGroupControllerContext = GroupControllerContext; impl GroupCustomize for DateGroupController { type GroupTypeOption = DateTypeOption; @@ -80,7 +69,7 @@ impl GroupCustomize for DateGroupController { content: &str, cell_data: &::CellData, ) -> bool { - content == group_id(cell_data, &self.context.get_setting_content()) + content == get_date_group_id(cell_data, &self.context.get_setting_content()) } fn create_or_delete_group_when_cell_changed( @@ -93,7 +82,7 @@ impl GroupCustomize for DateGroupController { let mut inserted_group = None; if self .context - .get_group(&group_id(&_cell_data.into(), &setting_content)) + .get_group(&get_date_group_id(&_cell_data.into(), &setting_content)) .is_none() { let group = make_group_from_date_cell(&_cell_data.into(), &setting_content); @@ -106,7 +95,7 @@ impl GroupCustomize for DateGroupController { let deleted_group = match _old_cell_data.and_then(|old_cell_data| { self .context - .get_group(&group_id(&old_cell_data.into(), &setting_content)) + .get_group(&get_date_group_id(&old_cell_data.into(), &setting_content)) }) { None => None, Some((_, group)) => { @@ -138,7 +127,7 @@ impl GroupCustomize for DateGroupController { let setting_content = self.context.get_setting_content(); self.context.iter_mut_status_groups(|group| { let mut changeset = GroupRowsNotificationPB::new(group.id.clone()); - if group.id == group_id(&cell_data.into(), &setting_content) { + if group.id == get_date_group_id(&cell_data.into(), &setting_content) { if !group.contains_row(&row_detail.row.id) { changeset .inserted_rows @@ -180,7 +169,7 @@ impl GroupCustomize for DateGroupController { let setting_content = self.context.get_setting_content(); let deleted_group = match self .context - .get_group(&group_id(cell_data, &setting_content)) + .get_group(&get_date_group_id(cell_data, &setting_content)) { Some((_, group)) if group.rows.len() == 1 => Some(group.clone()), _ => None, @@ -194,11 +183,7 @@ impl GroupCustomize for DateGroupController { (deleted_group, changesets) } - fn move_row( - &mut self, - _cell_data: &::CellProtobufType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.context.iter_mut_groups(|group| { if let Some(changeset) = move_group_row(group, &mut context) { @@ -211,13 +196,13 @@ impl GroupCustomize for DateGroupController { fn delete_group_when_move_row( &mut self, _row: &Row, - _cell_data: &::CellProtobufType, + cell_data: &::CellProtobufType, ) -> Option { let mut deleted_group = None; let setting_content = self.context.get_setting_content(); if let Some((_, group)) = self .context - .get_group(&group_id(&_cell_data.into(), &setting_content)) + .get_group(&get_date_group_id(&cell_data.into(), &setting_content)) { if group.rows.len() == 1 { deleted_group = Some(GroupPB::from(group.clone())); @@ -229,16 +214,12 @@ impl GroupCustomize for DateGroupController { deleted_group } - fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { + fn delete_group(&mut self, group_id: &str) -> FlowyResult> { self.context.delete_group(group_id)?; Ok(None) } -} -impl GroupController for DateGroupController { - fn did_update_field_type_option(&mut self, _field: &Field) {} - - fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { None => tracing::warn!("Can not find the group: {}", group_id), Some((_, _)) => { @@ -253,7 +234,7 @@ impl GroupController for DateGroupController { pub struct DateGroupBuilder(); #[async_trait] impl GroupsBuilder for DateGroupBuilder { - type Context = DateGroupContext; + type Context = DateGroupControllerContext; type GroupTypeOption = DateTypeOption; async fn build( @@ -265,39 +246,31 @@ impl GroupsBuilder for DateGroupBuilder { let cells = context.get_all_cells().await; // Generate the groups - let mut group_configs: Vec = cells + let mut groups: Vec = cells .into_iter() .flat_map(|value| value.into_date_field_cell_data()) .filter(|cell| cell.timestamp.is_some()) - .map(|cell| { - let group = make_group_from_date_cell(&cell, &context.get_setting_content()); - GeneratedGroupConfig { - filter_content: group.id.clone(), - group, - } - }) + .map(|cell| make_group_from_date_cell(&cell, &context.get_setting_content())) .collect(); - group_configs.sort_by(|a, b| a.filter_content.cmp(&b.filter_content)); + groups.sort_by(|a, b| a.id.cmp(&b.id)); let no_status_group = Some(make_no_status_group(field)); + GeneratedGroups { no_status_group, - group_configs, + groups, } } } fn make_group_from_date_cell(cell_data: &DateCellData, setting_content: &str) -> Group { - let group_id = group_id(cell_data, setting_content); - Group::new( - group_id.clone(), - group_name_from_id(&group_id, setting_content), - ) + let group_id = get_date_group_id(cell_data, setting_content); + Group::new(group_id) } const GROUP_ID_DATE_FORMAT: &str = "%Y/%m/%d"; -fn group_id(cell_data: &DateCellData, setting_content: &str) -> String { +fn get_date_group_id(cell_data: &DateCellData, setting_content: &str) -> String { let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default(); let date_time = date_time_from_timestamp(cell_data.timestamp); @@ -354,63 +327,6 @@ fn group_id(cell_data: &DateCellData, setting_content: &str) -> String { date.to_string() } -fn group_name_from_id(group_id: &str, setting_content: &str) -> String { - let config = DateGroupConfiguration::from_json(setting_content).unwrap_or_default(); - let date = NaiveDate::parse_from_str(group_id, GROUP_ID_DATE_FORMAT).unwrap(); - - let tmp; - match config.condition { - DateCondition::Day => { - tmp = format!("{} {}, {}", date.format("%b"), date.day(), date.year(),); - tmp - }, - DateCondition::Week => { - let begin_of_week = date - .checked_sub_days(Days::new(date.weekday().num_days_from_monday() as u64)) - .unwrap() - .format("%d"); - let end_of_week = date - .checked_add_days(Days::new(6 - date.weekday().num_days_from_monday() as u64)) - .unwrap() - .format("%d"); - - tmp = format!( - "Week of {} {}-{} {}", - date.format("%b"), - begin_of_week, - end_of_week, - date.year() - ); - tmp - }, - DateCondition::Month => { - tmp = format!("{} {}", date.format("%b"), date.year(),); - tmp - }, - DateCondition::Year => date.year().to_string(), - DateCondition::Relative => { - let now = date_time_from_timestamp(Some(timestamp())); - - let diff = date.signed_duration_since(now.date_naive()); - let result = match diff.num_days() { - 0 => "Today", - -1 => "Yesterday", - 1 => "Tomorrow", - -7 => "Last 7 days", - 2 => "Next 7 days", - -30 => "Last 30 days", - 8 => "Next 30 days", - _ => { - tmp = format!("{} {}", date.format("%b"), date.year(),); - &tmp - }, - }; - - result.to_string() - }, - } -} - fn date_time_from_timestamp(timestamp: Option) -> DateTime { match timestamp { Some(timestamp) => { @@ -423,24 +339,14 @@ fn date_time_from_timestamp(timestamp: Option) -> DateTime { } } -pub struct DateGroupOperationInterceptorImpl {} - -#[async_trait] -impl GroupOperationInterceptor for DateGroupOperationInterceptorImpl { - type GroupTypeOption = DateTypeOption; -} - #[cfg(test)] mod tests { - use std::vec; - use chrono::{offset, Days, Duration, NaiveDateTime}; - use crate::services::{ - field::{date_type_option::DateTypeOption, DateCellData}, - group::controller_impls::date_controller::{ - group_id, group_name_from_id, GROUP_ID_DATE_FORMAT, - }, + use crate::services::field::date_type_option::DateTypeOption; + use crate::services::field::DateCellData; + use crate::services::group::controller_impls::date_controller::{ + get_date_group_id, GROUP_ID_DATE_FORMAT, }; #[test] @@ -449,7 +355,6 @@ mod tests { cell_data: DateCellData, setting_content: String, exp_group_id: String, - exp_group_name: String, } let mar_14_2022 = NaiveDateTime::from_timestamp_opt(1647251762, 0).unwrap(); @@ -471,7 +376,6 @@ mod tests { cell_data: mar_14_2022_cd.clone(), setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), exp_group_id: "2022/03/01".to_string(), - exp_group_name: "Mar 2022".to_string(), }, GroupIDTest { cell_data: DateCellData { @@ -481,7 +385,6 @@ mod tests { }, setting_content: r#"{"condition": 0, "hide_empty": false}"#.to_string(), exp_group_id: today.format(GROUP_ID_DATE_FORMAT).to_string(), - exp_group_name: "Today".to_string(), }, GroupIDTest { cell_data: DateCellData { @@ -495,13 +398,11 @@ mod tests { .unwrap() .format(GROUP_ID_DATE_FORMAT) .to_string(), - exp_group_name: "Last 7 days".to_string(), }, GroupIDTest { cell_data: mar_14_2022_cd.clone(), setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), exp_group_id: "2022/03/14".to_string(), - exp_group_name: "Mar 14, 2022".to_string(), }, GroupIDTest { cell_data: DateCellData { @@ -516,19 +417,16 @@ mod tests { }, setting_content: r#"{"condition": 2, "hide_empty": false}"#.to_string(), exp_group_id: "2022/03/14".to_string(), - exp_group_name: "Week of Mar 14-20 2022".to_string(), }, GroupIDTest { cell_data: mar_14_2022_cd.clone(), setting_content: r#"{"condition": 3, "hide_empty": false}"#.to_string(), exp_group_id: "2022/03/01".to_string(), - exp_group_name: "Mar 2022".to_string(), }, GroupIDTest { cell_data: mar_14_2022_cd, setting_content: r#"{"condition": 4, "hide_empty": false}"#.to_string(), exp_group_id: "2022/01/01".to_string(), - exp_group_name: "2022".to_string(), }, GroupIDTest { cell_data: DateCellData { @@ -538,7 +436,6 @@ mod tests { }, setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), exp_group_id: "2023/06/02".to_string(), - exp_group_name: "".to_string(), }, GroupIDTest { cell_data: DateCellData { @@ -548,18 +445,12 @@ mod tests { }, setting_content: r#"{"condition": 1, "hide_empty": false}"#.to_string(), exp_group_id: "2023/06/03".to_string(), - exp_group_name: "".to_string(), }, ]; for (i, test) in tests.iter().enumerate() { - let group_id = group_id(&test.cell_data, &test.setting_content); + let group_id = get_date_group_id(&test.cell_data, &test.setting_content); assert_eq!(test.exp_group_id, group_id, "test {}", i); - - if !test.exp_group_name.is_empty() { - let group_name = group_name_from_id(&group_id, &test.setting_content); - assert_eq!(test.exp_group_name, group_name, "test {}", i); - } } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs index 021615b359..bcfd48bc09 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/default_controller.rs @@ -1,6 +1,5 @@ use std::sync::Arc; -use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{Cells, Row, RowDetail, RowId}; @@ -10,9 +9,11 @@ use crate::entities::{ GroupChangesPB, GroupPB, GroupRowsNotificationPB, InsertedGroupPB, InsertedRowPB, }; use crate::services::group::action::{ - DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupControllerOperation, + DidMoveGroupRowResult, DidUpdateGroupRowResult, GroupController, +}; +use crate::services::group::{ + GroupChangeset, GroupControllerDelegate, GroupData, MoveGroupRowContext, }; -use crate::services::group::{GroupChangesets, GroupController, GroupData, MoveGroupRowContext}; /// A [DefaultGroupController] is used to handle the group actions for the [FieldType] that doesn't /// implement its own group controller. The default group controller only contains one group, which @@ -21,29 +22,24 @@ use crate::services::group::{GroupChangesets, GroupController, GroupData, MoveGr pub struct DefaultGroupController { pub field_id: String, pub group: GroupData, + pub delegate: Arc, } const DEFAULT_GROUP_CONTROLLER: &str = "DefaultGroupController"; impl DefaultGroupController { - pub fn new(field: &Arc) -> Self { - let group = GroupData::new( - DEFAULT_GROUP_CONTROLLER.to_owned(), - field.id.clone(), - "".to_owned(), - "".to_owned(), - true, - ); + pub fn new(field: &Field, delegate: Arc) -> Self { + let group = GroupData::new(DEFAULT_GROUP_CONTROLLER.to_owned(), field.id.clone(), true); Self { field_id: field.id.clone(), group, + delegate, } } } -#[async_trait] -impl GroupControllerOperation for DefaultGroupController { - fn field_id(&self) -> &str { +impl GroupController for DefaultGroupController { + fn get_grouping_field_id(&self) -> &str { &self.field_id } @@ -78,12 +74,12 @@ impl GroupControllerOperation for DefaultGroupController { row_detail: &RowDetail, index: usize, ) -> Vec { - self.group.add_row(row_detail.clone()); + self.group.add_row((*row_detail).clone()); vec![GroupRowsNotificationPB::insert( self.group.id.clone(), vec![InsertedRowPB { - row_meta: row_detail.into(), + row_meta: (*row_detail).clone().into(), index: Some(index as i32), is_new: true, }], @@ -133,18 +129,12 @@ impl GroupControllerOperation for DefaultGroupController { Ok((vec![], None)) } - async fn apply_group_changeset( + fn apply_group_changeset( &mut self, - _changeset: &GroupChangesets, - ) -> FlowyResult<(Vec, TypeOptionData)> { - Ok((Vec::new(), TypeOptionData::default())) - } -} - -impl GroupController for DefaultGroupController { - fn did_update_field_type_option(&mut self, _field: &Field) { - // Do nothing + _changeset: &[GroupChangeset], + ) -> FlowyResult<(Vec, Option)> { + Ok((Vec::new(), None)) } - fn will_create_row(&mut self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} + fn will_create_row(&self, _cells: &mut Cells, _field: &Field, _group_id: &str) {} } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs index dfc7ce8ce9..cae19109f6 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/multi_select_controller.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; @@ -11,11 +11,11 @@ use crate::services::field::{ TypeOption, }; use crate::services::group::action::GroupCustomize; -use crate::services::group::controller::{BaseGroupController, GroupController}; +use crate::services::group::controller::BaseGroupController; use crate::services::group::{ add_or_remove_select_option_row, generate_select_option_groups, make_no_status_group, - move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset, GroupContext, - GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, + move_group_row, remove_select_option_row, GeneratedGroups, Group, GroupChangeset, + GroupControllerContext, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -23,14 +23,12 @@ pub struct MultiSelectGroupConfiguration { pub hide_empty: bool, } -pub type MultiSelectOptionGroupContext = GroupContext; +pub type MultiSelectGroupControllerContext = GroupControllerContext; // MultiSelect pub type MultiSelectGroupController = BaseGroupController< MultiSelectGroupConfiguration, - MultiSelectTypeOption, MultiSelectGroupBuilder, SelectOptionCellDataParser, - MultiSelectGroupOperationInterceptorImpl, >; impl GroupCustomize for MultiSelectGroupController { @@ -80,11 +78,7 @@ impl GroupCustomize for MultiSelectGroupController { (None, changesets) } - fn move_row( - &mut self, - _cell_data: &::CellProtobufType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.context.iter_mut_groups(|group| { if let Some(changeset) = move_group_row(group, &mut context) { @@ -94,85 +88,47 @@ impl GroupCustomize for MultiSelectGroupController { group_changeset } - fn generate_new_group( + fn create_group( &mut self, name: String, ) -> FlowyResult<(Option, Option)> { - let mut new_type_option = self.type_option.clone(); - let new_select_option = self.type_option.create_option(&name); + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + FlowyError::internal().with_context("Failed to get grouping field type option") + })?; + let new_select_option = new_type_option.create_option(&name); new_type_option.insert_option(new_select_option.clone()); - let new_group = Group::new(new_select_option.id, new_select_option.name); + let new_group = Group::new(new_select_option.id); let inserted_group_pb = self.context.add_new_group(new_group)?; Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } - fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { - if let Some(option_index) = self - .type_option + fn delete_group(&mut self, group_id: &str) -> FlowyResult> { + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + FlowyError::internal().with_context("Failed to get grouping field type option") + })?; + if let Some(option_index) = new_type_option .options .iter() .position(|option| option.id == group_id) { // Remove the option if the group is found - let mut new_type_option = self.type_option.clone(); new_type_option.options.remove(option_index); Ok(Some(new_type_option.into())) } else { Ok(None) } } -} -impl GroupController for MultiSelectGroupController { - fn did_update_field_type_option(&mut self, _field: &Field) {} - - fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { - match self.context.get_group(group_id) { - None => tracing::warn!("Can not find the group: {}", group_id), - Some((_, group)) => { - let cell = insert_select_option_cell(vec![group.id.clone()], field); - cells.insert(field.id.clone(), cell); - }, - } - } -} - -pub struct MultiSelectGroupBuilder; -#[async_trait] -impl GroupsBuilder for MultiSelectGroupBuilder { - type Context = MultiSelectOptionGroupContext; - type GroupTypeOption = MultiSelectTypeOption; - - async fn build( - field: &Field, - _context: &Self::Context, - type_option: &Self::GroupTypeOption, - ) -> GeneratedGroups { - let group_configs = generate_select_option_groups(&field.id, &type_option.options); - GeneratedGroups { - no_status_group: Some(make_no_status_group(field)), - group_configs, - } - } -} - -pub struct MultiSelectGroupOperationInterceptorImpl; - -#[async_trait] -impl GroupOperationInterceptor for MultiSelectGroupOperationInterceptorImpl { - type GroupTypeOption = MultiSelectTypeOption; - - #[tracing::instrument(level = "trace", skip_all)] - async fn type_option_from_group_changeset( - &self, + fn update_type_option_when_update_group( + &mut self, changeset: &GroupChangeset, type_option: &Self::GroupTypeOption, - _view_id: &str, - ) -> Option { + ) -> Option { if let Some(name) = &changeset.name { let mut new_type_option = type_option.clone(); + let select_option = type_option .options .iter() @@ -184,9 +140,40 @@ impl GroupOperationInterceptor for MultiSelectGroupOperationInterceptorImpl { ..select_option.to_owned() }; new_type_option.insert_option(new_select_option); - return Some(new_type_option.into()); - } - None + Some(new_type_option) + } else { + None + } + } + + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { + match self.context.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_index, group)) => { + let cell = insert_select_option_cell(vec![group.id.clone()], field); + cells.insert(field.id.clone(), cell); + }, + } + } +} + +pub struct MultiSelectGroupBuilder; +#[async_trait] +impl GroupsBuilder for MultiSelectGroupBuilder { + type Context = MultiSelectGroupControllerContext; + type GroupTypeOption = MultiSelectTypeOption; + + async fn build( + field: &Field, + _context: &Self::Context, + type_option: &Self::GroupTypeOption, + ) -> GeneratedGroups { + let groups = generate_select_option_groups(&field.id, &type_option.options); + + GeneratedGroups { + no_status_group: Some(make_no_status_group(field)), + groups, + } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs index 6986ad0e83..d26ef50b70 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/single_select_controller.rs @@ -1,7 +1,7 @@ use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use serde::{Deserialize, Serialize}; use crate::entities::{FieldType, GroupPB, GroupRowsNotificationPB, InsertedGroupPB}; @@ -11,12 +11,11 @@ use crate::services::field::{ TypeOption, }; use crate::services::group::action::GroupCustomize; -use crate::services::group::controller::{BaseGroupController, GroupController}; +use crate::services::group::controller::BaseGroupController; use crate::services::group::controller_impls::select_option_controller::util::*; -use crate::services::group::entities::GroupData; use crate::services::group::{ - make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupContext, - GroupOperationInterceptor, GroupsBuilder, MoveGroupRowContext, + make_no_status_group, GeneratedGroups, Group, GroupChangeset, GroupControllerContext, + GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -24,19 +23,19 @@ pub struct SingleSelectGroupConfiguration { pub hide_empty: bool, } -pub type SingleSelectOptionGroupContext = GroupContext; +pub type SingleSelectGroupControllerContext = + GroupControllerContext; // SingleSelect pub type SingleSelectGroupController = BaseGroupController< SingleSelectGroupConfiguration, - SingleSelectTypeOption, SingleSelectGroupBuilder, SelectOptionCellDataParser, - SingleSelectGroupOperationInterceptorImpl, >; impl GroupCustomize for SingleSelectGroupController { type GroupTypeOption = SingleSelectTypeOption; + fn can_group( &self, content: &str, @@ -81,11 +80,7 @@ impl GroupCustomize for SingleSelectGroupController { (None, changesets) } - fn move_row( - &mut self, - _cell_data: &::CellProtobufType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.context.iter_mut_groups(|group| { if let Some(changeset) = move_group_row(group, &mut context) { @@ -95,87 +90,47 @@ impl GroupCustomize for SingleSelectGroupController { group_changeset } - fn generate_new_group( + fn create_group( &mut self, name: String, ) -> FlowyResult<(Option, Option)> { - let mut new_type_option = self.type_option.clone(); - let new_select_option = self.type_option.create_option(&name); + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + FlowyError::internal().with_context("Failed to get grouping field type option") + })?; + let new_select_option = new_type_option.create_option(&name); new_type_option.insert_option(new_select_option.clone()); - let new_group = Group::new(new_select_option.id, new_select_option.name); + let new_group = Group::new(new_select_option.id); let inserted_group_pb = self.context.add_new_group(new_group)?; Ok((Some(new_type_option.into()), Some(inserted_group_pb))) } - fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { - if let Some(option_index) = self - .type_option + fn delete_group(&mut self, group_id: &str) -> FlowyResult> { + let mut new_type_option = self.get_grouping_field_type_option().ok_or_else(|| { + FlowyError::internal().with_context("Failed to get grouping field type option") + })?; + if let Some(option_index) = new_type_option .options .iter() .position(|option| option.id == group_id) { // Remove the option if the group is found - let mut new_type_option = self.type_option.clone(); new_type_option.options.remove(option_index); Ok(Some(new_type_option.into())) } else { - // Return None if no matching group is found Ok(None) } } -} -impl GroupController for SingleSelectGroupController { - fn did_update_field_type_option(&mut self, _field: &Field) {} - - fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { - let group: Option<&mut GroupData> = self.context.get_mut_group(group_id); - match group { - None => {}, - Some(group) => { - let cell = insert_select_option_cell(vec![group.id.clone()], field); - cells.insert(field.id.clone(), cell); - }, - } - } -} - -pub struct SingleSelectGroupBuilder(); -#[async_trait] -impl GroupsBuilder for SingleSelectGroupBuilder { - type Context = SingleSelectOptionGroupContext; - type GroupTypeOption = SingleSelectTypeOption; - async fn build( - field: &Field, - _context: &Self::Context, - type_option: &Self::GroupTypeOption, - ) -> GeneratedGroups { - let group_configs = generate_select_option_groups(&field.id, &type_option.options); - - GeneratedGroups { - no_status_group: Some(make_no_status_group(field)), - group_configs, - } - } -} - -pub struct SingleSelectGroupOperationInterceptorImpl; - -#[async_trait] -impl GroupOperationInterceptor for SingleSelectGroupOperationInterceptorImpl { - type GroupTypeOption = SingleSelectTypeOption; - - #[tracing::instrument(level = "trace", skip_all)] - async fn type_option_from_group_changeset( - &self, + fn update_type_option_when_update_group( + &mut self, changeset: &GroupChangeset, type_option: &Self::GroupTypeOption, - _view_id: &str, - ) -> Option { + ) -> Option { if let Some(name) = &changeset.name { let mut new_type_option = type_option.clone(); + let select_option = type_option .options .iter() @@ -187,9 +142,39 @@ impl GroupOperationInterceptor for SingleSelectGroupOperationInterceptorImpl { ..select_option.to_owned() }; new_type_option.insert_option(new_select_option); - return Some(new_type_option.into()); - } - None + Some(new_type_option) + } else { + None + } + } + + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { + match self.context.get_group(group_id) { + None => tracing::warn!("Can not find the group: {}", group_id), + Some((_index, group)) => { + let cell = insert_select_option_cell(vec![group.id.clone()], field); + cells.insert(field.id.clone(), cell); + }, + } + } +} + +pub struct SingleSelectGroupBuilder(); +#[async_trait] +impl GroupsBuilder for SingleSelectGroupBuilder { + type Context = SingleSelectGroupControllerContext; + type GroupTypeOption = SingleSelectTypeOption; + async fn build( + field: &Field, + _context: &Self::Context, + type_option: &Self::GroupTypeOption, + ) -> GeneratedGroups { + let groups = generate_select_option_groups(&field.id, &type_option.options); + + GeneratedGroups { + no_status_group: Some(make_no_status_group(field)), + groups, + } } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs index b8a144b594..01bd4cdc0d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/select_option_controller/util.rs @@ -9,7 +9,7 @@ use crate::services::cell::{ insert_checkbox_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, }; use crate::services::field::{SelectOption, SelectOptionIds, CHECK}; -use crate::services::group::{GeneratedGroupConfig, Group, GroupData, MoveGroupRowContext}; +use crate::services::group::{Group, GroupData, MoveGroupRowContext}; pub fn add_or_remove_select_option_row( group: &mut GroupData, @@ -186,16 +186,10 @@ pub fn make_inserted_cell(group_id: &str, field: &Field) -> Option { } } -pub fn generate_select_option_groups( - _field_id: &str, - options: &[SelectOption], -) -> Vec { +pub fn generate_select_option_groups(_field_id: &str, options: &[SelectOption]) -> Vec { let groups = options .iter() - .map(|option| GeneratedGroupConfig { - group: Group::new(option.id.clone(), option.name.clone()), - filter_content: option.id.clone(), - }) + .map(|option| Group::new(option.id.clone())) .collect(); groups diff --git a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs index a687acce5d..195bae405c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/controller_impls/url_controller.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use async_trait::async_trait; use collab_database::fields::{Field, TypeOptionData}; use collab_database::rows::{new_cell_builder, Cell, Cells, Row, RowDetail}; @@ -13,11 +11,10 @@ use crate::entities::{ use crate::services::cell::insert_url_cell; use crate::services::field::{TypeOption, URLCellData, URLCellDataParser, URLTypeOption}; use crate::services::group::action::GroupCustomize; -use crate::services::group::configuration::GroupContext; -use crate::services::group::controller::{BaseGroupController, GroupController}; +use crate::services::group::configuration::GroupControllerContext; +use crate::services::group::controller::BaseGroupController; use crate::services::group::{ - make_no_status_group, move_group_row, GeneratedGroupConfig, GeneratedGroups, Group, - GroupOperationInterceptor, GroupTypeOptionCellOperation, GroupsBuilder, MoveGroupRowContext, + make_no_status_group, move_group_row, GeneratedGroups, Group, GroupsBuilder, MoveGroupRowContext, }; #[derive(Default, Serialize, Deserialize)] @@ -25,15 +22,10 @@ pub struct URLGroupConfiguration { pub hide_empty: bool, } -pub type URLGroupController = BaseGroupController< - URLGroupConfiguration, - URLTypeOption, - URLGroupGenerator, - URLCellDataParser, - URLGroupOperationInterceptorImpl, ->; +pub type URLGroupController = + BaseGroupController; -pub type URLGroupContext = GroupContext; +pub type URLGroupControllerContext = GroupControllerContext; impl GroupCustomize for URLGroupController { type GroupTypeOption = URLTypeOption; @@ -64,7 +56,7 @@ impl GroupCustomize for URLGroupController { let mut inserted_group = None; if self.context.get_group(&_cell_data.url).is_none() { let cell_data: URLCellData = _cell_data.clone().into(); - let group = make_group_from_url_cell(&cell_data); + let group = Group::new(cell_data.data); let mut new_group = self.context.add_new_group(group)?; new_group.group.rows.push(RowMetaPB::from(_row_detail)); inserted_group = Some(new_group); @@ -155,11 +147,7 @@ impl GroupCustomize for URLGroupController { (deleted_group, changesets) } - fn move_row( - &mut self, - _cell_data: &::CellProtobufType, - mut context: MoveGroupRowContext, - ) -> Vec { + fn move_row(&mut self, mut context: MoveGroupRowContext) -> Vec { let mut group_changeset = vec![]; self.context.iter_mut_groups(|group| { if let Some(changeset) = move_group_row(group, &mut context) { @@ -168,13 +156,14 @@ impl GroupCustomize for URLGroupController { }); group_changeset } + fn delete_group_when_move_row( &mut self, _row: &Row, - _cell_data: &::CellProtobufType, + cell_data: &::CellProtobufType, ) -> Option { let mut deleted_group = None; - if let Some((_, group)) = self.context.get_group(&_cell_data.content) { + if let Some((_index, group)) = self.context.get_group(&cell_data.content) { if group.rows.len() == 1 { deleted_group = Some(GroupPB::from(group.clone())); } @@ -185,16 +174,12 @@ impl GroupCustomize for URLGroupController { deleted_group } - fn delete_group_custom(&mut self, group_id: &str) -> FlowyResult> { + fn delete_group(&mut self, group_id: &str) -> FlowyResult> { self.context.delete_group(group_id)?; Ok(None) } -} -impl GroupController for URLGroupController { - fn did_update_field_type_option(&mut self, _field: &Field) {} - - fn will_create_row(&mut self, cells: &mut Cells, field: &Field, group_id: &str) { + fn will_create_row(&self, cells: &mut Cells, field: &Field, group_id: &str) { match self.context.get_group(group_id) { None => tracing::warn!("Can not find the group: {}", group_id), Some((_, group)) => { @@ -208,7 +193,7 @@ impl GroupController for URLGroupController { pub struct URLGroupGenerator(); #[async_trait] impl GroupsBuilder for URLGroupGenerator { - type Context = URLGroupContext; + type Context = URLGroupControllerContext; type GroupTypeOption = URLTypeOption; async fn build( @@ -220,36 +205,18 @@ impl GroupsBuilder for URLGroupGenerator { let cells = context.get_all_cells().await; // Generate the groups - let group_configs = cells + let groups = cells .into_iter() .flat_map(|value| value.into_url_field_cell_data()) .filter(|cell| !cell.data.is_empty()) - .map(|cell| GeneratedGroupConfig { - group: make_group_from_url_cell(&cell), - filter_content: cell.data, - }) + .map(|cell| Group::new(cell.data.clone())) .collect(); let no_status_group = Some(make_no_status_group(field)); + GeneratedGroups { no_status_group, - group_configs, + groups, } } } - -fn make_group_from_url_cell(cell: &URLCellData) -> Group { - let group_id = cell.data.clone(); - let group_name = cell.data.clone(); - Group::new(group_id, group_name) -} - -pub struct URLGroupOperationInterceptorImpl { - #[allow(dead_code)] - pub(crate) cell_writer: Arc, -} - -#[async_trait::async_trait] -impl GroupOperationInterceptor for URLGroupOperationInterceptorImpl { - type GroupTypeOption = URLTypeOption; -} diff --git a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs index 253c12bac9..12692fd812 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/entities.rs @@ -14,16 +14,6 @@ pub struct GroupSetting { pub content: String, } -pub struct GroupChangesets { - pub changesets: Vec, -} - -impl From> for GroupChangesets { - fn from(changesets: Vec) -> Self { - Self { changesets } - } -} - #[derive(Clone, Default, Debug)] pub struct GroupChangeset { pub group_id: String, @@ -92,7 +82,6 @@ impl From for GroupSettingMap { #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct Group { pub id: String, - pub name: String, #[serde(default = "GROUP_VISIBILITY")] pub visible: bool, } @@ -104,9 +93,8 @@ impl TryFrom for Group { match value.get_str_value("id") { None => bail!("Invalid group data"), Some(id) => { - let name = value.get_str_value("name").unwrap_or_default(); let visible = value.get_bool_value("visible").unwrap_or_default(); - Ok(Self { id, name, visible }) + Ok(Self { id, visible }) }, } } @@ -116,7 +104,6 @@ impl From for GroupMap { fn from(group: Group) -> Self { GroupMapBuilder::new() .insert_str_value("id", group.id) - .insert_str_value("name", group.name) .insert_bool_value("visible", group.visible) .build() } @@ -125,12 +112,8 @@ impl From for GroupMap { const GROUP_VISIBILITY: fn() -> bool = || true; impl Group { - pub fn new(id: String, name: String) -> Self { - Self { - id, - name, - visible: true, - } + pub fn new(id: String) -> Self { + Self { id, visible: true } } } @@ -138,32 +121,20 @@ impl Group { pub struct GroupData { pub id: String, pub field_id: String, - pub name: String, pub is_default: bool, pub is_visible: bool, pub(crate) rows: Vec, - - /// [filter_content] is used to determine which group the cell belongs to. - pub filter_content: String, } impl GroupData { - pub fn new( - id: String, - field_id: String, - name: String, - filter_content: String, - is_visible: bool, - ) -> Self { + pub fn new(id: String, field_id: String, is_visible: bool) -> Self { let is_default = id == field_id; Self { id, field_id, is_default, is_visible, - name, rows: vec![], - filter_content, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs index c221c7fdaf..8eb677ed26 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/group_builder.rs @@ -4,21 +4,17 @@ use std::sync::Arc; use async_trait::async_trait; use collab_database::fields::Field; use collab_database::rows::{Cell, RowDetail, RowId}; -use collab_database::views::DatabaseLayout; use flowy_error::FlowyResult; use crate::entities::FieldType; use crate::services::field::TypeOption; use crate::services::group::{ - CheckboxGroupContext, CheckboxGroupController, CheckboxGroupOperationInterceptorImpl, - DateGroupContext, DateGroupController, DateGroupOperationInterceptorImpl, DefaultGroupController, - Group, GroupController, GroupSetting, GroupSettingReader, GroupSettingWriter, - GroupTypeOptionCellOperation, MultiSelectGroupController, - MultiSelectGroupOperationInterceptorImpl, MultiSelectOptionGroupContext, - SingleSelectGroupController, SingleSelectGroupOperationInterceptorImpl, - SingleSelectOptionGroupContext, URLGroupContext, URLGroupController, - URLGroupOperationInterceptorImpl, + CheckboxGroupController, CheckboxGroupControllerContext, DateGroupController, + DateGroupControllerContext, DefaultGroupController, Group, GroupContextDelegate, GroupController, + GroupControllerDelegate, GroupSetting, MultiSelectGroupController, + MultiSelectGroupControllerContext, SingleSelectGroupController, + SingleSelectGroupControllerContext, URLGroupController, URLGroupControllerContext, }; /// The [GroupsBuilder] trait is used to generate the groups for different [FieldType] @@ -36,12 +32,7 @@ pub trait GroupsBuilder: Send + Sync + 'static { pub struct GeneratedGroups { pub no_status_group: Option, - pub group_configs: Vec, -} - -pub struct GeneratedGroupConfig { - pub group: Group, - pub filter_content: String, + pub groups: Vec, } pub struct MoveGroupRowContext<'a> { @@ -94,138 +85,97 @@ impl RowChangeset { fields(grouping_field_id=%grouping_field.id, grouping_field_type) err )] -pub async fn make_group_controller( - view_id: String, - grouping_field: Arc, - row_details: Vec>, - setting_reader: R, - setting_writer: W, - type_option_cell_writer: TW, +pub async fn make_group_controller( + view_id: &str, + grouping_field: Field, + delegate: D, ) -> FlowyResult> where - R: GroupSettingReader, - W: GroupSettingWriter, - TW: GroupTypeOptionCellOperation, + D: GroupContextDelegate + GroupControllerDelegate, { let grouping_field_type = FieldType::from(grouping_field.field_type); tracing::Span::current().record("grouping_field", &grouping_field_type.default_name()); let mut group_controller: Box; - let configuration_reader = Arc::new(setting_reader); - let configuration_writer = Arc::new(setting_writer); - let type_option_cell_writer = Arc::new(type_option_cell_writer); + let delegate = Arc::new(delegate); match grouping_field_type { FieldType::SingleSelect => { - let configuration = SingleSelectOptionGroupContext::new( - view_id, + let configuration = SingleSelectGroupControllerContext::new( + view_id.to_string(), grouping_field.clone(), - configuration_reader, - configuration_writer, + delegate.clone(), ) .await?; - let operation_interceptor = SingleSelectGroupOperationInterceptorImpl; let controller = - SingleSelectGroupController::new(&grouping_field, configuration, operation_interceptor) - .await?; + SingleSelectGroupController::new(&grouping_field, configuration, delegate.clone()).await?; group_controller = Box::new(controller); }, FieldType::MultiSelect => { - let configuration = MultiSelectOptionGroupContext::new( - view_id, + let configuration = MultiSelectGroupControllerContext::new( + view_id.to_string(), grouping_field.clone(), - configuration_reader, - configuration_writer, + delegate.clone(), ) .await?; - let operation_interceptor = MultiSelectGroupOperationInterceptorImpl; let controller = - MultiSelectGroupController::new(&grouping_field, configuration, operation_interceptor) - .await?; + MultiSelectGroupController::new(&grouping_field, configuration, delegate.clone()).await?; group_controller = Box::new(controller); }, FieldType::Checkbox => { - let configuration = CheckboxGroupContext::new( - view_id, + let configuration = CheckboxGroupControllerContext::new( + view_id.to_string(), grouping_field.clone(), - configuration_reader, - configuration_writer, + delegate.clone(), ) .await?; - let operation_interceptor = CheckboxGroupOperationInterceptorImpl {}; let controller = - CheckboxGroupController::new(&grouping_field, configuration, operation_interceptor).await?; + CheckboxGroupController::new(&grouping_field, configuration, delegate.clone()).await?; group_controller = Box::new(controller); }, FieldType::URL => { - let configuration = URLGroupContext::new( - view_id, + let configuration = URLGroupControllerContext::new( + view_id.to_string(), grouping_field.clone(), - configuration_reader, - configuration_writer, + delegate.clone(), ) .await?; - let operation_interceptor = URLGroupOperationInterceptorImpl { - cell_writer: type_option_cell_writer, - }; let controller = - URLGroupController::new(&grouping_field, configuration, operation_interceptor).await?; + URLGroupController::new(&grouping_field, configuration, delegate.clone()).await?; group_controller = Box::new(controller); }, FieldType::DateTime => { - let configuration = DateGroupContext::new( - view_id, + let configuration = DateGroupControllerContext::new( + view_id.to_string(), grouping_field.clone(), - configuration_reader, - configuration_writer, + delegate.clone(), ) .await?; - let operation_interceptor = DateGroupOperationInterceptorImpl {}; let controller = - DateGroupController::new(&grouping_field, configuration, operation_interceptor).await?; + DateGroupController::new(&grouping_field, configuration, delegate.clone()).await?; group_controller = Box::new(controller); }, _ => { - group_controller = Box::new(DefaultGroupController::new(&grouping_field)); + group_controller = Box::new(DefaultGroupController::new( + &grouping_field, + delegate.clone(), + )); }, } // Separates the rows into different groups + let row_details = delegate.get_all_rows(view_id).await; + let rows = row_details .iter() .map(|row| row.as_ref()) - .collect::>(); + .collect::>(); + group_controller.fill_groups(rows.as_slice(), &grouping_field)?; + Ok(group_controller) } -#[tracing::instrument(level = "debug", skip_all)] -pub fn find_new_grouping_field( - fields: &[Arc], - _layout: &DatabaseLayout, -) -> Option> { - let mut groupable_field_revs = fields - .iter() - .flat_map(|field_rev| { - let field_type = FieldType::from(field_rev.field_type); - match field_type.can_be_group() { - true => Some(field_rev.clone()), - false => None, - } - }) - .collect::>>(); - - if groupable_field_revs.is_empty() { - // If there is not groupable fields then we use the primary field. - fields - .iter() - .find(|field_rev| field_rev.is_primary) - .cloned() - } else { - Some(groupable_field_revs.remove(0)) - } -} - /// Returns a `default` group configuration for the [Field] /// /// # Arguments @@ -240,7 +190,6 @@ pub fn default_group_setting(field: &Field) -> GroupSetting { pub fn make_no_status_group(field: &Field) -> Group { Group { id: field.id.clone(), - name: format!("No {}", field.name), visible: true, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs index c9f9e91b65..c2ac8300b4 100644 --- a/frontend/rust-lib/flowy-database2/src/services/group/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/group/mod.rs @@ -5,6 +5,7 @@ mod controller_impls; mod entities; mod group_builder; +pub(crate) use action::GroupController; pub(crate) use configuration::*; pub(crate) use controller::*; pub(crate) use controller_impls::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs index 0c968dd353..4a9f09e63c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -21,16 +21,16 @@ use crate::services::field::{ default_order, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellExt, }; use crate::services::sort::{ - InsertSortedRowResult, ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, - SortCondition, + InsertRowResult, ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition, }; pub trait SortDelegate: Send + Sync { fn get_sort(&self, view_id: &str, sort_id: &str) -> Fut>>; /// Returns all the rows after applying grid's filter fn get_rows(&self, view_id: &str) -> Fut>>; + fn filter_row(&self, row_detail: &RowDetail) -> Fut; fn get_field(&self, field_id: &str) -> Option; - fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>>; + fn get_fields(&self, view_id: &str, field_ids: Option>) -> Fut>; } pub struct SortController { @@ -94,14 +94,27 @@ impl SortController { } } - pub async fn did_create_row(&self, row_id: RowId) { + pub async fn did_create_row(&self, preliminary_index: usize, row_detail: &RowDetail) { + if !self.delegate.filter_row(row_detail).await { + return; + } + if !self.sorts.is_empty() { self .gen_task( - SortEvent::NewRowInserted(row_id), + SortEvent::NewRowInserted(row_detail.clone()), QualityOfService::Background, ) .await; + } else { + let result = InsertRowResult { + view_id: self.view_id.clone(), + row: row_detail.clone(), + index: preliminary_index, + }; + let _ = self + .notifier + .send(DatabaseViewChanged::InsertRowNotification(result)); } } @@ -117,6 +130,7 @@ impl SortController { pub async fn process(&mut self, predicate: &str) -> FlowyResult<()> { let event_type = SortEvent::from_str(predicate).unwrap(); let mut row_details = self.delegate.get_rows(&self.view_id).await; + match event_type { SortEvent::SortDidChanged | SortEvent::DeleteAllSorts => { self.sort_rows(&mut row_details).await; @@ -161,22 +175,20 @@ impl SortController { _ => tracing::trace!("The row index cache is outdated"), } }, - SortEvent::NewRowInserted(row_id) => { + SortEvent::NewRowInserted(row_detail) => { self.sort_rows(&mut row_details).await; - let row_index = self.row_index_cache.get(&row_id).cloned(); + let row_index = self.row_index_cache.get(&row_detail.row.id).cloned(); match row_index { Some(row_index) => { - let notification = InsertSortedRowResult { - row_id: row_id.clone(), + let notification = InsertRowResult { view_id: self.view_id.clone(), + row: row_detail.clone(), index: row_index, }; - self.row_index_cache.insert(row_id, row_index); + self.row_index_cache.insert(row_detail.row.id, row_index); let _ = self .notifier - .send(DatabaseViewChanged::InsertSortedRowNotification( - notification, - )); + .send(DatabaseViewChanged::InsertRowNotification(notification)); }, _ => tracing::trace!("The row index cache is outdated"), } @@ -290,7 +302,7 @@ fn cmp_row( left: &Row, right: &Row, sort: &Arc, - fields: &[Arc], + fields: &[Field], cell_data_cache: &CellCache, ) -> Ordering { match fields @@ -335,18 +347,16 @@ fn cmp_row( fn cmp_cell( left_cell: Option<&Cell>, right_cell: Option<&Cell>, - field: &Arc, + field: &Field, field_type: FieldType, cell_data_cache: &CellCache, sort_condition: SortCondition, ) -> Ordering { - match TypeOptionCellExt::new_with_cell_data_cache(field.as_ref(), Some(cell_data_cache.clone())) + match TypeOptionCellExt::new(field, Some(cell_data_cache.clone())) .get_type_option_cell_data_handler(&field_type) { None => default_order(), - Some(handler) => { - handler.handle_cell_compare(left_cell, right_cell, field.as_ref(), sort_condition) - }, + Some(handler) => handler.handle_cell_compare(left_cell, right_cell, field, sort_condition), } } @@ -354,7 +364,7 @@ fn cmp_cell( enum SortEvent { SortDidChanged, RowDidChanged(RowId), - NewRowInserted(RowId), + NewRowInserted(RowDetail), DeleteAllSorts, } diff --git a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs index 66bfea4f3f..9f9d37d4fb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use anyhow::bail; use collab::core::any_map::AnyMapExtension; -use collab_database::rows::RowId; +use collab_database::rows::{RowDetail, RowId}; use collab_database::views::{SortMap, SortMapBuilder}; #[derive(Debug, Clone)] @@ -107,9 +107,9 @@ pub struct ReorderSingleRowResult { } #[derive(Clone)] -pub struct InsertSortedRowResult { +pub struct InsertRowResult { pub view_id: String, - pub row_id: RowId, + pub row: RowDetail, pub index: usize, } diff --git a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs index c253422242..72b62b55df 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/block_test/script.rs @@ -1,7 +1,6 @@ -use collab_database::database::gen_row_id; use collab_database::rows::RowId; -use lib_infra::util::timestamp; +use flowy_database2::entities::CreateRowPayloadPB; use crate::database::database_editor::DatabaseEditorTest; @@ -30,17 +29,11 @@ impl DatabaseRowTest { pub async fn run_script(&mut self, script: RowScript) { match script { RowScript::CreateEmptyRow => { - let params = collab_database::rows::CreateRowParams { - id: gen_row_id(), - timestamp: timestamp(), + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), ..Default::default() }; - let row_detail = self - .editor - .create_row(&self.view_id, None, params) - .await - .unwrap() - .unwrap(); + let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); self .row_by_row_id .insert(row_detail.row.id.to_string(), row_detail.into()); diff --git a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs index 8e4af7073f..cccaba68fe 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/database_editor.rs @@ -155,12 +155,13 @@ impl DatabaseEditorTest { type_option.options } - pub fn get_single_select_type_option(&self, field_id: &str) -> SingleSelectTypeOption { + pub fn get_single_select_type_option(&self, field_id: &str) -> Vec { let field_type = FieldType::SingleSelect; let field = self.get_field(field_id, field_type); - field + let type_option = field .get_type_option::(field_type) - .unwrap() + .unwrap(); + type_option.options } #[allow(dead_code)] diff --git a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs index bc54883697..3ec982f461 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/field_test/test.rs @@ -1,7 +1,7 @@ use collab_database::database::gen_option_id; use flowy_database2::entities::{FieldChangesetParams, FieldType}; -use flowy_database2::services::field::{SelectOption, CHECK, UNCHECK}; +use flowy_database2::services::field::{SelectOption, SingleSelectTypeOption, CHECK, UNCHECK}; use crate::database::field_test::script::DatabaseFieldTest; use crate::database::field_test::script::FieldScript::*; @@ -104,16 +104,16 @@ async fn grid_switch_from_select_option_to_checkbox_test() { let field = test.get_first_field(FieldType::SingleSelect); // Update the type option data of single select option - let mut single_select_type_option = test.get_single_select_type_option(&field.id); - single_select_type_option.options.clear(); + let mut options = test.get_single_select_type_option(&field.id); + options.clear(); // Add a new option with name CHECK - single_select_type_option.options.push(SelectOption { + options.push(SelectOption { id: gen_option_id(), name: CHECK.to_string(), color: Default::default(), }); // Add a new option with name UNCHECK - single_select_type_option.options.push(SelectOption { + options.push(SelectOption { id: gen_option_id(), name: UNCHECK.to_string(), color: Default::default(), @@ -122,7 +122,11 @@ async fn grid_switch_from_select_option_to_checkbox_test() { let scripts = vec![ UpdateTypeOption { field_id: field.id.clone(), - type_option: single_select_type_option.into(), + type_option: SingleSelectTypeOption { + options, + disable_color: false, + } + .into(), }, SwitchToField { field_id: field.id.clone(), @@ -159,16 +163,10 @@ async fn grid_switch_from_checkbox_to_select_option_test() { ]; test.run_scripts(scripts).await; - let single_select_type_option = test.get_single_select_type_option(&checkbox_field.id); - assert_eq!(single_select_type_option.options.len(), 2); - assert!(single_select_type_option - .options - .iter() - .any(|option| option.name == UNCHECK)); - assert!(single_select_type_option - .options - .iter() - .any(|option| option.name == CHECK)); + let options = test.get_single_select_type_option(&checkbox_field.id); + assert_eq!(options.len(), 2); + assert!(options.iter().any(|option| option.name == UNCHECK)); + assert!(options.iter().any(|option| option.name == CHECK)); } // Test when switching the current field from Multi-select to Text test diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs new file mode 100644 index 0000000000..107e588fed --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/advanced_filter_test.rs @@ -0,0 +1,314 @@ +use bytes::Bytes; +use flowy_database2::entities::{ + CheckboxFilterConditionPB, CheckboxFilterPB, DateFilterConditionPB, DateFilterPB, FieldType, + FilterDataPB, FilterPB, FilterType, NumberFilterConditionPB, NumberFilterPB, +}; +use lib_infra::box_any::BoxAny; +use protobuf::ProtobufError; +use std::convert::TryInto; + +use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged, FilterScript::*}; + +/// Create a single advanced filter: +/// +/// 1. Add an OR filter +/// 2. Add a Checkbox and an AND filter to its children +/// 3. Add a DateTime and a Number filter to the AND filter's children +/// +#[tokio::test] +async fn create_advanced_filter_test() { + let mut test = DatabaseFilterTest::new().await; + + let create_checkbox_filter = || -> CheckboxFilterPB { + CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + } + }; + + let create_date_filter = || -> DateFilterPB { + DateFilterPB { + condition: DateFilterConditionPB::DateAfter, + timestamp: Some(1651366800), + ..Default::default() + } + }; + + let create_number_filter = || -> NumberFilterPB { + NumberFilterPB { + condition: NumberFilterConditionPB::NumberIsNotEmpty, + content: "".to_string(), + } + }; + + let scripts = vec![ + CreateOrFilter { + parent_filter_id: None, + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![], + data: None, + }], + }, + ]; + test.run_scripts(scripts).await; + // OR + + let or_filter = test.get_filter(FilterType::Or, None).await.unwrap(); + + let checkbox_filter_bytes: Result = create_checkbox_filter().try_into(); + let checkbox_filter_bytes = checkbox_filter_bytes.unwrap().to_vec(); + + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(or_filter.id.clone()), + field_type: FieldType::Checkbox, + data: BoxAny::new(create_checkbox_filter()), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 4, + }), + }, + CreateAndFilter { + parent_filter_id: Some(or_filter.id), + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Checkbox, + data: checkbox_filter_bytes.clone(), + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::And, + children: vec![], + data: None, + }, + ], + data: None, + }], + }, + AssertNumberOfVisibleRows { expected: 3 }, + ]; + test.run_scripts(scripts).await; + // IS_CHECK OR AND + + let and_filter = test.get_filter(FilterType::And, None).await.unwrap(); + + let date_filter_bytes: Result = create_date_filter().try_into(); + let date_filter_bytes = date_filter_bytes.unwrap().to_vec(); + let number_filter_bytes: Result = create_number_filter().try_into(); + let number_filter_bytes = number_filter_bytes.unwrap().to_vec(); + + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(and_filter.id.clone()), + field_type: FieldType::DateTime, + data: BoxAny::new(create_date_filter()), + changed: None, + }, + CreateDataFilter { + parent_filter_id: Some(and_filter.id), + field_type: FieldType::Number, + data: BoxAny::new(create_number_filter()), + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Checkbox, + data: checkbox_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::And, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::DateTime, + data: date_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Number, + data: number_filter_bytes, + }), + }, + ], + data: None, + }, + ], + data: None, + }], + }, + AssertNumberOfVisibleRows { expected: 4 }, + ]; + test.run_scripts(scripts).await; + // IS_CHECK OR (DATE > 1651366800 AND NUMBER NOT EMPTY) +} + +/// Create the same advanced filter single advanced filter: +/// +/// 1. Add an OR filter +/// 2. Add a Checkbox and a DateTime filter to its children +/// 3. Add a Number filter to the DateTime filter's children +/// +#[tokio::test] +async fn create_advanced_filter_with_conversion_test() { + let mut test = DatabaseFilterTest::new().await; + + let create_checkbox_filter = || -> CheckboxFilterPB { + CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + } + }; + + let create_date_filter = || -> DateFilterPB { + DateFilterPB { + condition: DateFilterConditionPB::DateAfter, + timestamp: Some(1651366800), + ..Default::default() + } + }; + + let create_number_filter = || -> NumberFilterPB { + NumberFilterPB { + condition: NumberFilterConditionPB::NumberIsNotEmpty, + content: "".to_string(), + } + }; + + let scripts = vec![CreateOrFilter { + parent_filter_id: None, + changed: None, + }]; + test.run_scripts(scripts).await; + // IS_CHECK OR DATE > 1651366800 + + let or_filter = test.get_filter(FilterType::Or, None).await.unwrap(); + + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(or_filter.id.clone()), + field_type: FieldType::Checkbox, + data: BoxAny::new(create_checkbox_filter()), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: 4, + }), + }, + CreateDataFilter { + parent_filter_id: Some(or_filter.id.clone()), + field_type: FieldType::DateTime, + data: BoxAny::new(create_date_filter()), + changed: None, + }, + ]; + test.run_scripts(scripts).await; + // OR + + let date_filter = test + .get_filter(FilterType::Data, Some(FieldType::DateTime)) + .await + .unwrap(); + + let checkbox_filter_bytes: Result = create_checkbox_filter().try_into(); + let checkbox_filter_bytes = checkbox_filter_bytes.unwrap().to_vec(); + let date_filter_bytes: Result = create_date_filter().try_into(); + let date_filter_bytes = date_filter_bytes.unwrap().to_vec(); + let number_filter_bytes: Result = create_number_filter().try_into(); + let number_filter_bytes = number_filter_bytes.unwrap().to_vec(); + + let scripts = vec![ + CreateDataFilter { + parent_filter_id: Some(date_filter.id), + field_type: FieldType::Number, + data: BoxAny::new(create_number_filter()), + changed: None, + }, + Wait { millisecond: 100 }, + AssertFilters { + expected: vec![FilterPB { + id: "".to_string(), + filter_type: FilterType::Or, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Checkbox, + data: checkbox_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::And, + children: vec![ + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::DateTime, + data: date_filter_bytes, + }), + }, + FilterPB { + id: "".to_string(), + filter_type: FilterType::Data, + children: vec![], + data: Some(FilterDataPB { + field_id: "".to_string(), + field_type: FieldType::Number, + data: number_filter_bytes, + }), + }, + ], + data: None, + }, + ], + data: None, + }], + }, + AssertNumberOfVisibleRows { expected: 4 }, + ]; + test.run_scripts(scripts).await; + // IS_CHECK OR (DATE > 1651366800 AND NUMBER NOT EMPTY) +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs index dd30c75df6..881a1cebf9 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checkbox_filter_test.rs @@ -1,4 +1,5 @@ -use flowy_database2::entities::CheckboxFilterConditionPB; +use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, FieldType}; +use lib_infra::box_any::BoxAny; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -6,27 +7,39 @@ use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged} #[tokio::test] async fn grid_filter_checkbox_is_check_test() { let mut test = DatabaseFilterTest::new().await; + let expected = 3; let row_count = test.row_details.len(); - // The initial number of unchecked is 3 - // The initial number of checked is 2 - let scripts = vec![CreateCheckboxFilter { - condition: CheckboxFilterConditionPB::IsChecked, - changed: Some(FilterRowChanged { - showing_num_of_rows: 0, - hiding_num_of_rows: row_count - 3, - }), - }]; - test.run_scripts(scripts).await; -} - -#[tokio::test] -async fn grid_filter_checkbox_is_uncheck_test() { - let mut test = DatabaseFilterTest::new().await; - let expected = 4; - let row_count = test.row_details.len(); + // The initial number of checked is 3 + // The initial number of unchecked is 4 let scripts = vec![ - CreateCheckboxFilter { - condition: CheckboxFilterConditionPB::IsUnChecked, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checkbox, + data: BoxAny::new(CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + }), + changed: Some(FilterRowChanged { + showing_num_of_rows: 0, + hiding_num_of_rows: row_count - expected, + }), + }, + AssertNumberOfVisibleRows { expected }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_checkbox_is_uncheck_test() { + let mut test = DatabaseFilterTest::new().await; + let expected = 4; + let row_count = test.row_details.len(); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checkbox, + data: BoxAny::new(CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsUnChecked, + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs index b6bbfc88f6..3da9cab5a2 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/checklist_filter_test.rs @@ -1,5 +1,6 @@ -use flowy_database2::entities::{ChecklistFilterConditionPB, FieldType}; +use flowy_database2::entities::{ChecklistFilterConditionPB, ChecklistFilterPB, FieldType}; use flowy_database2::services::field::checklist_type_option::ChecklistCellData; +use lib_infra::box_any::BoxAny; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -16,8 +17,12 @@ async fn grid_filter_checklist_is_incomplete_test() { row_id: test.row_details[0].row.id.clone(), selected_option_ids: option_ids, }, - CreateChecklistFilter { - condition: ChecklistFilterConditionPB::IsIncomplete, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checklist, + data: BoxAny::new(ChecklistFilterPB { + condition: ChecklistFilterConditionPB::IsIncomplete, + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -39,8 +44,12 @@ async fn grid_filter_checklist_is_complete_test() { row_id: test.row_details[0].row.id.clone(), selected_option_ids: option_ids, }, - CreateChecklistFilter { - condition: ChecklistFilterConditionPB::IsComplete, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Checklist, + data: BoxAny::new(ChecklistFilterPB { + condition: ChecklistFilterConditionPB::IsComplete, + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs index 86caf2d8fa..34964b9720 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/date_filter_test.rs @@ -1,4 +1,5 @@ -use flowy_database2::entities::DateFilterConditionPB; +use flowy_database2::entities::{DateFilterConditionPB, DateFilterPB, FieldType}; +use lib_infra::box_any::BoxAny; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -9,11 +10,15 @@ async fn grid_filter_date_is_test() { let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ - CreateDateFilter { - condition: DateFilterConditionPB::DateIs, - start: None, - end: None, - timestamp: Some(1647251762), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateIs, + start: None, + end: None, + timestamp: Some(1647251762), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -30,11 +35,15 @@ async fn grid_filter_date_after_test() { let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ - CreateDateFilter { - condition: DateFilterConditionPB::DateAfter, - start: None, - end: None, - timestamp: Some(1647251762), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateAfter, + start: None, + end: None, + timestamp: Some(1647251762), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -51,11 +60,15 @@ async fn grid_filter_date_on_or_after_test() { let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ - CreateDateFilter { - condition: DateFilterConditionPB::DateOnOrAfter, - start: None, - end: None, - timestamp: Some(1668359085), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateOnOrAfter, + start: None, + end: None, + timestamp: Some(1668359085), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -72,11 +85,15 @@ async fn grid_filter_date_on_or_before_test() { let row_count = test.row_details.len(); let expected = 4; let scripts = vec![ - CreateDateFilter { - condition: DateFilterConditionPB::DateOnOrBefore, - start: None, - end: None, - timestamp: Some(1668359085), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateOnOrBefore, + start: None, + end: None, + timestamp: Some(1668359085), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -93,11 +110,15 @@ async fn grid_filter_date_within_test() { let row_count = test.row_details.len(); let expected = 5; let scripts = vec![ - CreateDateFilter { - condition: DateFilterConditionPB::DateWithIn, - start: Some(1647251762), - end: Some(1668704685), - timestamp: None, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::DateTime, + data: BoxAny::new(DateFilterPB { + condition: DateFilterConditionPB::DateWithIn, + start: Some(1647251762), + end: Some(1668704685), + timestamp: None, + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs index 160bf3427f..bf5d1513c9 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/mod.rs @@ -1,3 +1,4 @@ +mod advanced_filter_test; mod checkbox_filter_test; mod checklist_filter_test; mod date_filter_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs index c6cdef1db2..e041ba1b4c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/number_filter_test.rs @@ -1,4 +1,5 @@ -use flowy_database2::entities::NumberFilterConditionPB; +use flowy_database2::entities::{FieldType, NumberFilterConditionPB, NumberFilterPB}; +use lib_infra::box_any::BoxAny; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -9,9 +10,13 @@ async fn grid_filter_number_is_equal_test() { let row_count = test.row_details.len(); let expected = 1; let scripts = vec![ - CreateNumberFilter { - condition: NumberFilterConditionPB::Equal, - content: "1".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { + condition: NumberFilterConditionPB::Equal, + content: "1".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -28,9 +33,13 @@ async fn grid_filter_number_is_less_than_test() { let row_count = test.row_details.len(); let expected = 2; let scripts = vec![ - CreateNumberFilter { - condition: NumberFilterConditionPB::LessThan, - content: "3".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { + condition: NumberFilterConditionPB::LessThan, + content: "3".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -48,9 +57,13 @@ async fn grid_filter_number_is_less_than_test2() { let row_count = test.row_details.len(); let expected = 2; let scripts = vec![ - CreateNumberFilter { - condition: NumberFilterConditionPB::LessThan, - content: "$3".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { + condition: NumberFilterConditionPB::LessThan, + content: "$3".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -67,9 +80,13 @@ async fn grid_filter_number_is_less_than_or_equal_test() { let row_count = test.row_details.len(); let expected = 3; let scripts = vec![ - CreateNumberFilter { - condition: NumberFilterConditionPB::LessThanOrEqualTo, - content: "3".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { + condition: NumberFilterConditionPB::LessThanOrEqualTo, + content: "3".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -86,9 +103,13 @@ async fn grid_filter_number_is_empty_test() { let row_count = test.row_details.len(); let expected = 2; let scripts = vec![ - CreateNumberFilter { - condition: NumberFilterConditionPB::NumberIsEmpty, - content: "".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { + condition: NumberFilterConditionPB::NumberIsEmpty, + content: "".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -105,9 +126,13 @@ async fn grid_filter_number_is_not_empty_test() { let row_count = test.row_details.len(); let expected = 5; let scripts = vec![ - CreateNumberFilter { - condition: NumberFilterConditionPB::NumberIsNotEmpty, - content: "".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::Number, + data: BoxAny::new(NumberFilterPB { + condition: NumberFilterConditionPB::NumberIsNotEmpty, + content: "".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs index 1518398719..f2b58070e7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/script.rs @@ -3,14 +3,12 @@ use std::time::Duration; use collab_database::rows::RowId; -use flowy_database2::services::filter::FilterContext; +use flowy_database2::services::filter::{FilterChangeset, FilterInner}; +use lib_infra::box_any::BoxAny; use tokio::sync::broadcast::Receiver; use flowy_database2::entities::{ - CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, - DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterPayloadPB, FieldType, - FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, - TextFilterConditionPB, TextFilterPB, UpdateFilterParams, UpdateFilterPayloadPB, + DatabaseViewSettingPB, FieldType, FilterPB, FilterType, TextFilterConditionPB, TextFilterPB, }; use flowy_database2::services::database_view::DatabaseViewChanged; use lib_dispatch::prelude::af_spawn; @@ -37,12 +35,10 @@ pub enum FilterScript { option_id: String, changed: Option, }, - InsertFilter { - payload: UpdateFilterPayloadPB, - }, - CreateTextFilter { - condition: TextFilterConditionPB, - content: String, + CreateDataFilter { + parent_filter_id: Option, + field_type: FieldType, + data: BoxAny, changed: Option, }, UpdateTextFilter { @@ -51,50 +47,36 @@ pub enum FilterScript { content: String, changed: Option, }, - CreateNumberFilter { - condition: NumberFilterConditionPB, - content: String, + CreateAndFilter { + parent_filter_id: Option, changed: Option, }, - CreateCheckboxFilter { - condition: CheckboxFilterConditionPB, + CreateOrFilter { + parent_filter_id: Option, changed: Option, }, - CreateDateFilter { - condition: DateFilterConditionPB, - start: Option, - end: Option, - timestamp: Option, - changed: Option, - }, - CreateMultiSelectFilter { - condition: SelectOptionConditionPB, - option_ids: Vec, - }, - CreateSingleSelectFilter { - condition: SelectOptionConditionPB, - option_ids: Vec, - changed: Option, - }, - CreateChecklistFilter { - condition: ChecklistFilterConditionPB, - changed: Option, - }, - AssertFilterCount { - count: i32, - }, DeleteFilter { - filter_context: FilterContext, + filter_id: String, + field_id: String, changed: Option, }, - AssertFilterContent { - filter_id: String, - condition: i64, - content: String, + // CreateSimpleAdvancedFilter, + // CreateComplexAdvancedFilter, + AssertFilterCount { + count: usize, }, AssertNumberOfVisibleRows { expected: usize, }, + AssertFilters { + /// 1. assert that the filter type is correct + /// 2. if the filter is data, assert that the field_type, condition and content are correct + /// (no field_id) + /// 3. if the filter is and/or, assert that each child is correct as well. + expected: Vec, + }, + // AssertSimpleAdvancedFilter, + // AssertComplexAdvancedFilterResult, #[allow(dead_code)] AssertGridSetting { expected_setting: DatabaseViewSettingPB, @@ -118,14 +100,54 @@ impl DatabaseFilterTest { } } - pub fn view_id(&self) -> String { - self.view_id.clone() - } - pub async fn get_all_filters(&self) -> Vec { self.editor.get_all_filters(&self.view_id).await.items } + pub async fn get_filter( + &self, + filter_type: FilterType, + field_type: Option, + ) -> Option { + let filters = self.inner.editor.get_all_filters(&self.view_id).await; + + for filter in filters.items.iter() { + let result = Self::find_filter(filter, filter_type, field_type); + if result.is_some() { + return result; + } + } + + None + } + + fn find_filter( + filter: &FilterPB, + filter_type: FilterType, + field_type: Option, + ) -> Option { + match &filter.filter_type { + FilterType::And | FilterType::Or if filter.filter_type == filter_type => Some(filter.clone()), + FilterType::And | FilterType::Or => { + for child_filter in filter.children.iter() { + if let Some(result) = Self::find_filter(child_filter, filter_type, field_type) { + return Some(result); + } + } + None + }, + FilterType::Data + if filter.filter_type == filter_type + && field_type.map_or(false, |field_type| { + field_type == filter.data.clone().unwrap().field_type + }) => + { + Some(filter.clone()) + }, + _ => None, + } + } + pub async fn run_scripts(&mut self, scripts: Vec) { for script in scripts { self.run_script(script).await; @@ -139,13 +161,7 @@ impl DatabaseFilterTest { text, changed, } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); + self.subscribe_view_changed().await; self.assert_future_changed(changed).await; self.update_text_cell(row_id, &text).await.unwrap(); }, @@ -163,46 +179,35 @@ impl DatabaseFilterTest { option_id, changed, } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); + self.subscribe_view_changed().await; self.assert_future_changed(changed).await; self .update_single_select_cell(row_id, &option_id) .await .unwrap(); }, - FilterScript::InsertFilter { payload } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - self.insert_filter(payload).await; - }, - FilterScript::CreateTextFilter { - condition, - content, + FilterScript::CreateDataFilter { + parent_filter_id, + field_type, + data, changed, } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); + self.subscribe_view_changed().await; self.assert_future_changed(changed).await; - let field = self.get_first_field(FieldType::RichText); - let text_filter = TextFilterPB { condition, content }; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, text_filter); - self.insert_filter(payload).await; + let field = self.get_first_field(field_type); + let params = FilterChangeset::Insert { + parent_filter_id, + data: FilterInner::Data { + field_id: field.id, + field_type, + condition_and_content: data, + }, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); }, FilterScript::UpdateTextFilter { filter, @@ -210,172 +215,76 @@ impl DatabaseFilterTest { content, changed, } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - self.assert_future_changed(changed).await; - let params = UpdateFilterParams { - view_id: self.view_id(), - field_id: filter.field_id, - filter_id: Some(filter.id), - field_type: filter.field_type, - condition: condition as i64, - content, - }; - self.editor.create_or_update_filter(params).await.unwrap(); - }, - FilterScript::CreateNumberFilter { - condition, - content, - changed, - } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - self.assert_future_changed(changed).await; - let field = self.get_first_field(FieldType::Number); - let number_filter = NumberFilterPB { condition, content }; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, number_filter); - self.insert_filter(payload).await; - }, - FilterScript::CreateCheckboxFilter { condition, changed } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - self.assert_future_changed(changed).await; - let field = self.get_first_field(FieldType::Checkbox); - let checkbox_filter = CheckboxFilterPB { condition }; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, checkbox_filter); - self.insert_filter(payload).await; - }, - FilterScript::CreateDateFilter { - condition, - start, - end, - timestamp, - changed, - } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - self.assert_future_changed(changed).await; - let field = self.get_first_field(FieldType::DateTime); - let date_filter = DateFilterPB { - condition, - start, - end, - timestamp, - }; + self.subscribe_view_changed().await; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, date_filter); - self.insert_filter(payload).await; - }, - FilterScript::CreateMultiSelectFilter { - condition, - option_ids, - } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); - let field = self.get_first_field(FieldType::MultiSelect); - let filter = SelectOptionFilterPB { - condition, - option_ids, + self.assert_future_changed(changed).await; + let current_filter = filter.data.unwrap(); + let params = FilterChangeset::UpdateData { + filter_id: filter.id, + data: FilterInner::Data { + field_id: current_filter.field_id, + field_type: current_filter.field_type, + condition_and_content: BoxAny::new(TextFilterPB { condition, content }), + }, }; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); - self.insert_filter(payload).await; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); }, - FilterScript::CreateSingleSelectFilter { - condition, - option_ids, + FilterScript::CreateAndFilter { + parent_filter_id, changed, } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); + self.subscribe_view_changed().await; self.assert_future_changed(changed).await; - let field = self.get_first_field(FieldType::SingleSelect); - let filter = SelectOptionFilterPB { - condition, - option_ids, + let params = FilterChangeset::Insert { + parent_filter_id, + data: FilterInner::And { children: vec![] }, }; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); - self.insert_filter(payload).await; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); }, - FilterScript::CreateChecklistFilter { condition, changed } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); + FilterScript::CreateOrFilter { + parent_filter_id, + changed, + } => { + self.subscribe_view_changed().await; self.assert_future_changed(changed).await; - let field = self.get_first_field(FieldType::Checklist); - let filter = ChecklistFilterPB { condition }; - let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); - self.insert_filter(payload).await; + let params = FilterChangeset::Insert { + parent_filter_id, + data: FilterInner::Or { children: vec![] }, + }; + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); }, FilterScript::AssertFilterCount { count } => { let filters = self.editor.get_all_filters(&self.view_id).await.items; - assert_eq!(count as usize, filters.len()); - }, - FilterScript::AssertFilterContent { - filter_id, - condition, - content, - } => { - let filter = self - .editor - .get_filter(&self.view_id, &filter_id) - .await - .unwrap(); - assert_eq!(&filter.content, &content); - assert_eq!(filter.condition, condition); + assert_eq!(count, filters.len()); }, FilterScript::DeleteFilter { - filter_context, + filter_id, + field_id, changed, } => { - self.recv = Some( - self - .editor - .subscribe_view_changed(&self.view_id()) - .await - .unwrap(), - ); + self.subscribe_view_changed().await; self.assert_future_changed(changed).await; - let params = DeleteFilterPayloadPB { - filter_id: filter_context.filter_id, - view_id: self.view_id(), - field_id: filter_context.field_id, - field_type: filter_context.field_type, + let params = FilterChangeset::Delete { + filter_id, + field_id, }; - self.editor.delete_filter(params).await.unwrap(); + self + .editor + .modify_view_filters(&self.view_id, params) + .await + .unwrap(); }, FilterScript::AssertGridSetting { expected_setting } => { let setting = self @@ -385,6 +294,12 @@ impl DatabaseFilterTest { .unwrap(); assert_eq!(expected_setting, setting); }, + FilterScript::AssertFilters { expected } => { + let actual = self.get_all_filters().await; + for (actual_filter, expected_filter) in actual.iter().zip(expected.iter()) { + Self::assert_filter(actual_filter, expected_filter); + } + }, FilterScript::AssertNumberOfVisibleRows { expected } => { let grid = self.editor.get_database_data(&self.view_id).await.unwrap(); assert_eq!(grid.rows.len(), expected); @@ -395,6 +310,16 @@ impl DatabaseFilterTest { } } + async fn subscribe_view_changed(&mut self) { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + } + async fn assert_future_changed(&mut self, change: Option) { if change.is_none() { return; @@ -424,9 +349,24 @@ impl DatabaseFilterTest { }); } - async fn insert_filter(&self, payload: UpdateFilterPayloadPB) { - let params: UpdateFilterParams = payload.try_into().unwrap(); - self.editor.create_or_update_filter(params).await.unwrap(); + fn assert_filter(actual: &FilterPB, expected: &FilterPB) { + assert_eq!(actual.filter_type, expected.filter_type); + assert_eq!(actual.children.is_empty(), expected.children.is_empty()); + assert_eq!(actual.data.is_some(), expected.data.is_some()); + + match actual.filter_type { + FilterType::Data => { + let actual_data = actual.data.clone().unwrap(); + let expected_data = expected.data.clone().unwrap(); + assert_eq!(actual_data.field_type, expected_data.field_type); + assert_eq!(actual_data.data, expected_data.data); + }, + FilterType::And | FilterType::Or => { + for (actual_child, expected_child) in actual.children.iter().zip(expected.children.iter()) { + Self::assert_filter(actual_child, expected_child); + } + }, + } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs index 16c848ea12..eb808d0bc3 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/select_option_filter_test.rs @@ -1,4 +1,5 @@ -use flowy_database2::entities::{FieldType, SelectOptionConditionPB}; +use flowy_database2::entities::{FieldType, SelectOptionFilterConditionPB, SelectOptionFilterPB}; +use lib_infra::box_any::BoxAny; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged}; @@ -7,9 +8,14 @@ use crate::database::filter_test::script::{DatabaseFilterTest, FilterRowChanged} async fn grid_filter_multi_select_is_empty_test() { let mut test = DatabaseFilterTest::new().await; let scripts = vec![ - CreateMultiSelectFilter { - condition: SelectOptionConditionPB::OptionIsEmpty, - option_ids: vec![], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsEmpty, + option_ids: vec![], + }), + changed: None, }, AssertNumberOfVisibleRows { expected: 2 }, ]; @@ -20,9 +26,14 @@ async fn grid_filter_multi_select_is_empty_test() { async fn grid_filter_multi_select_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; let scripts = vec![ - CreateMultiSelectFilter { - condition: SelectOptionConditionPB::OptionIsNotEmpty, - option_ids: vec![], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, + option_ids: vec![], + }), + changed: None, }, AssertNumberOfVisibleRows { expected: 5 }, ]; @@ -35,11 +46,16 @@ async fn grid_filter_multi_select_is_test() { let field = test.get_first_field(FieldType::MultiSelect); let mut options = test.get_multi_select_type_option(&field.id); let scripts = vec![ - CreateMultiSelectFilter { - condition: SelectOptionConditionPB::OptionIs, - option_ids: vec![options.remove(0).id, options.remove(0).id], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![options.remove(0).id, options.remove(0).id], + }), + changed: None, }, - AssertNumberOfVisibleRows { expected: 5 }, + AssertNumberOfVisibleRows { expected: 1 }, ]; test.run_scripts(scripts).await; } @@ -50,11 +66,16 @@ async fn grid_filter_multi_select_is_test2() { let field = test.get_first_field(FieldType::MultiSelect); let mut options = test.get_multi_select_type_option(&field.id); let scripts = vec![ - CreateMultiSelectFilter { - condition: SelectOptionConditionPB::OptionIs, - option_ids: vec![options.remove(1).id], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![options.remove(1).id], + }), + changed: None, }, - AssertNumberOfVisibleRows { expected: 4 }, + AssertNumberOfVisibleRows { expected: 1 }, ]; test.run_scripts(scripts).await; } @@ -65,9 +86,13 @@ async fn grid_filter_single_select_is_empty_test() { let expected = 3; let row_count = test.row_details.len(); let scripts = vec![ - CreateSingleSelectFilter { - condition: SelectOptionConditionPB::OptionIsEmpty, - option_ids: vec![], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::SingleSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsEmpty, + option_ids: vec![], + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -82,13 +107,17 @@ async fn grid_filter_single_select_is_empty_test() { async fn grid_filter_single_select_is_test() { let mut test = DatabaseFilterTest::new().await; let field = test.get_first_field(FieldType::SingleSelect); - let mut options = test.get_single_select_type_option(&field.id).options; + let mut options = test.get_single_select_type_option(&field.id); let expected = 2; let row_count = test.row_details.len(); let scripts = vec![ - CreateSingleSelectFilter { - condition: SelectOptionConditionPB::OptionIs, - option_ids: vec![options.remove(0).id], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::SingleSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![options.remove(0).id], + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - expected, @@ -104,14 +133,18 @@ async fn grid_filter_single_select_is_test2() { let mut test = DatabaseFilterTest::new().await; let field = test.get_first_field(FieldType::SingleSelect); let row_details = test.get_rows().await; - let mut options = test.get_single_select_type_option(&field.id).options; + let mut options = test.get_single_select_type_option(&field.id); let option = options.remove(0); let row_count = test.row_details.len(); let scripts = vec![ - CreateSingleSelectFilter { - condition: SelectOptionConditionPB::OptionIs, - option_ids: vec![option.id.clone()], + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::SingleSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: vec![option.id.clone()], + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: row_count - 2, @@ -136,3 +169,43 @@ async fn grid_filter_single_select_is_test2() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn grid_filter_multi_select_contains_test() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: vec![options.remove(0).id, options.remove(0).id], + }), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 5 }, + ]; + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn grid_filter_multi_select_contains_test2() { + let mut test = DatabaseFilterTest::new().await; + let field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&field.id); + let scripts = vec![ + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::MultiSelect, + data: BoxAny::new(SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: vec![options.remove(1).id], + }), + changed: None, + }, + AssertNumberOfVisibleRows { expected: 4 }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs index 3c4940d261..600f4342fa 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/filter_test/text_filter_test.rs @@ -1,7 +1,5 @@ -use flowy_database2::entities::{ - FieldType, TextFilterConditionPB, TextFilterPB, UpdateFilterPayloadPB, -}; -use flowy_database2::services::filter::FilterContext; +use flowy_database2::entities::{FieldType, TextFilterConditionPB, TextFilterPB}; +use lib_infra::box_any::BoxAny; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::*; @@ -10,9 +8,13 @@ use crate::database::filter_test::script::*; async fn grid_filter_text_is_empty_test() { let mut test = DatabaseFilterTest::new().await; let scripts = vec![ - CreateTextFilter { - condition: TextFilterConditionPB::TextIsEmpty, - content: "".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextIsEmpty, + content: "".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 5, @@ -28,9 +30,13 @@ async fn grid_filter_text_is_not_empty_test() { let mut test = DatabaseFilterTest::new().await; // Only one row's text of the initial rows is "" let scripts = vec![ - CreateTextFilter { - condition: TextFilterConditionPB::TextIsNotEmpty, - content: "".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextIsNotEmpty, + content: "".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 1, @@ -44,7 +50,8 @@ async fn grid_filter_text_is_not_empty_test() { test .run_scripts(vec![ DeleteFilter { - filter_context: FilterContext::from(&filter), + filter_id: filter.id, + field_id: filter.data.unwrap().field_id, changed: Some(FilterRowChanged { showing_num_of_rows: 1, hiding_num_of_rows: 0, @@ -59,9 +66,13 @@ async fn grid_filter_text_is_not_empty_test() { async fn grid_filter_is_text_test() { let mut test = DatabaseFilterTest::new().await; // Only one row's text of the initial rows is "A" - let scripts = vec![CreateTextFilter { - condition: TextFilterConditionPB::Is, - content: "A".to_string(), + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextIs, + content: "A".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 5, @@ -73,9 +84,13 @@ async fn grid_filter_is_text_test() { #[tokio::test] async fn grid_filter_contain_text_test() { let mut test = DatabaseFilterTest::new().await; - let scripts = vec![CreateTextFilter { - condition: TextFilterConditionPB::Contains, - content: "A".to_string(), + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "A".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 2, @@ -90,9 +105,13 @@ async fn grid_filter_contain_text_test2() { let row_detail = test.row_details.clone(); let scripts = vec![ - CreateTextFilter { - condition: TextFilterConditionPB::Contains, - content: "A".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "A".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 2, @@ -114,9 +133,13 @@ async fn grid_filter_contain_text_test2() { async fn grid_filter_does_not_contain_text_test() { let mut test = DatabaseFilterTest::new().await; // None of the initial rows contains the text "AB" - let scripts = vec![CreateTextFilter { - condition: TextFilterConditionPB::DoesNotContain, - content: "AB".to_string(), + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextDoesNotContain, + content: "AB".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 0, @@ -128,9 +151,13 @@ async fn grid_filter_does_not_contain_text_test() { #[tokio::test] async fn grid_filter_start_with_text_test() { let mut test = DatabaseFilterTest::new().await; - let scripts = vec![CreateTextFilter { - condition: TextFilterConditionPB::StartsWith, - content: "A".to_string(), + let scripts = vec![CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextStartsWith, + content: "A".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 3, @@ -143,9 +170,13 @@ async fn grid_filter_start_with_text_test() { async fn grid_filter_ends_with_text_test() { let mut test = DatabaseFilterTest::new().await; let scripts = vec![ - CreateTextFilter { - condition: TextFilterConditionPB::EndsWith, - content: "A".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextEndsWith, + content: "A".to_string(), + }), changed: None, }, AssertNumberOfVisibleRows { expected: 2 }, @@ -157,9 +188,13 @@ async fn grid_filter_ends_with_text_test() { async fn grid_update_text_filter_test() { let mut test = DatabaseFilterTest::new().await; let scripts = vec![ - CreateTextFilter { - condition: TextFilterConditionPB::EndsWith, - content: "A".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextEndsWith, + content: "A".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 4, @@ -175,7 +210,7 @@ async fn grid_update_text_filter_test() { let scripts = vec![ UpdateTextFilter { filter, - condition: TextFilterConditionPB::Is, + condition: TextFilterConditionPB::TextIs, content: "A".to_string(), changed: Some(FilterRowChanged { showing_num_of_rows: 0, @@ -190,14 +225,16 @@ async fn grid_update_text_filter_test() { #[tokio::test] async fn grid_filter_delete_test() { let mut test = DatabaseFilterTest::new().await; - let field = test.get_first_field(FieldType::RichText).clone(); - let text_filter = TextFilterPB { - condition: TextFilterConditionPB::TextIsEmpty, - content: "".to_string(), - }; - let payload = UpdateFilterPayloadPB::new(&test.view_id(), &field, text_filter); let scripts = vec![ - InsertFilter { payload }, + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + changed: None, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextIsEmpty, + content: "".to_string(), + }), + }, AssertFilterCount { count: 1 }, AssertNumberOfVisibleRows { expected: 1 }, ]; @@ -207,7 +244,8 @@ async fn grid_filter_delete_test() { test .run_scripts(vec![ DeleteFilter { - filter_context: FilterContext::from(&filter), + filter_id: filter.id, + field_id: filter.data.unwrap().field_id, changed: None, }, AssertFilterCount { count: 0 }, @@ -221,9 +259,13 @@ async fn grid_filter_update_empty_text_cell_test() { let mut test = DatabaseFilterTest::new().await; let row_details = test.row_details.clone(); let scripts = vec![ - CreateTextFilter { - condition: TextFilterConditionPB::TextIsEmpty, - content: "".to_string(), + CreateDataFilter { + parent_filter_id: None, + field_type: FieldType::RichText, + data: BoxAny::new(TextFilterPB { + condition: TextFilterConditionPB::TextIsEmpty, + content: "".to_string(), + }), changed: Some(FilterRowChanged { showing_num_of_rows: 0, hiding_num_of_rows: 5, diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs index 5ee7161868..418dafa0f7 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/date_group_test.rs @@ -3,12 +3,8 @@ use std::vec; use chrono::NaiveDateTime; use chrono::{offset, Duration}; -use collab_database::database::gen_row_id; -use collab_database::rows::CreateRowParams; -use collab_database::views::OrderObjectPosition; -use flowy_database2::entities::FieldType; -use flowy_database2::services::cell::CellBuilder; +use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; use flowy_database2::services::field::DateCellData; use crate::database::group_test::script::DatabaseGroupTest; @@ -26,19 +22,17 @@ async fn group_by_date_test() { .unwrap() .timestamp() .to_string(); + let mut cells = HashMap::new(); cells.insert(date_field.id.clone(), timestamp); - let cells = CellBuilder::with_cells(cells, &[date_field.clone()]).build(); - let params = CreateRowParams { - id: gen_row_id(), - cells, - height: 60, - visibility: true, - row_position: OrderObjectPosition::default(), - timestamp: 0, + let params = CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: cells, + ..Default::default() }; - let res = test.editor.create_row(&test.view_id, None, params).await; + + let res = test.editor.create_row(params).await; assert!(res.is_ok()); } @@ -69,56 +63,50 @@ async fn group_by_date_test() { row_count: 0, }, // Added via `make_test_board` - AssertGroupIDName { + AssertGroupId { group_index: 1, group_id: "2022/03/01".to_string(), - group_name: "Mar 2022".to_string(), }, AssertGroupRowCount { group_index: 1, row_count: 3, }, // Added via `make_test_board` - AssertGroupIDName { + AssertGroupId { group_index: 2, group_id: "2022/11/01".to_string(), - group_name: "Nov 2022".to_string(), }, AssertGroupRowCount { group_index: 2, row_count: 2, }, - AssertGroupIDName { + AssertGroupId { group_index: 3, group_id: last_30_days, - group_name: "Last 30 days".to_string(), }, AssertGroupRowCount { group_index: 3, row_count: 1, }, - AssertGroupIDName { + AssertGroupId { group_index: 4, group_id: last_day, - group_name: "Yesterday".to_string(), }, AssertGroupRowCount { group_index: 4, row_count: 2, }, - AssertGroupIDName { + AssertGroupId { group_index: 5, group_id: today.format("%Y/%m/%d").to_string(), - group_name: "Today".to_string(), }, AssertGroupRowCount { group_index: 5, row_count: 1, }, - AssertGroupIDName { + AssertGroupId { group_index: 6, group_id: next_7_days, - group_name: "Next 7 days".to_string(), }, AssertGroupRowCount { group_index: 6, @@ -186,10 +174,9 @@ async fn change_date_on_moving_row_to_another_group() { group_index: 2, row_count: 3, }, - AssertGroupIDName { + AssertGroupId { group_index: 2, group_id: "2022/11/01".to_string(), - group_name: "Nov 2022".to_string(), }, ]; test.run_scripts(scripts).await; diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs index e1d24b29da..48f47b01e0 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/script.rs @@ -1,8 +1,7 @@ -use collab_database::database::gen_row_id; use collab_database::fields::Field; -use collab_database::rows::{CreateRowParams, RowId}; +use collab_database::rows::RowId; -use flowy_database2::entities::{FieldType, GroupPB, RowMetaPB}; +use flowy_database2::entities::{CreateRowPayloadPB, FieldType, GroupPB, RowMetaPB}; use flowy_database2::services::cell::{ delete_select_option_cell, insert_date_cell, insert_select_option_cell, insert_url_cell, }; @@ -10,7 +9,6 @@ use flowy_database2::services::field::{ edit_single_select_type_option, SelectOption, SelectTypeOptionSharedAction, SingleSelectTypeOption, }; -use lib_infra::util::timestamp; use crate::database::database_editor::DatabaseEditorTest; @@ -62,10 +60,9 @@ pub enum GroupScript { GroupByField { field_id: String, }, - AssertGroupIDName { + AssertGroupId { group_index: usize, group_id: String, - group_name: String, }, CreateGroup { name: String, @@ -138,16 +135,13 @@ impl DatabaseGroupTest { }, GroupScript::CreateRow { group_index } => { let group = self.group_at_index(group_index).await; - let params = CreateRowParams { - id: gen_row_id(), - timestamp: timestamp(), - ..Default::default() + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), + row_position: Default::default(), + group_id: Some(group.group_id), + data: Default::default(), }; - let _ = self - .editor - .create_row(&self.view_id, Some(group.group_id.clone()), params) - .await - .unwrap(); + let _ = self.editor.create_row(params).await.unwrap(); }, GroupScript::DeleteRow { group_index, @@ -246,7 +240,6 @@ impl DatabaseGroupTest { } => { let group = self.group_at_index(group_index).await; assert_eq!(group.group_id, group_pb.group_id); - assert_eq!(group.group_name, group_pb.group_name); }, GroupScript::UpdateSingleSelectSelectOption { inserted_options } => { self @@ -264,14 +257,12 @@ impl DatabaseGroupTest { .await .unwrap(); }, - GroupScript::AssertGroupIDName { + GroupScript::AssertGroupId { group_index, group_id, - group_name, } => { let group = self.group_at_index(group_index).await; assert_eq!(group_id, group.group_id, "group index: {}", group_index); - assert_eq!(group_name, group.group_name, "group index: {}", group_index); }, GroupScript::CreateGroup { name } => self .editor diff --git a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs index 61e4d0e6aa..33e2b1563c 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/group_test/test.rs @@ -457,13 +457,17 @@ async fn group_insert_single_select_option_test() { let scripts = vec![ AssertGroupCount(4), UpdateSingleSelectSelectOption { - inserted_options: vec![SelectOption::new(new_option_name)], + inserted_options: vec![SelectOption { + id: new_option_name.to_string(), + name: new_option_name.to_string(), + color: Default::default(), + }], }, AssertGroupCount(5), ]; test.run_scripts(scripts).await; let new_group = test.group_at_index(4).await; - assert_eq!(new_group.group_name, new_option_name); + assert_eq!(new_group.group_id, new_option_name); } #[tokio::test] @@ -499,6 +503,4 @@ async fn group_manual_create_new_group() { AssertGroupCount(5), ]; test.run_scripts(scripts).await; - let new_group = test.group_at_index(4).await; - assert_eq!(new_group.group_name, new_group_name); } diff --git a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs index e87cec40e6..01362d75b4 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mock_data/grid_mock_data.rs @@ -117,11 +117,7 @@ pub fn make_test_grid() -> DatabaseData { fields.push(url); }, FieldType::Checklist => { - // let option1 = SelectOption::with_color(FIRST_THING, SelectOptionColor::Purple); - // let option2 = SelectOption::with_color(SECOND_THING, SelectOptionColor::Orange); - // let option3 = SelectOption::with_color(THIRD_THING, SelectOptionColor::Yellow); let type_option = ChecklistTypeOption; - // type_option.options.extend(vec![option1, option2, option3]); let checklist_field = FieldBuilder::new(field_type, type_option) .name("TODO") .visibility(true) diff --git a/frontend/rust-lib/flowy-database2/tests/database/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/mod.rs index 5333d54c33..f1614f5493 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mod.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mod.rs @@ -7,7 +7,7 @@ mod field_test; mod filter_test; mod group_test; mod layout_test; -mod sort_test; - mod mock_data; +mod pre_fill_cell_test; mod share_test; +mod sort_test; diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/mod.rs new file mode 100644 index 0000000000..0e76b61079 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/mod.rs @@ -0,0 +1,3 @@ +mod pre_fill_row_according_to_filter_test; +mod pre_fill_row_with_payload_test; +mod script; diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs new file mode 100644 index 0000000000..1000df6f4b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_according_to_filter_test.rs @@ -0,0 +1,433 @@ +use flowy_database2::entities::{ + CheckboxFilterConditionPB, CheckboxFilterPB, DateFilterConditionPB, DateFilterPB, FieldType, + FilterDataPB, SelectOptionFilterConditionPB, SelectOptionFilterPB, TextFilterConditionPB, + TextFilterPB, +}; +use flowy_database2::services::field::SELECTION_IDS_SEPARATOR; + +use crate::database::pre_fill_cell_test::script::{ + DatabasePreFillRowCellTest, PreFillRowCellTestScript::*, +}; + +// This suite of tests cover creating an empty row into a database that has +// active filters. Where appropriate, the row's cell data will be pre-filled +// into the row's cells before creating it in collab. + +#[tokio::test] +async fn according_to_text_contains_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "sample".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + CreateEmptyRow, + Wait { milliseconds: 100 }, + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len() - 1, + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len() - 1, + from_field_type: FieldType::RichText, + expected_content: "sample".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_empty_text_contains_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextContains, + content: "".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + CreateEmptyRow, + Wait { milliseconds: 100 }, + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len() - 1, + exists: false, + }]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_text_is_not_empty_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: text_field.id.clone(), + field_type: FieldType::RichText, + data: TextFilterPB { + condition: TextFilterConditionPB::TextIsNotEmpty, + content: "".to_string(), + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(6), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_checkbox_is_unchecked_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: checkbox_field.id.clone(), + field_type: FieldType::Checkbox, + data: CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsUnChecked, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(4), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(5), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: 4, + exists: false, + }]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_checkbox_is_checked_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: checkbox_field.id.clone(), + field_type: FieldType::Checkbox, + data: CheckboxFilterPB { + condition: CheckboxFilterConditionPB::IsChecked, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(3), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(4), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: 3, + exists: true, + }, + AssertCellContent { + field_id: checkbox_field.id, + row_index: 3, + from_field_type: FieldType::Checkbox, + expected_content: "Yes".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_date_time_is_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let datetime_field = test.get_first_field(FieldType::DateTime); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: datetime_field.id.clone(), + field_type: FieldType::DateTime, + data: DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: Some(1710510086), + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(0), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(1), + ]; + + test.run_scripts(scripts).await; + + let scripts = vec![ + AssertCellExistence { + field_id: datetime_field.id.clone(), + row_index: 0, + exists: true, + }, + AssertCellContent { + field_id: datetime_field.id, + row_index: 0, + from_field_type: FieldType::DateTime, + expected_content: "2024/03/15".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_invalid_date_time_is_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let datetime_field = test.get_first_field(FieldType::DateTime); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: datetime_field.id.clone(), + field_type: FieldType::DateTime, + data: DateFilterPB { + condition: DateFilterConditionPB::DateIs, + timestamp: None, + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(7), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(8), + AssertCellExistence { + field_id: datetime_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_select_option_is_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let filtering_options = vec![options[1].clone(), options[2].clone()]; + let ids = filtering_options + .iter() + .map(|option| option.id.clone()) + .collect(); + let stringified_expected = filtering_options + .iter() + .map(|option| option.name.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIs, + option_ids: ids, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(1), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(2), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 1, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 1, + from_field_type: FieldType::MultiSelect, + expected_content: stringified_expected, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_select_option_contains_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let filtering_options = vec![options[1].clone(), options[2].clone()]; + let ids = filtering_options + .iter() + .map(|option| option.id.clone()) + .collect(); + let stringified_expected = filtering_options.first().unwrap().name.clone(); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionContains, + option_ids: ids, + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(5), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 5, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 5, + from_field_type: FieldType::MultiSelect, + expected_content: stringified_expected, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn according_to_select_option_is_not_empty_filter_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let stringified_expected = options.first().unwrap().name.clone(); + + let scripts = vec![ + AssertRowCount(7), + InsertFilter { + filter: FilterDataPB { + field_id: multi_select_field.id.clone(), + field_type: FieldType::MultiSelect, + data: SelectOptionFilterPB { + condition: SelectOptionFilterConditionPB::OptionIsNotEmpty, + ..Default::default() + } + .try_into() + .unwrap(), + }, + }, + Wait { milliseconds: 100 }, + AssertRowCount(5), + CreateEmptyRow, + Wait { milliseconds: 100 }, + AssertRowCount(6), + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: 5, + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id, + row_index: 5, + from_field_type: FieldType::MultiSelect, + expected_content: stringified_expected, + }, + ]; + + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs new file mode 100644 index 0000000000..b1b42d6479 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/pre_fill_row_with_payload_test.rs @@ -0,0 +1,422 @@ +use std::collections::HashMap; + +use flowy_database2::entities::{CreateRowPayloadPB, FieldType}; +use flowy_database2::services::field::{DateCellData, SELECTION_IDS_SEPARATOR}; + +use crate::database::pre_fill_cell_test::script::{ + DatabasePreFillRowCellTest, PreFillRowCellTestScript::*, +}; + +// This suite of tests cover creating a row using `CreateRowPayloadPB` that passes +// in some cell data in its `data` field of `HashMap` which is a +// map of `field_id` to its corresponding cell data as a String. If valid, the cell +// data will be pre-filled into the row's cells before creating it in collab. + +#[tokio::test] +async fn row_data_payload_with_empty_hashmap_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::new(), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_unknown_field_id_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let malformed_field_id = "this_field_id_will_never_exist"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([( + malformed_field_id.to_string(), + "sample cell data".to_string(), + )]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + AssertCellContent { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "".to_string(), + }, + AssertCellExistence { + field_id: malformed_field_id.to_string(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_empty_string_text_data_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let cell_data = ""; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_text_data_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let cell_data = "sample cell data"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(text_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_multi_text_data_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let text_field = test.get_first_field(FieldType::RichText); + let number_field = test.get_first_field(FieldType::Number); + let url_field = test.get_first_field(FieldType::URL); + + let text_cell_data = "sample cell data"; + let number_cell_data = "1234"; + let url_cell_data = "appflowy.io"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([ + (text_field.id.clone(), text_cell_data.to_string()), + (number_field.id.clone(), number_cell_data.to_string()), + (url_field.id.clone(), url_cell_data.to_string()), + ]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: text_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: text_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: text_cell_data.to_string(), + }, + AssertCellExistence { + field_id: number_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: number_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "$1,234".to_string(), + }, + AssertCellExistence { + field_id: url_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: url_field.id, + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: url_cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_date_time_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let date_field = test.get_first_field(FieldType::DateTime); + let cell_data = "1710510086"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::RichText, + expected_content: "2024/03/15".to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_invalid_date_time_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let date_field = test.get_first_field(FieldType::DateTime); + let cell_data = DateCellData { + timestamp: Some(1710510086), + ..Default::default() + } + .to_string(); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(date_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: date_field.id.clone(), + row_index: test.row_details.len(), + exists: false, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_checkbox_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let cell_data = "Yes"; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(checkbox_field.id.clone(), cell_data.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: checkbox_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: checkbox_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::Checkbox, + expected_content: cell_data.to_string(), + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_select_option_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let options = test.get_multi_select_type_option(&multi_select_field.id); + + let ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let stringified_cell_data = options + .iter() + .map(|option| option.name.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(multi_select_field.id.clone(), ids)]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertCellContent { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + from_field_type: FieldType::MultiSelect, + expected_content: stringified_cell_data, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_invalid_select_option_id_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let multi_select_field = test.get_first_field(FieldType::MultiSelect); + let mut options = test.get_multi_select_type_option(&multi_select_field.id); + + let first_id = options.swap_remove(0).id; + let ids = [first_id.clone(), "nonsense".to_string()].join(SELECTION_IDS_SEPARATOR); + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(multi_select_field.id.clone(), ids.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertSelectOptionCellStrict { + field_id: multi_select_field.id.clone(), + row_index: test.row_details.len(), + expected_content: first_id, + }, + ]; + + test.run_scripts(scripts).await; +} + +#[tokio::test] +async fn row_data_payload_with_too_many_select_option_test() { + let mut test = DatabasePreFillRowCellTest::new().await; + + let single_select_field = test.get_first_field(FieldType::SingleSelect); + let mut options = test.get_single_select_type_option(&single_select_field.id); + + let ids = options + .iter() + .map(|option| option.id.clone()) + .collect::>() + .join(SELECTION_IDS_SEPARATOR); + + let stringified_cell_data = options.swap_remove(0).id; + + let scripts = vec![ + CreateRowWithPayload { + payload: CreateRowPayloadPB { + view_id: test.view_id.clone(), + data: HashMap::from([(single_select_field.id.clone(), ids.to_string())]), + ..Default::default() + }, + }, + Wait { milliseconds: 100 }, + AssertCellExistence { + field_id: single_select_field.id.clone(), + row_index: test.row_details.len(), + exists: true, + }, + AssertSelectOptionCellStrict { + field_id: single_select_field.id.clone(), + row_index: test.row_details.len(), + expected_content: stringified_cell_data, + }, + ]; + + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs new file mode 100644 index 0000000000..e78732ec51 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/pre_fill_cell_test/script.rs @@ -0,0 +1,164 @@ +use std::ops::{Deref, DerefMut}; +use std::time::Duration; + +use flowy_database2::entities::{CreateRowPayloadPB, FieldType, FilterDataPB, InsertFilterPB}; +use flowy_database2::services::cell::stringify_cell_data; +use flowy_database2::services::field::{SelectOptionIds, SELECTION_IDS_SEPARATOR}; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum PreFillRowCellTestScript { + CreateEmptyRow, + CreateRowWithPayload { + payload: CreateRowPayloadPB, + }, + InsertFilter { + filter: FilterDataPB, + }, + AssertRowCount(usize), + AssertCellExistence { + field_id: String, + row_index: usize, + exists: bool, + }, + AssertCellContent { + field_id: String, + row_index: usize, + from_field_type: FieldType, + expected_content: String, + }, + AssertSelectOptionCellStrict { + field_id: String, + row_index: usize, + expected_content: String, + }, + Wait { + milliseconds: u64, + }, +} + +pub struct DatabasePreFillRowCellTest { + inner: DatabaseEditorTest, +} + +impl DatabasePreFillRowCellTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { inner: editor_test } + } + + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; + } + } + + pub async fn run_script(&mut self, script: PreFillRowCellTestScript) { + match script { + PreFillRowCellTestScript::CreateEmptyRow => { + let params = CreateRowPayloadPB { + view_id: self.view_id.clone(), + ..Default::default() + }; + let row_detail = self.editor.create_row(params).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + PreFillRowCellTestScript::CreateRowWithPayload { payload } => { + let row_detail = self.editor.create_row(payload).await.unwrap().unwrap(); + self + .row_by_row_id + .insert(row_detail.row.id.to_string(), row_detail.into()); + self.row_details = self.get_rows().await; + }, + PreFillRowCellTestScript::InsertFilter { filter } => self + .editor + .modify_view_filters( + &self.view_id, + InsertFilterPB { + parent_filter_id: None, + data: filter, + } + .try_into() + .unwrap(), + ) + .await + .unwrap(), + PreFillRowCellTestScript::AssertRowCount(expected_row_count) => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + assert_eq!(expected_row_count, rows.len()); + }, + PreFillRowCellTestScript::AssertCellExistence { + field_id, + row_index, + exists, + } => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); + + let cell = row_detail.row.cells.get(&field_id).cloned(); + + assert_eq!(exists, cell.is_some()); + }, + PreFillRowCellTestScript::AssertCellContent { + field_id, + row_index, + from_field_type, + expected_content, + } => { + let field = self.editor.get_field(&field_id).unwrap(); + let field_type = FieldType::from(field.field_type); + + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); + + let cell = row_detail + .row + .cells + .get(&field_id) + .cloned() + .unwrap_or_default(); + let content = stringify_cell_data(&cell, &from_field_type, &field_type, &field); + assert_eq!(content, expected_content); + }, + PreFillRowCellTestScript::AssertSelectOptionCellStrict { + field_id, + row_index, + expected_content, + } => { + let rows = self.editor.get_rows(&self.view_id).await.unwrap(); + let row_detail = rows.get(row_index).unwrap(); + + let cell = row_detail + .row + .cells + .get(&field_id) + .cloned() + .unwrap_or_default(); + + let content = SelectOptionIds::from(&cell).join(SELECTION_IDS_SEPARATOR); + + assert_eq!(content, expected_content); + }, + PreFillRowCellTestScript::Wait { milliseconds } => { + tokio::time::sleep(Duration::from_millis(milliseconds)).await; + }, + } + } +} + +impl Deref for DatabasePreFillRowCellTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for DatabasePreFillRowCellTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs index 90c0c5f17b..cfa9859075 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/script.rs @@ -8,7 +8,7 @@ use futures::stream::StreamExt; use tokio::sync::broadcast::Receiver; use flowy_database2::entities::{ - DeleteSortPayloadPB, FieldType, ReorderSortPayloadPB, UpdateSortPayloadPB, + CreateRowPayloadPB, DeleteSortPayloadPB, FieldType, ReorderSortPayloadPB, UpdateSortPayloadPB, }; use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::database_view::DatabaseViewChanged; @@ -155,15 +155,10 @@ impl DatabaseSortTest { ); self .editor - .create_row( - &self.view_id, - None, - collab_database::rows::CreateRowParams { - id: collab_database::database::gen_row_id(), - timestamp: collab_database::database::timestamp(), - ..Default::default() - }, - ) + .create_row(CreateRowPayloadPB { + view_id: self.view_id.clone(), + ..Default::default() + }) .await .unwrap(); }, @@ -217,7 +212,7 @@ async fn assert_sort_changed( old_row_orders.insert(changed.new_index, old); assert_eq!(old_row_orders, new_row_orders); }, - DatabaseViewChanged::InsertSortedRowNotification(_changed) => {}, + DatabaseViewChanged::InsertRowNotification(_changed) => {}, _ => {}, } }) diff --git a/frontend/rust-lib/flowy-date/build.rs b/frontend/rust-lib/flowy-date/build.rs index 84b506ee31..e015eb2580 100644 --- a/frontend/rust-lib/flowy-date/build.rs +++ b/frontend/rust-lib/flowy-date/build.rs @@ -13,5 +13,11 @@ fn main() { env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri, ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); } } diff --git a/frontend/rust-lib/flowy-document-pub/src/cloud.rs b/frontend/rust-lib/flowy-document-pub/src/cloud.rs index 7ff9cd6a36..2f4da1bd37 100644 --- a/frontend/rust-lib/flowy-document-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-document-pub/src/cloud.rs @@ -1,5 +1,4 @@ use anyhow::Error; -use collab::core::collab::CollabDocState; pub use collab_document::blocks::DocumentData; use flowy_error::FlowyError; @@ -13,7 +12,7 @@ pub trait DocumentCloudService: Send + Sync + 'static { &self, document_id: &str, workspace_id: &str, - ) -> FutureResult; + ) -> FutureResult, FlowyError>; fn get_document_snapshots( &self, diff --git a/frontend/rust-lib/flowy-document/Cargo.toml b/frontend/rust-lib/flowy-document/Cargo.toml index 52d688d8ac..b787d6e527 100644 --- a/frontend/rust-lib/flowy-document/Cargo.toml +++ b/frontend/rust-lib/flowy-document/Cargo.toml @@ -35,8 +35,8 @@ indexmap = {version = "2.1.0", features = ["serde"]} uuid.workspace = true futures.workspace = true tokio-stream = { workspace = true, features = ["sync"] } +dashmap = "5" scraper = "0.18.0" -lru.workspace = true [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2", features = ["js"]} @@ -55,4 +55,4 @@ tauri_ts = ["flowy-codegen/ts"] web_ts = [ "flowy-codegen/ts", ] - +verbose_log = ["collab-document/verbose_log"] diff --git a/frontend/rust-lib/flowy-document/build.rs b/frontend/rust-lib/flowy-document/build.rs index c3d11111cb..9fdde3edf6 100644 --- a/frontend/rust-lib/flowy-document/build.rs +++ b/frontend/rust-lib/flowy-document/build.rs @@ -13,6 +13,12 @@ fn main() { env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri, ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); } #[cfg(feature = "web_ts")] diff --git a/frontend/rust-lib/flowy-document/src/document.rs b/frontend/rust-lib/flowy-document/src/document.rs index 928ebebea5..41b46b05fa 100644 --- a/frontend/rust-lib/flowy-document/src/document.rs +++ b/frontend/rust-lib/flowy-document/src/document.rs @@ -1,18 +1,18 @@ +use crate::entities::{ + DocEventPB, DocumentAwarenessStatesPB, DocumentSnapshotStatePB, DocumentSyncStatePB, +}; +use crate::notification::{send_notification, DocumentNotification}; +use collab::core::collab::MutexCollab; +use collab_document::{blocks::DocumentData, document::Document}; +use flowy_error::FlowyResult; +use futures::StreamExt; +use lib_dispatch::prelude::af_spawn; +use parking_lot::Mutex; use std::{ ops::{Deref, DerefMut}, sync::Arc, }; - -use collab::core::collab::MutexCollab; -use collab_document::{blocks::DocumentData, document::Document}; -use futures::StreamExt; -use parking_lot::Mutex; - -use flowy_error::FlowyResult; -use lib_dispatch::prelude::af_spawn; - -use crate::entities::{DocEventPB, DocumentSnapshotStatePB, DocumentSyncStatePB}; -use crate::notification::{send_notification, DocumentNotification}; +use tracing::{instrument, trace, warn}; /// This struct wrap the document::Document #[derive(Clone)] @@ -47,18 +47,46 @@ impl MutexDocument { Document::create_with_data(collab, data).map(|inner| Self(Arc::new(Mutex::new(inner))))?; Ok(document) } + + #[instrument(level = "debug", skip_all)] + pub fn start_init_sync(&self) { + if let Some(document) = self.0.try_lock() { + if let Some(collab) = document.get_collab().try_lock() { + collab.start_init_sync(); + } else { + warn!("Failed to start init sync, collab is locked"); + } + } else { + warn!("Failed to start init sync, document is locked"); + } + } } fn subscribe_document_changed(doc_id: &str, document: &MutexDocument) { - let doc_id = doc_id.to_string(); + let doc_id_clone_for_block_changed = doc_id.to_owned(); document .lock() .subscribe_block_changed(move |events, is_remote| { + trace!("subscribe_document_changed: {:?}", events); // send notification to the client. - send_notification(&doc_id, DocumentNotification::DidReceiveUpdate) - .payload::((events, is_remote).into()) - .send(); + send_notification( + &doc_id_clone_for_block_changed, + DocumentNotification::DidReceiveUpdate, + ) + .payload::((events, is_remote, None).into()) + .send(); }); + + let doc_id_clone_for_awareness_state = doc_id.to_owned(); + document.lock().subscribe_awareness_state(move |events| { + trace!("subscribe_awareness_state: {:?}", events); + send_notification( + &doc_id_clone_for_awareness_state, + DocumentNotification::DidUpdateDocumentAwarenessState, + ) + .payload::(events.into()) + .send(); + }); } fn subscribe_document_snapshot_state(collab: &Arc) { @@ -93,6 +121,7 @@ fn subscribe_document_sync_state(collab: &Arc) { } }); } + unsafe impl Sync for MutexDocument {} unsafe impl Send for MutexDocument {} diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index 8fc07c1597..65e9dcf820 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -1,7 +1,13 @@ use std::collections::HashMap; use collab::core::collab_state::SyncState; -use collab_document::blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData}; +use collab_document::{ + blocks::{json_str_to_hashmap, Block, BlockAction, DocumentData}, + document_awareness::{ + DocumentAwarenessPosition, DocumentAwarenessSelection, DocumentAwarenessState, + DocumentAwarenessUser, + }, +}; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; @@ -301,6 +307,9 @@ pub struct DocEventPB { #[pb(index = 2)] pub is_remote: bool, + + #[pb(index = 3, one_of)] + pub new_snapshot: Option, } #[derive(Default, ProtoBuf)] @@ -445,14 +454,27 @@ pub struct DocumentSnapshotStatePB { #[derive(Debug, Default, ProtoBuf)] pub struct DocumentSyncStatePB { #[pb(index = 1)] - pub is_syncing: bool, + pub value: DocumentSyncState, +} + +#[derive(Debug, Default, ProtoBuf_Enum, PartialEq, Eq, Clone, Copy)] +pub enum DocumentSyncState { + #[default] + InitSyncBegin = 0, + InitSyncEnd = 1, + Syncing = 2, + SyncFinished = 3, } impl From for DocumentSyncStatePB { fn from(value: SyncState) -> Self { - Self { - is_syncing: value.is_syncing(), - } + let value = match value { + SyncState::InitSyncBegin => DocumentSyncState::InitSyncBegin, + SyncState::InitSyncEnd => DocumentSyncState::InitSyncEnd, + SyncState::Syncing => DocumentSyncState::Syncing, + SyncState::SyncFinished => DocumentSyncState::SyncFinished, + }; + Self { value } } } @@ -499,3 +521,124 @@ pub struct DocumentSnapshotData { pub object_id: String, pub encoded_v1: Vec, } + +#[derive(ProtoBuf, Debug, Default)] +pub struct DocumentAwarenessStatesPB { + #[pb(index = 1)] + pub value: HashMap, +} + +impl From> for DocumentAwarenessStatesPB { + fn from(value: HashMap) -> Self { + let value = value + .into_iter() + .map(|(k, v)| (k.to_string(), v.into())) + .collect(); + Self { value } + } +} + +#[derive(ProtoBuf, Debug, Default)] +pub struct UpdateDocumentAwarenessStatePB { + #[pb(index = 1)] + pub document_id: String, + #[pb(index = 2, one_of)] + pub selection: Option, + #[pb(index = 3, one_of)] + pub metadata: Option, +} + +#[derive(ProtoBuf, Debug, Default)] +pub struct DocumentAwarenessStatePB { + #[pb(index = 1)] + pub version: i64, + #[pb(index = 2)] + pub user: DocumentAwarenessUserPB, + #[pb(index = 3, one_of)] + pub selection: Option, + #[pb(index = 4, one_of)] + pub metadata: Option, + #[pb(index = 5)] + pub timestamp: i64, +} + +impl From for DocumentAwarenessStatePB { + fn from(value: DocumentAwarenessState) -> Self { + DocumentAwarenessStatePB { + version: value.version, + user: value.user.into(), + selection: value.selection.map(|s| s.into()), + metadata: value.metadata, + timestamp: value.timestamp, + } + } +} + +#[derive(ProtoBuf, Debug, Default)] +pub struct DocumentAwarenessUserPB { + #[pb(index = 1)] + pub uid: i64, + #[pb(index = 2)] + pub device_id: String, +} + +impl From for DocumentAwarenessUserPB { + fn from(value: DocumentAwarenessUser) -> Self { + DocumentAwarenessUserPB { + uid: value.uid, + device_id: value.device_id.to_string(), + } + } +} + +#[derive(ProtoBuf, Debug, Default)] +pub struct DocumentAwarenessSelectionPB { + #[pb(index = 1)] + pub start: DocumentAwarenessPositionPB, + #[pb(index = 2)] + pub end: DocumentAwarenessPositionPB, +} + +#[derive(ProtoBuf, Debug, Default)] +pub struct DocumentAwarenessPositionPB { + #[pb(index = 1)] + pub path: Vec, + #[pb(index = 2)] + pub offset: u64, +} + +impl From for DocumentAwarenessSelection { + fn from(value: DocumentAwarenessSelectionPB) -> Self { + DocumentAwarenessSelection { + start: value.start.into(), + end: value.end.into(), + } + } +} + +impl From for DocumentAwarenessSelectionPB { + fn from(value: DocumentAwarenessSelection) -> Self { + DocumentAwarenessSelectionPB { + start: value.start.into(), + end: value.end.into(), + } + } +} + +impl From for DocumentAwarenessPosition { + fn from(value: DocumentAwarenessPositionPB) -> Self { + DocumentAwarenessPosition { + path: value.path, + offset: value.offset, + } + } +} + +impl From for DocumentAwarenessPositionPB { + fn from(value: DocumentAwarenessPosition) -> Self { + DocumentAwarenessPositionPB { + path: value.path, + offset: value.offset, + } + } +} diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index b8176f79a7..a055885176 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -8,6 +8,7 @@ use std::sync::{Arc, Weak}; use collab_document::blocks::{ BlockAction, BlockActionPayload, BlockActionType, BlockEvent, BlockEventPayload, DeltaType, + DocumentData, }; use flowy_error::{FlowyError, FlowyResult}; @@ -293,12 +294,15 @@ impl From for DeltaTypePB { } } -impl From<(&Vec, bool)> for DocEventPB { - fn from((events, is_remote): (&Vec, bool)) -> Self { +impl From<(&Vec, bool, Option)> for DocEventPB { + fn from( + (events, is_remote, new_snapshot): (&Vec, bool, Option), + ) -> Self { // Convert each individual `BlockEvent` to a protobuf `BlockEventPB`, and collect the results into a `Vec` Self { events: events.iter().map(|e| e.to_owned().into()).collect(), is_remote, + new_snapshot: new_snapshot.map(|d| d.into()), } } } @@ -451,3 +455,16 @@ pub(crate) async fn delete_file_handler( let manager = upgrade_document(manager)?; manager.delete_file(local_file_path, url).await } + +pub(crate) async fn set_awareness_local_state_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> FlowyResult<()> { + let manager = upgrade_document(manager)?; + let data = data.into_inner(); + let doc_id = data.document_id.clone(); + manager + .set_document_awareness_local_state(&doc_id, data) + .await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-document/src/event_map.rs b/frontend/rust-lib/flowy-document/src/event_map.rs index 7ef1ecde5f..1e11db6356 100644 --- a/frontend/rust-lib/flowy-document/src/event_map.rs +++ b/frontend/rust-lib/flowy-document/src/event_map.rs @@ -4,6 +4,7 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; +use tracing::event; use crate::event_handler::get_snapshot_meta_handler; use crate::{event_handler::*, manager::DocumentManager}; @@ -42,6 +43,10 @@ pub fn init(document_manager: Weak) -> AFPlugin { .event(DocumentEvent::UploadFile, upload_file_handler) .event(DocumentEvent::DownloadFile, download_file_handler) .event(DocumentEvent::DeleteFile, delete_file_handler) + .event( + DocumentEvent::SetAwarenessState, + set_awareness_local_state_handler, + ) } #[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)] @@ -118,4 +123,7 @@ pub enum DocumentEvent { DownloadFile = 16, #[event(input = "UploadedFilePB")] DeleteFile = 17, + + #[event(input = "UpdateDocumentAwarenessStatePB")] + SetAwarenessState = 18, } diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index a53b670500..17ef553da5 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -1,23 +1,22 @@ -use std::num::NonZeroUsize; use std::sync::Arc; use std::sync::Weak; -use collab::core::collab::{CollabDocState, MutexCollab}; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab::core::collab_plugin::EncodedCollab; use collab::core::origin::CollabOrigin; use collab::preclude::Collab; use collab_document::blocks::DocumentData; use collab_document::document::Document; +use collab_document::document_awareness::DocumentAwarenessState; +use collab_document::document_awareness::DocumentAwarenessUser; use collab_document::document_data::default_document_data; use collab_entity::CollabType; use collab_plugins::CollabKVDB; +use dashmap::DashMap; use flowy_storage::object_from_disk; -use lru::LruCache; -use parking_lot::Mutex; +use lib_infra::util::timestamp; use tokio::io::AsyncWriteExt; -use tracing::error; -use tracing::info; -use tracing::warn; +use tracing::{error, trace}; use tracing::{event, instrument}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; @@ -28,6 +27,7 @@ use flowy_storage::ObjectStorageService; use lib_dispatch::prelude::af_spawn; use crate::document::MutexDocument; +use crate::entities::UpdateDocumentAwarenessStatePB; use crate::entities::{ DocumentSnapshotData, DocumentSnapshotMeta, DocumentSnapshotMetaPB, DocumentSnapshotPB, }; @@ -35,6 +35,7 @@ use crate::reminder::DocumentReminderAction; pub trait DocumentUserService: Send + Sync { fn user_id(&self) -> Result; + fn device_id(&self) -> Result; fn workspace_id(&self) -> Result; fn collab_db(&self, uid: i64) -> Result, FlowyError>; } @@ -50,7 +51,8 @@ pub trait DocumentSnapshotService: Send + Sync { pub struct DocumentManager { pub user_service: Arc, collab_builder: Arc, - documents: Arc>>>, + documents: Arc>>, + removing_documents: Arc>>, cloud_service: Arc, storage_service: Weak, snapshot_service: Arc, @@ -64,11 +66,11 @@ impl DocumentManager { storage_service: Weak, snapshot_service: Arc, ) -> Self { - let documents = Arc::new(Mutex::new(LruCache::new(NonZeroUsize::new(10).unwrap()))); Self { user_service, collab_builder, - documents, + documents: Arc::new(Default::default()), + removing_documents: Arc::new(Default::default()), cloud_service, storage_service, snapshot_service, @@ -76,7 +78,7 @@ impl DocumentManager { } pub async fn initialize(&self, _uid: i64, _workspace_id: String) -> FlowyResult<()> { - self.documents.lock().clear(); + self.documents.clear(); Ok(()) } @@ -122,7 +124,7 @@ impl DocumentManager { .doc_state .to_vec(); let collab = self - .collab_for_document(uid, doc_id, doc_state, false) + .collab_for_document(uid, doc_id, DocStateSource::FromDocState(doc_state), false) .await?; collab.lock().flush(); Ok(()) @@ -134,18 +136,24 @@ impl DocumentManager { /// If the document exists, open the document and cache it #[tracing::instrument(level = "info", skip(self), err)] pub async fn get_document(&self, doc_id: &str) -> FlowyResult> { - if let Some(doc) = self.documents.lock().get(doc_id).cloned() { + if let Some(doc) = self.documents.get(doc_id).map(|item| item.value().clone()) { return Ok(doc); } - let mut doc_state = CollabDocState::default(); + if let Some(doc) = self.restore_document_from_removing(doc_id) { + return Ok(doc); + } + + let mut doc_state = DocStateSource::FromDisk; // If the document does not exist in local disk, try get the doc state from the cloud. This happens // When user_device_a create a document and user_device_b open the document. if !self.is_doc_exist(doc_id).await? { - doc_state = self - .cloud_service - .get_document_doc_state(doc_id, &self.user_service.workspace_id()?) - .await?; + doc_state = DocStateSource::FromDocState( + self + .cloud_service + .get_document_doc_state(doc_id, &self.user_service.workspace_id()?) + .await?, + ); // the doc_state should not be empty if remote return the doc state without error. if doc_state.is_empty() { @@ -165,10 +173,7 @@ impl DocumentManager { match MutexDocument::open(doc_id, collab) { Ok(document) => { let document = Arc::new(document); - self - .documents - .lock() - .put(doc_id.to_string(), document.clone()); + self.documents.insert(doc_id.to_string(), document.clone()); Ok(document) }, Err(err) => { @@ -183,29 +188,51 @@ impl DocumentManager { } pub async fn get_document_data(&self, doc_id: &str) -> FlowyResult { - let mut updates = vec![]; + let mut doc_state = DocStateSource::FromDisk; if !self.is_doc_exist(doc_id).await? { - updates = self - .cloud_service - .get_document_doc_state(doc_id, &self.user_service.workspace_id()?) - .await?; + doc_state = DocStateSource::FromDocState( + self + .cloud_service + .get_document_doc_state(doc_id, &self.user_service.workspace_id()?) + .await?, + ); } let uid = self.user_service.user_id()?; let collab = self - .collab_for_document(uid, doc_id, updates, false) + .collab_for_document(uid, doc_id, doc_state, false) .await?; Document::open(collab)? .get_document_data() .map_err(internal_error) } - #[instrument(level = "debug", skip(self), err)] + pub async fn open_document(&self, doc_id: &str) -> FlowyResult<()> { + if let Some(mutex_document) = self.restore_document_from_removing(doc_id) { + mutex_document.start_init_sync(); + } + Ok(()) + } + pub async fn close_document(&self, doc_id: &str) -> FlowyResult<()> { - // The lru will pop the least recently used document when the cache is full. - if let Ok(doc) = self.get_document(doc_id).await { - if let Some(doc) = doc.try_lock() { + if let Some((doc_id, document)) = self.documents.remove(doc_id) { + if let Some(doc) = document.try_lock() { + // clear the awareness state when close the document + doc.clean_awareness_local_state(); let _ = doc.flush(); } + let clone_doc_id = doc_id.clone(); + trace!("move document to removing_documents: {}", doc_id); + self.removing_documents.insert(doc_id, document); + + let weak_removing_documents = Arc::downgrade(&self.removing_documents); + af_spawn(async move { + tokio::time::sleep(std::time::Duration::from_secs(120)).await; + if let Some(removing_documents) = weak_removing_documents.upgrade() { + if removing_documents.remove(&clone_doc_id).is_some() { + trace!("drop document from removing_documents: {}", clone_doc_id); + } + } + }); } Ok(()) @@ -215,13 +242,37 @@ impl DocumentManager { let uid = self.user_service.user_id()?; if let Some(db) = self.user_service.collab_db(uid)?.upgrade() { db.delete_doc(uid, doc_id).await?; - // When deleting a document, we need to remove it from the cache. - self.documents.lock().pop(doc_id); + self.documents.remove(doc_id); } Ok(()) } + pub async fn set_document_awareness_local_state( + &self, + doc_id: &str, + state: UpdateDocumentAwarenessStatePB, + ) -> FlowyResult { + let uid = self.user_service.user_id()?; + let device_id = self.user_service.device_id()?; + if let Ok(doc) = self.get_document(doc_id).await { + if let Some(doc) = doc.try_lock() { + let user = DocumentAwarenessUser { uid, device_id }; + let selection = state.selection.map(|s| s.into()); + let state = DocumentAwarenessState { + version: 1, + user, + selection, + metadata: state.metadata, + timestamp: timestamp(), + }; + doc.set_awareness_local_state(state); + return Ok(true); + } + } + Ok(false) + } + /// Return the list of snapshots of the document. pub async fn get_document_snapshot_meta( &self, @@ -284,7 +335,7 @@ impl DocumentManager { #[cfg(not(target_arch = "wasm32"))] { if tokio::fs::metadata(&local_file_path).await.is_ok() { - warn!("file already exist in user local disk: {}", local_file_path); + tracing::warn!("file already exist in user local disk: {}", local_file_path); return Ok(()); } @@ -298,7 +349,7 @@ impl DocumentManager { .await?; let n = file.write(&object_value.raw).await?; - info!("downloaded {} bytes to file: {}", n, local_file_path); + tracing::info!("downloaded {} bytes to file: {}", n, local_file_path); } Ok(()) } @@ -326,22 +377,19 @@ impl DocumentManager { &self, uid: i64, doc_id: &str, - doc_state: CollabDocState, + doc_state: DocStateSource, sync_enable: bool, ) -> FlowyResult> { let db = self.user_service.collab_db(uid)?; - let collab = self - .collab_builder - .build_with_config( - uid, - doc_id, - CollabType::Document, - db, - doc_state, - CollabPersistenceConfig::default().snapshot_per_update(1000), - CollabBuilderConfig::default().sync_enable(sync_enable), - ) - .await?; + let collab = self.collab_builder.build_with_config( + uid, + doc_id, + CollabType::Document, + db, + doc_state, + CollabPersistenceConfig::default().snapshot_per_update(1000), + CollabBuilderConfig::default().sync_enable(sync_enable), + )?; Ok(collab) } @@ -372,6 +420,16 @@ impl DocumentManager { pub fn get_file_storage_service(&self) -> &Weak { &self.storage_service } + + fn restore_document_from_removing(&self, doc_id: &str) -> Option> { + let (doc_id, doc) = self.removing_documents.remove(doc_id)?; + trace!( + "move document {} from removing_documents to documents", + doc_id + ); + self.documents.insert(doc_id, doc.clone()); + Some(doc) + } } async fn doc_state_from_document_data( @@ -385,9 +443,11 @@ async fn doc_state_from_document_data( CollabOrigin::Empty, doc_id, vec![], + false, ))); - let _ = Document::create_with_data(collab.clone(), data).map_err(internal_error)?; - Ok::<_, FlowyError>(collab.encode_collab_v1()) + let document = Document::create_with_data(collab.clone(), data).map_err(internal_error)?; + let encode_collab = document.encode_collab()?; + Ok::<_, FlowyError>(encode_collab) }) .await??; Ok(encoded_collab) diff --git a/frontend/rust-lib/flowy-document/src/notification.rs b/frontend/rust-lib/flowy-document/src/notification.rs index b468ec20c7..9909971667 100644 --- a/frontend/rust-lib/flowy-document/src/notification.rs +++ b/frontend/rust-lib/flowy-document/src/notification.rs @@ -11,6 +11,7 @@ pub enum DocumentNotification { DidReceiveUpdate = 1, DidUpdateDocumentSnapshotState = 2, DidUpdateDocumentSyncState = 3, + DidUpdateDocumentAwarenessState = 4, } impl std::convert::From for i32 { @@ -24,6 +25,7 @@ impl std::convert::From for DocumentNotification { 1 => DocumentNotification::DidReceiveUpdate, 2 => DocumentNotification::DidUpdateDocumentSnapshotState, 3 => DocumentNotification::DidUpdateDocumentSyncState, + 4 => DocumentNotification::DidUpdateDocumentAwarenessState, _ => DocumentNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-document/src/parser/constant.rs b/frontend/rust-lib/flowy-document/src/parser/constant.rs index 27e817114d..20edb93871 100644 --- a/frontend/rust-lib/flowy-document/src/parser/constant.rs +++ b/frontend/rust-lib/flowy-document/src/parser/constant.rs @@ -107,7 +107,6 @@ pub const TEXT_DECORATION: &str = "text-decoration"; pub const BACKGROUND_COLOR: &str = "background-color"; pub const TRANSPARENT: &str = "transparent"; -pub const DEFAULT_FONT_COLOR: &str = "rgb(0, 0, 0)"; pub const COLOR: &str = "color"; pub const LINE_THROUGH: &str = "line-through"; diff --git a/frontend/rust-lib/flowy-document/src/parser/external/utils.rs b/frontend/rust-lib/flowy-document/src/parser/external/utils.rs index 257f81e772..1e31792f2f 100644 --- a/frontend/rust-lib/flowy-document/src/parser/external/utils.rs +++ b/frontend/rust-lib/flowy-document/src/parser/external/utils.rs @@ -428,10 +428,6 @@ fn get_attributes_with_style(style: &str) -> HashMap { attributes.insert(BG_COLOR.to_string(), Value::String(value.to_string())); }, COLOR => { - if value.eq(DEFAULT_FONT_COLOR) { - continue; - } - attributes.insert(FONT_COLOR.to_string(), Value::String(value.to_string())); }, _ => {}, diff --git a/frontend/rust-lib/flowy-document/src/reminder.rs b/frontend/rust-lib/flowy-document/src/reminder.rs index ed26bb82b2..3a58ace1b2 100644 --- a/frontend/rust-lib/flowy-document/src/reminder.rs +++ b/frontend/rust-lib/flowy-document/src/reminder.rs @@ -1,6 +1,5 @@ use collab_entity::reminder::Reminder; use serde::{Deserialize, Serialize}; -use serde_json::json; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum DocumentReminderAction { @@ -18,6 +17,8 @@ impl TryFrom for DocumentReminder { type Error = serde_json::Error; fn try_from(value: Reminder) -> Result { - serde_json::from_value(json!(value.meta.into_inner())) + Ok(Self { + document_id: value.object_id, + }) } } diff --git a/frontend/rust-lib/flowy-document/tests/assets/json/simple.json b/frontend/rust-lib/flowy-document/tests/assets/json/simple.json index 9a27d97913..2ab6a3275e 100644 --- a/frontend/rust-lib/flowy-document/tests/assets/json/simple.json +++ b/frontend/rust-lib/flowy-document/tests/assets/json/simple.json @@ -2,7 +2,10 @@ "type": "page", "data": { "delta": [{ - "insert": "This is a paragraph" + "insert": "This is a paragraph", + "attributes": { + "font_color": "rgb(0, 0, 0)" + } }] }, "children": [] diff --git a/frontend/rust-lib/flowy-document/tests/document/util.rs b/frontend/rust-lib/flowy-document/tests/document/util.rs index 860d3b7a40..12af2008be 100644 --- a/frontend/rust-lib/flowy-document/tests/document/util.rs +++ b/frontend/rust-lib/flowy-document/tests/document/util.rs @@ -2,7 +2,6 @@ use std::ops::Deref; use std::sync::Arc; use anyhow::Error; -use collab::core::collab::CollabDocState; use collab::preclude::CollabPlugin; use collab_document::blocks::DocumentData; use collab_document::document_data::default_document_data; @@ -24,7 +23,7 @@ use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; use flowy_storage::ObjectStorageService; use lib_infra::async_trait::async_trait; -use lib_infra::future::{to_fut, Fut, FutureResult}; +use lib_infra::future::FutureResult; pub struct DocumentTest { inner: DocumentManager, @@ -83,6 +82,10 @@ impl DocumentUserService for FakeUser { fn collab_db(&self, _uid: i64) -> Result, FlowyError> { Ok(Arc::downgrade(&self.collab_db)) } + + fn device_id(&self) -> Result { + Ok("".to_string()) + } } pub fn setup_log() { @@ -135,7 +138,7 @@ impl DocumentCloudService for LocalTestDocumentCloudServiceImpl { &self, document_id: &str, _workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult, FlowyError> { let document_id = document_id.to_string(); FutureResult::new(async move { Err(FlowyError::new( @@ -197,8 +200,8 @@ impl CollabCloudPluginProvider for DefaultCollabStorageProvider { CollabPluginProviderType::Local } - fn get_plugins(&self, _context: CollabPluginProviderContext) -> Fut>> { - to_fut(async move { vec![] }) + fn get_plugins(&self, _context: CollabPluginProviderContext) -> Vec> { + vec![] } fn is_sync_enabled(&self) -> bool { diff --git a/frontend/rust-lib/flowy-error/build.rs b/frontend/rust-lib/flowy-error/build.rs index 47839f938f..c3081d7488 100644 --- a/frontend/rust-lib/flowy-error/build.rs +++ b/frontend/rust-lib/flowy-error/build.rs @@ -3,11 +3,18 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); #[cfg(feature = "tauri_ts")] - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); + { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } #[cfg(feature = "web_ts")] flowy_codegen::protobuf_file::ts_gen( diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 8cc78a4ca0..4ddd316a8e 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -259,6 +259,12 @@ pub enum ErrorCode { #[error("Cloud request payload too large")] CloudRequestPayloadTooLarge = 90, + + #[error("Workspace limit exceeded")] + WorkspaceLimitExceeded = 91, + + #[error("Workspace member limit exceeded")] + WorkspaceMemberLimitExceeded = 92, } impl ErrorCode { diff --git a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs index c45bfb16c1..3c38bc4005 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -21,6 +21,11 @@ impl From for FlowyError { AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, AppErrorCode::NetworkError => ErrorCode::HttpError, AppErrorCode::PayloadTooLarge => ErrorCode::CloudRequestPayloadTooLarge, + AppErrorCode::UserUnAuthorized => match &*error.message { + "Workspace Limit Exceeded" => ErrorCode::WorkspaceLimitExceeded, + "Workspace Member Limit Exceeded" => ErrorCode::WorkspaceMemberLimitExceeded, + _ => ErrorCode::UserUnauthorized, + }, _ => ErrorCode::Internal, }; diff --git a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs index 316795ca59..cee216a217 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -1,5 +1,4 @@ pub use anyhow::Error; -use collab::core::collab::CollabDocState; use collab_entity::CollabType; pub use collab_folder::{Folder, FolderData, Workspace}; use uuid::Uuid; @@ -36,7 +35,7 @@ pub trait FolderCloudService: Send + Sync + 'static { uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult; + ) -> FutureResult, Error>; fn batch_create_folder_collab_objects( &self, diff --git a/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs b/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs index 7a7c7ca030..26c5368398 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/folder_builder.rs @@ -59,6 +59,7 @@ impl ViewBuilder { layout: ViewLayout::Document, child_views: vec![], is_favorite: false, + icon: None, } } diff --git a/frontend/rust-lib/flowy-folder/build.rs b/frontend/rust-lib/flowy-folder/build.rs index 0ea0f628f7..fac4cc65ae 100644 --- a/frontend/rust-lib/flowy-folder/build.rs +++ b/frontend/rust-lib/flowy-folder/build.rs @@ -13,6 +13,12 @@ fn main() { env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri, ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); } #[cfg(feature = "web_ts")] diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 7b756163a6..99412188d8 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -59,14 +59,14 @@ pub struct ViewPB { pub is_favorite: bool, } -pub fn view_pb_without_child_views(view: Arc) -> ViewPB { +pub fn view_pb_without_child_views(view: View) -> ViewPB { ViewPB { - id: view.id.clone(), - parent_view_id: view.parent_view_id.clone(), - name: view.name.clone(), + id: view.id, + parent_view_id: view.parent_view_id, + name: view.name, create_time: view.created_at, child_views: Default::default(), - layout: view.layout.clone().into(), + layout: view.layout.into(), icon: view.icon.clone().map(|icon| icon.into()), is_favorite: view.is_favorite, } @@ -81,7 +81,7 @@ pub fn view_pb_with_child_views(view: Arc, child_views: Vec>) -> create_time: view.created_at, child_views: child_views .into_iter() - .map(view_pb_without_child_views) + .map(|view| view_pb_without_child_views(view.as_ref().clone())) .collect(), layout: view.layout.clone().into(), icon: view.icon.clone().map(|icon| icon.into()), @@ -118,6 +118,15 @@ impl std::convert::From for ViewLayoutPB { } } +#[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] +pub struct SectionViewsPB { + #[pb(index = 1)] + pub section: ViewSectionPB, + + #[pb(index = 2)] + pub views: Vec, +} + #[derive(Eq, PartialEq, Debug, Default, ProtoBuf, Clone)] pub struct RepeatedViewPB { #[pb(index = 1)] @@ -181,6 +190,20 @@ pub struct CreateViewPayloadPB { // If the index is None or the index is out of range, the view will be appended to the end of the parent view. #[pb(index = 9, one_of)] pub index: Option, + + // The section of the view. + // Only the view in public section will be shown in the shared workspace view list. + // The view in private section will only be shown in the user's private view list. + #[pb(index = 10, one_of)] + pub section: Option, +} + +#[derive(Eq, PartialEq, Hash, Debug, ProtoBuf_Enum, Clone, Default)] +pub enum ViewSectionPB { + #[default] + // only support public and private section now. + Private = 0, + Public = 1, } /// The orphan view is meant to be a view that is not attached to any parent view. By default, this @@ -218,6 +241,8 @@ pub struct CreateViewParams { // The index of the view in the parent view. // If the index is None or the index is out of range, the view will be appended to the end of the parent view. pub index: Option, + // The section of the view. + pub section: Option, } impl TryInto for CreateViewPayloadPB { @@ -238,6 +263,7 @@ impl TryInto for CreateViewPayloadPB { meta: self.meta, set_as_current: self.set_as_current, index: self.index, + section: self.section, }) } } @@ -259,6 +285,7 @@ impl TryInto for CreateOrphanViewPayloadPB { meta: Default::default(), set_as_current: false, index: None, + section: None, }) } } @@ -384,6 +411,12 @@ pub struct MoveNestedViewPayloadPB { #[pb(index = 3, one_of)] pub prev_view_id: Option, + + #[pb(index = 4, one_of)] + pub from_section: Option, + + #[pb(index = 5, one_of)] + pub to_section: Option, } pub struct MoveViewParams { @@ -405,10 +438,13 @@ impl TryInto for MoveViewPayloadPB { } } +#[derive(Debug)] pub struct MoveNestedViewParams { pub view_id: String, pub new_parent_id: String, pub prev_view_id: Option, + pub from_section: Option, + pub to_section: Option, } impl TryInto for MoveNestedViewPayloadPB { @@ -422,6 +458,8 @@ impl TryInto for MoveNestedViewPayloadPB { view_id, new_parent_id, prev_view_id, + from_section: self.from_section, + to_section: self.to_section, }) } } @@ -437,6 +475,15 @@ pub struct UpdateRecentViewPayloadPB { pub add_in_recent: bool, } +#[derive(Default, ProtoBuf)] +pub struct UpdateViewVisibilityStatusPayloadPB { + #[pb(index = 1)] + pub view_ids: Vec, + + #[pb(index = 2)] + pub is_public: bool, +} + // impl<'de> Deserialize<'de> for ViewDataType { // fn deserialize(deserializer: D) -> Result>::Error> // where diff --git a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs index 6ce3328da6..21ff046226 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/workspace.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/workspace.rs @@ -97,6 +97,42 @@ pub struct WorkspaceIdPB { pub value: String, } +#[derive(Clone, Debug)] +pub struct WorkspaceIdParams { + pub value: String, +} + +impl TryInto for WorkspaceIdPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + Ok(WorkspaceIdParams { + value: WorkspaceIdentify::parse(self.value)?.0, + }) + } +} + +#[derive(Clone, ProtoBuf, Default, Debug)] +pub struct GetWorkspaceViewPB { + #[pb(index = 1)] + pub value: String, +} + +#[derive(Clone, Debug)] +pub struct GetWorkspaceViewParams { + pub value: String, +} + +impl TryInto for GetWorkspaceViewPB { + type Error = ErrorCode; + + fn try_into(self) -> Result { + Ok(GetWorkspaceViewParams { + value: WorkspaceIdentify::parse(self.value)?.0, + }) + } +} + #[derive(Default, ProtoBuf, Debug, Clone)] pub struct WorkspaceSettingPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index a5412decc6..92d0c753aa 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -1,4 +1,5 @@ use std::sync::{Arc, Weak}; +use tracing::instrument; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; @@ -28,7 +29,7 @@ pub(crate) async fn create_workspace_handler( .get_views_belong_to(&workspace.id) .await? .into_iter() - .map(view_pb_without_child_views) + .map(|view| view_pb_without_child_views(view.as_ref().clone())) .collect::>(); data_result_ok(WorkspacePB { id: workspace.id, @@ -48,10 +49,34 @@ pub(crate) async fn get_all_workspace_handler( #[tracing::instrument(level = "debug", skip(folder), err)] pub(crate) async fn get_workspace_views_handler( + data: AFPluginData, folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let child_views = folder.get_current_workspace_views().await?; + let params: GetWorkspaceViewParams = data.into_inner().try_into()?; + let child_views = folder.get_workspace_public_views(¶ms.value).await?; + let repeated_view: RepeatedViewPB = child_views.into(); + data_result_ok(repeated_view) +} + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn get_current_workspace_views_handler( + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let child_views = folder.get_current_workspace_public_views().await?; + let repeated_view: RepeatedViewPB = child_views.into(); + data_result_ok(repeated_view) +} + +#[tracing::instrument(level = "debug", skip(folder), err)] +pub(crate) async fn read_private_views_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> DataResult { + let folder = upgrade_folder(folder)?; + let params: GetWorkspaceViewParams = data.into_inner().try_into()?; + let child_views = folder.get_workspace_private_views(¶ms.value).await?; let repeated_view: RepeatedViewPB = child_views.into(); data_result_ok(repeated_view) } @@ -85,7 +110,7 @@ pub(crate) async fn create_view_handler( if set_as_current { let _ = folder.set_current_view(&view.id).await; } - data_result_ok(view_pb_without_child_views(Arc::new(view))) + data_result_ok(view_pb_without_child_views(view)) } pub(crate) async fn create_orphan_view_handler( @@ -99,11 +124,11 @@ pub(crate) async fn create_orphan_view_handler( if set_as_current { let _ = folder.set_current_view(&view.id).await; } - data_result_ok(view_pb_without_child_views(Arc::new(view))) + data_result_ok(view_pb_without_child_views(view)) } #[tracing::instrument(level = "debug", skip(data, folder), err)] -pub(crate) async fn read_view_handler( +pub(crate) async fn get_view_handler( data: AFPluginData, folder: AFPluginState>, ) -> DataResult { @@ -183,6 +208,7 @@ pub(crate) async fn set_latest_view_handler( Ok(()) } +#[instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn close_view_handler( data: AFPluginData, folder: AFPluginState>, @@ -212,9 +238,7 @@ pub(crate) async fn move_nested_view_handler( ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; let params: MoveNestedViewParams = data.into_inner().try_into()?; - folder - .move_nested_view(params.view_id, params.new_parent_id, params.prev_view_id) - .await?; + folder.move_nested_view(params).await?; Ok(()) } @@ -249,7 +273,7 @@ pub(crate) async fn read_recent_views_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let recent_items = folder.get_all_recent_sections().await; + let recent_items = folder.get_my_recent_sections().await; let mut views = vec![]; for item in recent_items { if let Ok(view) = folder.get_view_pb(&item.id).await { @@ -264,7 +288,7 @@ pub(crate) async fn read_trash_handler( folder: AFPluginState>, ) -> DataResult { let folder = upgrade_folder(folder)?; - let trash = folder.get_all_trash().await; + let trash = folder.get_my_trash_info().await; data_result_ok(trash.into()) } @@ -301,11 +325,11 @@ pub(crate) async fn restore_all_trash_handler( } #[tracing::instrument(level = "debug", skip(folder), err)] -pub(crate) async fn delete_all_trash_handler( +pub(crate) async fn delete_my_trash_handler( folder: AFPluginState>, ) -> Result<(), FlowyError> { let folder = upgrade_folder(folder)?; - folder.delete_all_trash().await; + folder.delete_my_trash().await; Ok(()) } @@ -313,11 +337,12 @@ pub(crate) async fn delete_all_trash_handler( pub(crate) async fn import_data_handler( data: AFPluginData, folder: AFPluginState>, -) -> Result<(), FlowyError> { +) -> DataResult { let folder = upgrade_folder(folder)?; let params: ImportParams = data.into_inner().try_into()?; - folder.import(params).await?; - Ok(()) + let view = folder.import(params).await?; + let view_pb = view_pb_without_child_views(view); + data_result_ok(view_pb) } #[tracing::instrument(level = "debug", skip(folder), err)] @@ -338,3 +363,14 @@ pub(crate) async fn reload_workspace_handler( folder.reload_workspace().await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip(data, folder), err)] +pub(crate) async fn update_view_visibility_status_handler( + data: AFPluginData, + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + let params = data.into_inner(); + folder.set_views_visibility(params.view_ids, params.is_public); + Ok(()) +} diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 6a4d1faa20..fc4e5953ef 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -11,13 +11,13 @@ use crate::manager::FolderManager; pub fn init(folder: Weak) -> AFPlugin { AFPlugin::new().name("Flowy-Folder").state(folder) // Workspace - .event(FolderEvent::CreateWorkspace, create_workspace_handler) + .event(FolderEvent::CreateFolderWorkspace, create_workspace_handler) .event(FolderEvent::GetCurrentWorkspaceSetting, read_current_workspace_setting_handler) .event(FolderEvent::ReadCurrentWorkspace, read_current_workspace_handler) .event(FolderEvent::ReadWorkspaceViews, get_workspace_views_handler) .event(FolderEvent::CreateView, create_view_handler) .event(FolderEvent::CreateOrphanView, create_orphan_view_handler) - .event(FolderEvent::GetView, read_view_handler) + .event(FolderEvent::GetView, get_view_handler) .event(FolderEvent::UpdateView, update_view_handler) .event(FolderEvent::DeleteView, delete_view_handler) .event(FolderEvent::DuplicateView, duplicate_view_handler) @@ -29,7 +29,7 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::RestoreTrashItem, putback_trash_handler) .event(FolderEvent::PermanentlyDeleteTrashItem, delete_trash_handler) .event(FolderEvent::RecoverAllTrashItems, restore_all_trash_handler) - .event(FolderEvent::PermanentlyDeleteAllTrashItem, delete_all_trash_handler) + .event(FolderEvent::PermanentlyDeleteAllTrashItem, delete_my_trash_handler) .event(FolderEvent::ImportData, import_data_handler) .event(FolderEvent::GetFolderSnapshots, get_folder_snapshots_handler) .event(FolderEvent::UpdateViewIcon, update_view_icon_handler) @@ -38,6 +38,9 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::ToggleFavorite, toggle_favorites_handler) .event(FolderEvent::UpdateRecentViews, update_recent_views_handler) .event(FolderEvent::ReloadWorkspace, reload_workspace_handler) + .event(FolderEvent::ReadPrivateViews, read_private_views_handler) + .event(FolderEvent::ReadCurrentWorkspaceViews, get_current_workspace_views_handler) + .event(FolderEvent::UpdateViewVisibilityStatus, update_view_visibility_status_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -45,7 +48,7 @@ pub fn init(folder: Weak) -> AFPlugin { pub enum FolderEvent { /// Create a new workspace #[event(input = "CreateWorkspacePayloadPB", output = "WorkspacePB")] - CreateWorkspace = 0, + CreateFolderWorkspace = 0, /// Read the current opening workspace. Currently, we only support one workspace #[event(output = "WorkspaceSettingPB")] @@ -59,9 +62,9 @@ pub enum FolderEvent { #[event(input = "WorkspaceIdPB")] DeleteWorkspace = 3, - /// Return a list of views of the current workspace. + /// Return a list of views of the specified workspace. /// Only the first level of child views are included. - #[event(input = "WorkspaceIdPB", output = "RepeatedViewPB")] + #[event(input = "GetWorkspaceViewPB", output = "RepeatedViewPB")] ReadWorkspaceViews = 5, /// Create a new view in the corresponding app @@ -124,7 +127,7 @@ pub enum FolderEvent { #[event()] PermanentlyDeleteAllTrashItem = 27, - #[event(input = "ImportPB")] + #[event(input = "ImportPB", output = "ViewPB")] ImportData = 30, #[event(input = "WorkspaceIdPB", output = "RepeatedFolderSnapshotPB")] @@ -156,4 +159,15 @@ pub enum FolderEvent { #[event()] ReloadWorkspace = 38, + + #[event(input = "GetWorkspaceViewPB", output = "RepeatedViewPB")] + ReadPrivateViews = 39, + + /// Return a list of views of the current workspace. + /// Only the first level of child views are included. + #[event(output = "RepeatedViewPB")] + ReadCurrentWorkspaceViews = 40, + + #[event(input = "UpdateViewVisibilityStatusPayloadPB")] + UpdateViewVisibilityStatus = 41, } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 04c1b2b4fc..42cc4467e6 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -1,30 +1,8 @@ -use std::fmt::{Display, Formatter}; -use std::ops::Deref; -use std::sync::{Arc, Weak}; - -use collab::core::collab::{CollabDocState, MutexCollab}; -use collab_entity::CollabType; -use collab_folder::error::FolderError; -use collab_folder::{ - Folder, FolderData, FolderNotify, Section, SectionItem, TrashInfo, UserId, View, ViewLayout, - ViewUpdate, Workspace, -}; -use parking_lot::{Mutex, RwLock}; -use tracing::{error, info, instrument}; - -use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; -use collab_integrate::{CollabKVDB, CollabPersistenceConfig}; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService}; -use flowy_folder_pub::folder_builder::ParentChildViews; - -use lib_infra::conditional_send_sync_trait; - use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, CreateViewParams, CreateWorkspaceParams, - DeletedViewPB, FolderSnapshotPB, RepeatedTrashPB, RepeatedViewIdPB, RepeatedViewPB, - UpdateViewParams, ViewPB, WorkspacePB, WorkspaceSettingPB, + DeletedViewPB, FolderSnapshotPB, MoveNestedViewParams, RepeatedTrashPB, RepeatedViewIdPB, + RepeatedViewPB, UpdateViewParams, ViewPB, ViewSectionPB, WorkspacePB, WorkspaceSettingPB, }; use crate::manager_observer::{ notify_child_views_changed, notify_did_update_workspace, notify_parent_view_did_change, @@ -38,6 +16,24 @@ use crate::util::{ folder_not_init_error, insert_parent_child_views, workspace_data_not_sync_error, }; use crate::view_operation::{create_view, FolderOperationHandler, FolderOperationHandlers}; +use collab::core::collab::{DocStateSource, MutexCollab}; +use collab_entity::CollabType; +use collab_folder::error::FolderError; +use collab_folder::{ + Folder, FolderData, FolderNotify, Section, SectionItem, TrashInfo, UserId, View, ViewLayout, + ViewUpdate, Workspace, +}; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; +use collab_integrate::{CollabKVDB, CollabPersistenceConfig}; +use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_folder_pub::cloud::{gen_view_id, FolderCloudService}; +use flowy_folder_pub::folder_builder::ParentChildViews; +use lib_infra::conditional_send_sync_trait; +use parking_lot::{Mutex, RwLock}; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; +use std::sync::{Arc, Weak}; +use tracing::{error, info, instrument}; conditional_send_sync_trait! { "[crate::manager::FolderUser] represents the user for folder."; @@ -113,7 +109,7 @@ impl FolderManager { }, |folder| { let workspace_pb_from_workspace = |workspace: Workspace, folder: &Folder| { - let views = get_workspace_view_pbs(&workspace.id, folder); + let views = get_workspace_public_view_pbs(&workspace.id, folder); let workspace: WorkspacePB = (workspace, views).into(); Ok::(workspace) }; @@ -128,7 +124,7 @@ impl FolderManager { /// Return a list of views of the current workspace. /// Only the first level of child views are included. - pub async fn get_current_workspace_views(&self) -> FlowyResult> { + pub async fn get_current_workspace_public_views(&self) -> FlowyResult> { let workspace_id = self .mutex_folder .lock() @@ -136,16 +132,24 @@ impl FolderManager { .map(|folder| folder.get_workspace_id()); if let Some(workspace_id) = workspace_id { - self.get_workspace_views(&workspace_id).await + self.get_workspace_public_views(&workspace_id).await } else { tracing::warn!("Can't get current workspace views"); Ok(vec![]) } } - pub async fn get_workspace_views(&self, workspace_id: &str) -> FlowyResult> { + pub async fn get_workspace_public_views(&self, workspace_id: &str) -> FlowyResult> { let views = self.with_folder(Vec::new, |folder| { - get_workspace_view_pbs(workspace_id, folder) + get_workspace_public_view_pbs(workspace_id, folder) + }); + + Ok(views) + } + + pub async fn get_workspace_private_views(&self, workspace_id: &str) -> FlowyResult> { + let views = self.with_folder(Vec::new, |folder| { + get_workspace_private_view_pbs(workspace_id, folder) }); Ok(views) @@ -156,24 +160,21 @@ impl FolderManager { uid: i64, workspace_id: &str, collab_db: Weak, - collab_doc_state: CollabDocState, + doc_state: DocStateSource, folder_notifier: T, ) -> Result { let folder_notifier = folder_notifier.into(); - let collab = self - .collab_builder - .build_with_config( - uid, - workspace_id, - CollabType::Folder, - collab_db, - collab_doc_state, - CollabPersistenceConfig::new() - .enable_snapshot(true) - .snapshot_per_update(50), - CollabBuilderConfig::default().sync_enable(true), - ) - .await?; + let collab = self.collab_builder.build_with_config( + uid, + workspace_id, + CollabType::Folder, + collab_db, + doc_state, + CollabPersistenceConfig::new() + .enable_snapshot(true) + .snapshot_per_update(50), + CollabBuilderConfig::default().sync_enable(true), + )?; let (should_clear, err) = match Folder::open(UserId::from(uid), collab, folder_notifier) { Ok(folder) => { return Ok(folder); @@ -199,20 +200,17 @@ impl FolderManager { workspace_id: &str, collab_db: Weak, ) -> Result, FlowyError> { - let collab = self - .collab_builder - .build_with_config( - uid, - workspace_id, - CollabType::Folder, - collab_db, - vec![], - CollabPersistenceConfig::new() - .enable_snapshot(true) - .snapshot_per_update(50), - CollabBuilderConfig::default().sync_enable(true), - ) - .await?; + let collab = self.collab_builder.build_with_config( + uid, + workspace_id, + CollabType::Folder, + collab_db, + DocStateSource::FromDisk, + CollabPersistenceConfig::new() + .enable_snapshot(true) + .snapshot_per_update(50), + CollabBuilderConfig::default().sync_enable(true), + )?; Ok(collab) } @@ -374,7 +372,7 @@ impl FolderManager { .views .get_views_belong_to(&workspace.id) .into_iter() - .map(view_pb_without_child_views) + .map(|view| view_pb_without_child_views(view.as_ref().clone())) .collect::>(); WorkspacePB { @@ -452,11 +450,16 @@ impl FolderManager { } let index = params.index; + let section = params.section.clone().unwrap_or(ViewSectionPB::Public); + let is_private = section == ViewSectionPB::Private; let view = create_view(self.user.user_id()?, params, view_layout); self.with_folder( || (), |folder| { folder.insert_view(view.clone(), index); + if is_private { + folder.add_private_view_ids(vec![view.id.clone()]); + } }, ); @@ -506,7 +509,7 @@ impl FolderManager { let folder = self.mutex_folder.lock(); let folder = folder.as_ref().ok_or_else(folder_not_init_error)?; let trash_ids = folder - .get_all_trash() + .get_all_trash_sections() .into_iter() .map(|trash| trash.id) .collect::>(); @@ -546,7 +549,7 @@ impl FolderManager { |folder| { if let Some(view) = folder.views.get_view(view_id) { self.unfavorite_view_and_decendants(view.clone(), folder); - folder.add_trash(vec![view_id.to_string()]); + folder.add_trash_view_ids(vec![view_id.to_string()]); // notify the parent view that the view is moved to trash send_notification(view_id, FolderNotification::DidMoveViewToTrash) .payload(DeletedViewPB { @@ -556,7 +559,7 @@ impl FolderManager { .send(); notify_child_views_changed( - view_pb_without_child_views(view), + view_pb_without_child_views(view.as_ref().clone()), ChildViewChangeReason::Delete, ); } @@ -573,11 +576,11 @@ impl FolderManager { let favorite_descendant_views: Vec = all_descendant_views .iter() .filter(|view| view.is_favorite) - .map(|view| view_pb_without_child_views(view.clone())) + .map(|view| view_pb_without_child_views(view.as_ref().clone())) .collect(); if !favorite_descendant_views.is_empty() { - folder.delete_favorites( + folder.delete_favorite_view_ids( favorite_descendant_views .iter() .map(|v| v.id.clone()) @@ -609,18 +612,26 @@ impl FolderManager { /// * `prev_view_id` - An `Option` that holds the id of the view after which the `view_id` should be positioned. /// #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn move_nested_view( - &self, - view_id: String, - new_parent_id: String, - prev_view_id: Option, - ) -> FlowyResult<()> { + pub async fn move_nested_view(&self, params: MoveNestedViewParams) -> FlowyResult<()> { + let view_id = params.view_id; + let new_parent_id = params.new_parent_id; + let prev_view_id = params.prev_view_id; + let from_section = params.from_section; + let to_section = params.to_section; let view = self.get_view_pb(&view_id).await?; let old_parent_id = view.parent_view_id; self.with_folder( || (), |folder| { folder.move_nested_view(&view_id, &new_parent_id, prev_view_id); + + if from_section != to_section { + if to_section == Some(ViewSectionPB::Private) { + folder.add_private_view_ids(vec![view_id.clone()]); + } else { + folder.delete_private_view_ids(vec![view_id.clone()]); + } + } }, ); notify_parent_view_did_change( @@ -733,6 +744,16 @@ impl FolderManager { None }; + let is_private = self.with_folder( + || false, + |folder| folder.is_view_in_section(Section::Private, &view.id), + ); + let section = if is_private { + ViewSectionPB::Private + } else { + ViewSectionPB::Public + }; + let duplicate_params = CreateViewParams { parent_view_id: view.parent_view_id.clone(), name: format!("{} (copy)", &view.name), @@ -743,6 +764,7 @@ impl FolderManager { meta: Default::default(), set_as_current: true, index, + section: Some(section), }; self.create_view_with_params(duplicate_params).await?; @@ -760,7 +782,15 @@ impl FolderManager { }, )?; - send_workspace_setting_notification(workspace_id, self.get_current_view().await); + let view = self.get_current_view().await; + if let Some(view) = &view { + let view_layout: ViewLayout = view.layout.clone().into(); + if let Some(handle) = self.operation_handlers.get(&view_layout) { + let _ = handle.open_view(view_id).await; + } + } + + send_workspace_setting_notification(workspace_id, view); Ok(()) } @@ -778,9 +808,9 @@ impl FolderManager { |folder| { if let Some(old_view) = folder.views.get_view(view_id) { if old_view.is_favorite { - folder.delete_favorites(vec![view_id.to_string()]); + folder.delete_favorite_view_ids(vec![view_id.to_string()]); } else { - folder.add_favorites(vec![view_id.to_string()]); + folder.add_favorite_view_ids(vec![view_id.to_string()]); } } }, @@ -836,7 +866,7 @@ impl FolderManager { } async fn send_update_recent_views_notification(&self) { - let recent_views = self.get_all_recent_sections().await; + let recent_views = self.get_my_recent_sections().await; send_notification("recent_views", FolderNotification::DidUpdateRecentViews) .payload(RepeatedViewIdPB { items: recent_views.into_iter().map(|item| item.id).collect(), @@ -849,14 +879,14 @@ impl FolderManager { self.get_sections(Section::Favorite) } - #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn get_all_recent_sections(&self) -> Vec { + #[tracing::instrument(level = "debug", skip(self))] + pub(crate) async fn get_my_recent_sections(&self) -> Vec { self.get_sections(Section::Recent) } #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn get_all_trash(&self) -> Vec { - self.with_folder(Vec::new, |folder| folder.get_all_trash()) + pub(crate) async fn get_my_trash_info(&self) -> Vec { + self.with_folder(Vec::new, |folder| folder.get_my_trash_info()) } #[tracing::instrument(level = "trace", skip(self))] @@ -864,7 +894,7 @@ impl FolderManager { self.with_folder( || (), |folder| { - folder.remote_all_trash(); + folder.remove_all_my_trash_sections(); }, ); send_notification("trash", FolderNotification::DidUpdateTrash) @@ -877,15 +907,15 @@ impl FolderManager { self.with_folder( || (), |folder| { - folder.delete_trash(vec![trash_id.to_string()]); + folder.delete_trash_view_ids(vec![trash_id.to_string()]); }, ); } /// Delete all the trash permanently. #[tracing::instrument(level = "trace", skip(self))] - pub(crate) async fn delete_all_trash(&self) { - let deleted_trash = self.with_folder(Vec::new, |folder| folder.get_all_trash()); + pub(crate) async fn delete_my_trash(&self) { + let deleted_trash = self.with_folder(Vec::new, |folder| folder.get_my_trash_info()); for trash in deleted_trash { let _ = self.delete_trash(&trash.id).await; } @@ -903,7 +933,7 @@ impl FolderManager { self.with_folder( || (), |folder| { - folder.delete_trash(vec![view_id.to_string()]); + folder.delete_trash_view_ids(vec![view_id.to_string()]); folder.views.delete_views(vec![view_id]); }, ); @@ -954,6 +984,7 @@ impl FolderManager { meta: Default::default(), set_as_current: false, index: None, + section: None, }; let view = create_view(self.user.user_id()?, params, import_data.view_layout); @@ -1089,37 +1120,148 @@ impl FolderManager { &self.cloud_service } + pub fn set_views_visibility(&self, view_ids: Vec, is_public: bool) { + self.with_folder( + || (), + |folder| { + if is_public { + folder.delete_private_view_ids(view_ids); + } else { + folder.add_private_view_ids(view_ids); + } + }, + ); + } + + /// Only support getting the Favorite and Recent sections. fn get_sections(&self, section_type: Section) -> Vec { self.with_folder(Vec::new, |folder| { - let trash_ids = folder - .get_all_trash() - .into_iter() - .map(|trash| trash.id) - .collect::>(); - - let mut views = match section_type { - Section::Favorite => folder.get_all_favorites(), - Section::Recent => folder.get_all_recent_sections(), + let views = match section_type { + Section::Favorite => folder.get_my_favorite_sections(), + Section::Recent => folder.get_my_recent_sections(), _ => vec![], }; - - // filter the views that are in the trash - views.retain(|view| !trash_ids.contains(&view.id)); + let view_ids_should_be_filtered = self.get_view_ids_should_be_filtered(folder); views + .into_iter() + .filter(|view| !view_ids_should_be_filtered.contains(&view.id)) + .collect() }) } + + /// Get all the view that are in the trash, including the child views of the child views. + /// For example, if A view which is in the trash has a child view B, this function will return + /// both A and B. + fn get_all_trash_ids(&self, folder: &Folder) -> Vec { + let trash_ids = folder + .get_all_trash_sections() + .into_iter() + .map(|trash| trash.id) + .collect::>(); + let mut all_trash_ids = trash_ids.clone(); + for trash_id in trash_ids { + all_trash_ids.extend(get_all_child_view_ids(folder, &trash_id)); + } + all_trash_ids + } + + /// Filter the views that are in the trash and belong to the other private sections. + fn get_view_ids_should_be_filtered(&self, folder: &Folder) -> Vec { + let trash_ids = self.get_all_trash_ids(folder); + let other_private_view_ids = self.get_other_private_view_ids(folder); + [trash_ids, other_private_view_ids].concat() + } + + fn get_other_private_view_ids(&self, folder: &Folder) -> Vec { + let my_private_view_ids = folder + .get_my_private_sections() + .into_iter() + .map(|view| view.id) + .collect::>(); + + let all_private_view_ids = folder + .get_all_private_sections() + .into_iter() + .map(|view| view.id) + .collect::>(); + + all_private_view_ids + .into_iter() + .filter(|id| !my_private_view_ids.contains(id)) + .collect() + } } -/// Return the views that belong to the workspace. The views are filtered by the trash. -pub(crate) fn get_workspace_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { - let items = folder.get_all_trash(); - let trash_ids = items +/// Return the views that belong to the workspace. The views are filtered by the trash and all the private views. +pub(crate) fn get_workspace_public_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { + // get the trash ids + let trash_ids = folder + .get_all_trash_sections() .into_iter() .map(|trash| trash.id) .collect::>(); + // get the private view ids + let private_view_ids = folder + .get_all_private_sections() + .into_iter() + .map(|view| view.id) + .collect::>(); + let mut views = folder.get_workspace_views(); - views.retain(|view| !trash_ids.contains(&view.id)); + + // filter the views that are in the trash and all the private views + views.retain(|view| !trash_ids.contains(&view.id) && !private_view_ids.contains(&view.id)); + + views + .into_iter() + .map(|view| { + // Get child views + let child_views = folder + .views + .get_views_belong_to(&view.id) + .into_iter() + .collect(); + view_pb_with_child_views(view, child_views) + }) + .collect() +} + +/// Get all the child views belong to the view id, including the child views of the child views. +fn get_all_child_view_ids(folder: &Folder, view_id: &str) -> Vec { + let child_view_ids = folder + .views + .get_views_belong_to(view_id) + .into_iter() + .map(|view| view.id.clone()) + .collect::>(); + let mut all_child_view_ids = child_view_ids.clone(); + for child_view_id in child_view_ids { + all_child_view_ids.extend(get_all_child_view_ids(folder, &child_view_id)); + } + all_child_view_ids +} + +/// Get the current private views of the user. +pub(crate) fn get_workspace_private_view_pbs(_workspace_id: &str, folder: &Folder) -> Vec { + // get the trash ids + let trash_ids = folder + .get_all_trash_sections() + .into_iter() + .map(|trash| trash.id) + .collect::>(); + + // get the private view ids + let private_view_ids = folder + .get_my_private_sections() + .into_iter() + .map(|view| view.id) + .collect::>(); + + let mut views = folder.get_workspace_views(); + + // filter the views that are in the trash and not in the private view ids + views.retain(|view| !trash_ids.contains(&view.id) && private_view_ids.contains(&view.id)); views .into_iter() @@ -1151,7 +1293,7 @@ pub enum FolderInitDataSource { /// It means using the data stored on local disk to initialize the folder LocalDisk { create_if_not_exist: bool }, /// If there is no data stored on local disk, we will use the data from the server to initialize the folder - Cloud(CollabDocState), + Cloud(Vec), /// If the user is new, we use the [DefaultFolderBuilder] to create the default folder. FolderData(FolderData), } diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index f73ea35953..b3dbf98364 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -6,6 +6,7 @@ use collab_integrate::CollabKVDB; use flowy_error::{FlowyError, FlowyResult}; +use collab::core::collab::DocStateSource; use std::sync::{Arc, Weak}; use tracing::{event, Level}; @@ -54,7 +55,13 @@ impl FolderManager { if is_exist { event!(Level::INFO, "Init folder from local disk"); self - .make_folder(uid, &workspace_id, collab_db, vec![], folder_notifier) + .make_folder( + uid, + &workspace_id, + collab_db, + DocStateSource::FromDisk, + folder_notifier, + ) .await? } else if create_if_not_exist { // 2. if the folder doesn't exist and create_if_not_exist is true, create a default folder @@ -76,7 +83,7 @@ impl FolderManager { uid, &workspace_id, collab_db.clone(), - doc_state, + DocStateSource::FromDocState(doc_state), folder_notifier.clone(), ) .await? @@ -86,7 +93,13 @@ impl FolderManager { if doc_state.is_empty() { event!(Level::ERROR, "remote folder data is empty, open from local"); self - .make_folder(uid, &workspace_id, collab_db, vec![], folder_notifier) + .make_folder( + uid, + &workspace_id, + collab_db, + DocStateSource::FromDisk, + folder_notifier, + ) .await? } else { event!(Level::INFO, "Restore folder from remote data"); @@ -95,7 +108,7 @@ impl FolderManager { uid, &workspace_id, collab_db.clone(), - doc_state, + DocStateSource::FromDocState(doc_state), folder_notifier.clone(), ) .await? diff --git a/frontend/rust-lib/flowy-folder/src/manager_observer.rs b/frontend/rust-lib/flowy-folder/src/manager_observer.rs index e37f20f31b..e0b9f325ec 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_observer.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_observer.rs @@ -14,9 +14,9 @@ use lib_dispatch::prelude::af_spawn; use crate::entities::{ view_pb_with_child_views, view_pb_without_child_views, ChildViewUpdatePB, FolderSnapshotStatePB, - FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, ViewPB, + FolderSyncStatePB, RepeatedTrashPB, RepeatedViewPB, SectionViewsPB, ViewPB, ViewSectionPB, }; -use crate::manager::{get_workspace_view_pbs, MutexFolder}; +use crate::manager::{get_workspace_private_view_pbs, get_workspace_public_view_pbs, MutexFolder}; use crate::notification::{send_notification, FolderNotification}; /// Listen on the [ViewChange] after create/delete/update events happened @@ -32,7 +32,7 @@ pub(crate) fn subscribe_folder_view_changed( match value { ViewChange::DidCreateView { view } => { notify_child_views_changed( - view_pb_without_child_views(Arc::new(view.clone())), + view_pb_without_child_views(view.clone()), ChildViewChangeReason::Create, ); notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id]); @@ -40,7 +40,7 @@ pub(crate) fn subscribe_folder_view_changed( ViewChange::DidDeleteView { views } => { for view in views { notify_child_views_changed( - view_pb_without_child_views(view), + view_pb_without_child_views(view.as_ref().clone()), ChildViewChangeReason::Delete, ); } @@ -48,7 +48,7 @@ pub(crate) fn subscribe_folder_view_changed( ViewChange::DidUpdate { view } => { notify_view_did_change(view.clone()); notify_child_views_changed( - view_pb_without_child_views(Arc::new(view.clone())), + view_pb_without_child_views(view.clone()), ChildViewChangeReason::Update, ); notify_parent_view_did_change(folder.clone(), vec![view.parent_view_id.clone()]); @@ -125,7 +125,7 @@ pub(crate) fn subscribe_folder_trash_changed( unique_ids.insert(view.parent_view_id.clone()); } - let repeated_trash: RepeatedTrashPB = folder.get_all_trash().into(); + let repeated_trash: RepeatedTrashPB = folder.get_my_trash_info().into(); send_notification("trash", FolderNotification::DidUpdateTrash) .payload(repeated_trash) .send(); @@ -140,7 +140,7 @@ pub(crate) fn subscribe_folder_trash_changed( }); } -/// Notify the the list of parent view ids that its child views were changed. +/// Notify the list of parent view ids that its child views were changed. #[tracing::instrument(level = "debug", skip(folder, parent_view_ids))] pub(crate) fn notify_parent_view_did_change>( folder: Arc, @@ -150,7 +150,7 @@ pub(crate) fn notify_parent_view_did_change>( let folder = folder.as_ref()?; let workspace_id = folder.get_workspace_id(); let trash_ids = folder - .get_all_trash() + .get_all_trash_sections() .into_iter() .map(|trash| trash.id) .collect::>(); @@ -159,9 +159,10 @@ pub(crate) fn notify_parent_view_did_change>( let parent_view_id = parent_view_id.as_ref(); // if the view's parent id equal to workspace id. Then it will fetch the current - // workspace views. Because the the workspace is not a view stored in the views map. + // workspace views. Because the workspace is not a view stored in the views map. if parent_view_id == workspace_id { - notify_did_update_workspace(&workspace_id, folder) + notify_did_update_workspace(&workspace_id, folder); + notify_did_update_section_views(&workspace_id, folder); } else { // Parent view can contain a list of child views. Currently, only get the first level // child views. @@ -181,17 +182,44 @@ pub(crate) fn notify_parent_view_did_change>( None } +pub(crate) fn notify_did_update_section_views(workspace_id: &str, folder: &Folder) { + let public_views = get_workspace_public_view_pbs(workspace_id, folder); + let private_views = get_workspace_private_view_pbs(workspace_id, folder); + tracing::trace!( + "Did update section views: public len = {}, private len = {}", + public_views.len(), + private_views.len() + ); + + // TODO(Lucas.xu) - Only notify the section changed, not the public/private both. + // Notify the public views + send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) + .payload(SectionViewsPB { + section: ViewSectionPB::Public, + views: public_views, + }) + .send(); + + // Notify the private views + send_notification(workspace_id, FolderNotification::DidUpdateSectionViews) + .payload(SectionViewsPB { + section: ViewSectionPB::Private, + views: private_views, + }) + .send(); +} + pub(crate) fn notify_did_update_workspace(workspace_id: &str, folder: &Folder) { - let repeated_view: RepeatedViewPB = get_workspace_view_pbs(workspace_id, folder).into(); - tracing::trace!("Did update workspace views: {:?}", repeated_view); + let repeated_view: RepeatedViewPB = get_workspace_public_view_pbs(workspace_id, folder).into(); send_notification(workspace_id, FolderNotification::DidUpdateWorkspaceViews) .payload(repeated_view) .send(); } fn notify_view_did_change(view: View) -> Option<()> { - let view_pb = view_pb_without_child_views(Arc::new(view.clone())); - send_notification(&view.id, FolderNotification::DidUpdateView) + let view_id = view.id.clone(); + let view_pb = view_pb_without_child_views(view); + send_notification(&view_id, FolderNotification::DidUpdateView) .payload(view_pb) .send(); None @@ -203,7 +231,7 @@ pub enum ChildViewChangeReason { Update, } -/// Notify the the list of parent view ids that its child views were changed. +/// Notify the list of parent view ids that its child views were changed. #[tracing::instrument(level = "debug", skip_all)] pub(crate) fn notify_child_views_changed(view_pb: ViewPB, reason: ChildViewChangeReason) { let parent_view_id = view_pb.parent_view_id.clone(); diff --git a/frontend/rust-lib/flowy-folder/src/notification.rs b/frontend/rust-lib/flowy-folder/src/notification.rs index df83edf46b..c57450a5d6 100644 --- a/frontend/rust-lib/flowy-folder/src/notification.rs +++ b/frontend/rust-lib/flowy-folder/src/notification.rs @@ -35,6 +35,9 @@ pub enum FolderNotification { DidUnfavoriteView = 37, DidUpdateRecentViews = 38, + + /// Trigger when the ROOT views (the first level) in section are updated + DidUpdateSectionViews = 39, } impl std::convert::From for i32 { @@ -60,6 +63,8 @@ impl std::convert::From for FolderNotification { 17 => FolderNotification::DidUpdateFolderSyncUpdate, 36 => FolderNotification::DidFavoriteView, 37 => FolderNotification::DidUnfavoriteView, + 38 => FolderNotification::DidUpdateRecentViews, + 39 => FolderNotification::DidUpdateSectionViews, _ => FolderNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-folder/src/test_helper.rs b/frontend/rust-lib/flowy-folder/src/test_helper.rs index b63448bc94..50e4b290ff 100644 --- a/frontend/rust-lib/flowy-folder/src/test_helper.rs +++ b/frontend/rust-lib/flowy-folder/src/test_helper.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use flowy_folder_pub::cloud::gen_view_id; -use crate::entities::{CreateViewParams, ViewLayoutPB}; +use crate::entities::{CreateViewParams, ViewLayoutPB, ViewSectionPB}; use crate::manager::FolderManager; #[cfg(feature = "test_helper")] @@ -47,6 +47,7 @@ impl FolderManager { meta: ext, set_as_current: true, index: None, + section: Some(ViewSectionPB::Public), }; self.create_view_with_params(params).await.unwrap(); view_id diff --git a/frontend/rust-lib/flowy-folder/src/user_default.rs b/frontend/rust-lib/flowy-folder/src/user_default.rs index be2e4c3cf4..0e2e3f4bc3 100644 --- a/frontend/rust-lib/flowy-folder/src/user_default.rs +++ b/frontend/rust-lib/flowy-folder/src/user_default.rs @@ -54,6 +54,7 @@ impl DefaultFolderBuilder { favorites: Default::default(), recent: Default::default(), trash: Default::default(), + private: Default::default(), } } } diff --git a/frontend/rust-lib/flowy-folder/src/view_operation.rs b/frontend/rust-lib/flowy-folder/src/view_operation.rs index c5472c9725..b1647344a6 100644 --- a/frontend/rust-lib/flowy-folder/src/view_operation.rs +++ b/frontend/rust-lib/flowy-folder/src/view_operation.rs @@ -32,6 +32,7 @@ pub trait FolderOperationHandler { FutureResult::new(async { Ok(()) }) } + fn open_view(&self, view_id: &str) -> FutureResult<(), FlowyError>; /// Closes the view and releases the resources that this view has in /// the backend fn close_view(&self, view_id: &str) -> FutureResult<(), FlowyError>; diff --git a/frontend/rust-lib/flowy-notification/build.rs b/frontend/rust-lib/flowy-notification/build.rs index acacab7e88..0be74ea9bc 100644 --- a/frontend/rust-lib/flowy-notification/build.rs +++ b/frontend/rust-lib/flowy-notification/build.rs @@ -3,11 +3,18 @@ fn main() { flowy_codegen::protobuf_file::dart_gen(env!("CARGO_PKG_NAME")); #[cfg(feature = "tauri_ts")] - flowy_codegen::protobuf_file::ts_gen( - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_NAME"), - flowy_codegen::Project::Tauri, - ); + { + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); + } #[cfg(feature = "web_ts")] flowy_codegen::protobuf_file::ts_gen( diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index 4cfc90ae75..dfd47a167f 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -45,15 +45,24 @@ mime_guess = "2.0" url = "2.4" tokio-util = "0.7" tokio-stream = { workspace = true, features = ["sync"] } -client-api = { version = "0.1.0", features = ["collab-sync", "test_util"] } lib-dispatch = { workspace = true } -yrs = "0.17.1" +yrs.workspace = true +rand = "0.8.5" + + +[dependencies.client-api] +version = "0.1.0" +features = [ + "collab-sync", + "test_util", + # Uncomment the following line to enable verbose logging for sync + # "sync_verbose_log", +] [dev-dependencies] uuid.workspace = true tracing-subscriber = { version = "0.3.3", features = ["env-filter"] } dotenv = "0.15.0" -yrs = "0.17.1" assert-json-diff = "2.0.2" serde_json.workspace = true client-api = { version = "0.1.0" } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs index ad5c7ce5cf..1342bb97aa 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/database.rs @@ -2,7 +2,7 @@ use anyhow::Error; use client_api::entity::QueryCollabResult::{Failed, Success}; use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::error::ErrorCode::RecordNotFound; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab::core::collab_plugin::EncodedCollab; use collab_entity::CollabType; use tracing::error; @@ -23,7 +23,7 @@ where object_id: &str, collab_type: CollabType, workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult>, Error> { let workspace_id = workspace_id.to_string(); let object_id = object_id.to_string(); let try_get_client = self.0.try_get_client(); @@ -36,10 +36,10 @@ where }, }; match try_get_client?.get_collab(params).await { - Ok(data) => Ok(data.doc_state.to_vec()), + Ok(data) => Ok(Some(data.doc_state.to_vec())), Err(err) => { if err.code == RecordNotFound { - Ok(vec![]) + Ok(None) } else { Err(Error::new(err)) } @@ -73,7 +73,10 @@ where .flat_map(|(object_id, result)| match result { Success { encode_collab_v1 } => { match EncodedCollab::decode_from_bytes(&encode_collab_v1) { - Ok(encode) => Some((object_id, encode.doc_state.to_vec())), + Ok(encode) => Some(( + object_id, + DocStateSource::FromDocState(encode.doc_state.to_vec()), + )), Err(err) => { error!("Failed to decode collab: {}", err); None diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs index 2712d272d3..7c5904ab1d 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/document.rs @@ -1,6 +1,6 @@ use anyhow::Error; use client_api::entity::{QueryCollab, QueryCollabParams}; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab::core::origin::CollabOrigin; use collab_document::document::Document; use collab_entity::CollabType; @@ -21,7 +21,7 @@ where &self, document_id: &str, workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult, FlowyError> { let workspace_id = workspace_id.to_string(); let try_get_client = self.0.try_get_client(); let document_id = document_id.to_string(); @@ -74,8 +74,12 @@ where .map_err(FlowyError::from)? .doc_state .to_vec(); - let document = - Document::from_doc_state(CollabOrigin::Empty, doc_state, &document_id, vec![])?; + let document = Document::from_doc_state( + CollabOrigin::Empty, + DocStateSource::FromDocState(doc_state), + &document_id, + vec![], + )?; Ok(document.get_document_data().ok()) }) } diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs index dcc1b8aa3a..4706babfb2 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/folder.rs @@ -2,7 +2,7 @@ use anyhow::Error; use client_api::entity::{ workspace_dto::CreateWorkspaceParam, CollabParams, QueryCollab, QueryCollabParams, }; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use collab_folder::RepeatedViewIdentifier; @@ -96,8 +96,13 @@ where .map_err(FlowyError::from)? .doc_state .to_vec(); - let folder = - Folder::from_collab_doc_state(uid, CollabOrigin::Empty, doc_state, &workspace_id, vec![])?; + let folder = Folder::from_collab_doc_state( + uid, + CollabOrigin::Empty, + DocStateSource::FromDocState(doc_state), + &workspace_id, + vec![], + )?; Ok(folder.get_folder_data()) }) } @@ -116,7 +121,7 @@ where _uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult { + ) -> FutureResult, Error> { let object_id = object_id.to_string(); let workspace_id = workspace_id.to_string(); let try_get_client = self.0.try_get_client(); diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs index 586081b033..c146df5cc7 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/impls/user/cloud_service_impl.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use std::sync::Arc; -use anyhow::{anyhow, Error}; +use anyhow::anyhow; use client_api::entity::workspace_dto::{ CreateWorkspaceMember, CreateWorkspaceParam, PatchWorkspaceParam, WorkspaceMemberChangeset, WorkspaceMemberInvitation, @@ -9,9 +9,9 @@ use client_api::entity::workspace_dto::{ use client_api::entity::{ AFRole, AFWorkspace, AFWorkspaceInvitation, AuthProvider, CollabParams, CreateCollabParams, }; +use client_api::entity::{QueryCollab, QueryCollabParams}; use client_api::{Client, ClientConfiguration}; -use collab::core::collab::CollabDocState; -use collab_entity::CollabObject; +use collab_entity::{CollabObject, CollabType}; use parking_lot::RwLock; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; @@ -181,14 +181,15 @@ where }) } - // Deprecated + #[allow(deprecated)] fn add_workspace_member( &self, user_email: String, workspace_id: String, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { + // TODO(zack): add_workspace_members will be deprecated after finishing the invite logic. Don't forget to remove the #[allow(deprecated)] try_get_client? .add_workspace_members( workspace_id, @@ -207,7 +208,7 @@ where invitee_email: String, workspace_id: String, role: Role, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { try_get_client? @@ -226,7 +227,7 @@ where fn list_workspace_invitations( &self, filter: Option, - ) -> FutureResult, Error> { + ) -> FutureResult, FlowyError> { let try_get_client = self.server.try_get_client(); let filter = filter.map(to_workspace_invitation_status); @@ -241,7 +242,7 @@ where }) } - fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), Error> { + fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { try_get_client? @@ -255,7 +256,7 @@ where &self, user_email: String, workspace_id: String, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { try_get_client? @@ -270,7 +271,7 @@ where user_email: String, workspace_id: String, role: Role, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { let changeset = WorkspaceMemberChangeset::new(user_email).with_role(to_af_role(role)); @@ -284,7 +285,7 @@ where fn get_workspace_members( &self, workspace_id: String, - ) -> FutureResult, Error> { + ) -> FutureResult, FlowyError> { let try_get_client = self.server.try_get_client(); FutureResult::new(async move { let members = try_get_client? @@ -297,15 +298,34 @@ where }) } - fn get_user_awareness_doc_state(&self, _uid: i64) -> FutureResult { - FutureResult::new(async { Ok(vec![]) }) + fn get_user_awareness_doc_state( + &self, + _uid: i64, + workspace_id: &str, + object_id: &str, + ) -> FutureResult, FlowyError> { + let workspace_id = workspace_id.to_string(); + let object_id = object_id.to_string(); + let try_get_client = self.server.try_get_client(); + FutureResult::new(async { + let params = QueryCollabParams { + workspace_id, + inner: QueryCollab { + object_id, + collab_type: CollabType::UserAwareness, + }, + }; + + let resp = try_get_client?.get_collab(params).await?; + Ok(resp.doc_state.to_vec()) + }) } fn subscribe_user_update(&self) -> Option { self.user_change_recv.write().take() } - fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> { + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -335,7 +355,7 @@ where &self, workspace_id: &str, objects: Vec, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { let workspace_id = workspace_id.to_string(); let try_get_client = self.server.try_get_client(); FutureResult::new(async move { @@ -405,6 +425,16 @@ where Ok(()) }) } + + fn leave_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let workspace_id = workspace_id.to_string(); + FutureResult::new(async move { + let client = try_get_client?; + client.leave_workspace(&workspace_id).await?; + Ok(()) + }) + } } async fn get_admin_client(client: &Arc) -> FlowyResult { @@ -467,6 +497,7 @@ fn to_user_workspace(af_workspace: AFWorkspace) -> UserWorkspace { name: af_workspace.workspace_name, created_at: af_workspace.created_at, workspace_database_object_id: af_workspace.database_storage_id.to_string(), + icon: af_workspace.icon, } } @@ -490,7 +521,7 @@ fn to_workspace_invitation(invi: AFWorkspaceInvitation) -> WorkspaceInvitation { } } -fn oauth_params_from_box_any(any: BoxAny) -> Result { +fn oauth_params_from_box_any(any: BoxAny) -> Result { let map: HashMap = any.unbox_or_error()?; let sign_in_url = map .get(USER_SIGN_IN_URL) diff --git a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs index 27f31dab1d..ca74f20377 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Error; -use client_api::collab_sync::collab_msg::ServerCollabMessage; +use client_api::collab_sync::ServerCollabMessage; use client_api::entity::UserMessage; use client_api::notify::{TokenState, TokenStateReceiver}; use client_api::ws::{ @@ -11,6 +11,7 @@ use client_api::ws::{ }; use client_api::{Client, ClientConfiguration}; use flowy_storage::ObjectStorageService; +use rand::Rng; use tokio::sync::watch; use tokio_stream::wrappers::WatchStream; use tracing::{error, event, info, warn}; @@ -24,7 +25,6 @@ use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_user_pub::cloud::{UserCloudService, UserUpdate}; use flowy_user_pub::entities::UserTokenState; use lib_dispatch::prelude::af_spawn; -use lib_infra::future::FutureResult; use crate::af_cloud::impls::{ AFCloudDatabaseCloudServiceImpl, AFCloudDocumentCloudServiceImpl, AFCloudFileStorageServiceImpl, @@ -195,7 +195,7 @@ impl AppFlowyServer for AppFlowyCloudServer { fn collab_ws_channel( &self, _object_id: &str, - ) -> FutureResult< + ) -> Result< Option<( Arc>, WSConnectStateReceiver, @@ -203,22 +203,10 @@ impl AppFlowyServer for AppFlowyCloudServer { )>, Error, > { - if self.enable_sync.load(Ordering::SeqCst) { - let object_id = _object_id.to_string(); - let weak_ws_client = Arc::downgrade(&self.ws_client); - FutureResult::new(async move { - match weak_ws_client.upgrade() { - None => Ok(None), - Some(ws_client) => { - let channel = ws_client.subscribe_collab(object_id).ok(); - let connect_state_recv = ws_client.subscribe_connect_state(); - Ok(channel.map(|c| (c, connect_state_recv, ws_client.is_connected()))) - }, - } - }) - } else { - FutureResult::new(async { Ok(None) }) - } + let object_id = _object_id.to_string(); + let channel = self.ws_client.subscribe_collab(object_id).ok(); + let connect_state_recv = self.ws_client.subscribe_connect_state(); + Ok(channel.map(|c| (c, connect_state_recv, self.ws_client.is_connected()))) } fn file_storage(&self) -> Option> { @@ -253,16 +241,7 @@ fn spawn_ws_conn( // Try to reconnect if the connection is timed out. if let Some(api_client) = weak_api_client.upgrade() { if enable_sync.load(Ordering::SeqCst) { - match api_client.ws_connect_info().await { - Ok(conn_info) => { - // sleep two seconds and then try to reconnect - tokio::time::sleep(Duration::from_secs(2)).await; - - event!(tracing::Level::INFO, "🟢reconnecting websocket"); - let _ = ws_client.connect(api_client.ws_addr(), conn_info).await; - }, - Err(err) => error!("Failed to get ws url: {}, connect state:{:?}", err, state), - } + attempt_reconnect(&ws_client, &api_client, 2).await; } } }, @@ -308,6 +287,28 @@ fn spawn_ws_conn( }); } +async fn attempt_reconnect( + ws_client: &Arc, + api_client: &Arc, + minimum_delay: u64, +) { + // Introduce randomness in the reconnection attempts to avoid thundering herd problem + let delay_seconds = rand::thread_rng().gen_range(minimum_delay..8); + tokio::time::sleep(Duration::from_secs(delay_seconds)).await; + event!( + tracing::Level::INFO, + "🟢 Attempting to reconnect websocket." + ); + match api_client.ws_connect_info().await { + Ok(conn_info) => { + if let Err(e) = ws_client.connect(api_client.ws_addr(), conn_info).await { + error!("Failed to reconnect websocket: {}", e); + } + }, + Err(err) => error!("Failed to get websocket URL: {}", err), + } +} + pub trait AFServer: Send + Sync + 'static { fn get_client(&self) -> Option>; fn try_get_client(&self) -> Result, Error>; diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs index 9092c967a9..9a4cad3445 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/database.rs @@ -1,6 +1,8 @@ use anyhow::Error; -use collab::core::collab::CollabDocState; +use collab::preclude::Collab; +use collab_entity::define::{DATABASE, DATABASE_ROW_DATA, WORKSPACE_DATABASES}; use collab_entity::CollabType; +use yrs::{Any, MapPrelim}; use flowy_database_pub::cloud::{CollabDocStateByOid, DatabaseCloudService, DatabaseSnapshot}; use lib_infra::future::FutureResult; @@ -10,11 +12,49 @@ pub(crate) struct LocalServerDatabaseCloudServiceImpl(); impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { fn get_database_object_doc_state( &self, - _object_id: &str, - _collab_type: CollabType, + object_id: &str, + collab_type: CollabType, _workspace_id: &str, - ) -> FutureResult { - FutureResult::new(async move { Ok(vec![]) }) + ) -> FutureResult>, Error> { + let object_id = object_id.to_string(); + // create the minimal required data for the given collab type + FutureResult::new(async move { + let data = match collab_type { + CollabType::Database => { + let collab = Collab::new(1, object_id, collab_type, vec![], false); + collab.with_origin_transact_mut(|txn| { + collab.insert_map_with_txn(txn, DATABASE); + }); + collab + .encode_collab_v1(|_| Ok::<(), Error>(()))? + .doc_state + .to_vec() + }, + CollabType::WorkspaceDatabase => { + let collab = Collab::new(1, object_id, collab_type, vec![], false); + collab.with_origin_transact_mut(|txn| { + collab.create_array_with_txn::>(txn, WORKSPACE_DATABASES, vec![]); + }); + collab + .encode_collab_v1(|_| Ok::<(), Error>(()))? + .doc_state + .to_vec() + }, + CollabType::DatabaseRow => { + let collab = Collab::new(1, object_id, collab_type, vec![], false); + collab.with_origin_transact_mut(|txn| { + collab.insert_map_with_txn(txn, DATABASE_ROW_DATA); + }); + collab + .encode_collab_v1(|_| Ok::<(), Error>(()))? + .doc_state + .to_vec() + }, + _ => vec![], + }; + + Ok(Some(data)) + }) } fn batch_get_database_object_doc_state( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs index e22d36bc04..bc712d03d0 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/document.rs @@ -1,5 +1,4 @@ use anyhow::Error; -use collab::core::collab::CollabDocState; use flowy_document_pub::cloud::*; use flowy_error::{ErrorCode, FlowyError}; @@ -12,7 +11,7 @@ impl DocumentCloudService for LocalServerDocumentCloudServiceImpl { &self, document_id: &str, _workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult, FlowyError> { let document_id = document_id.to_string(); FutureResult::new(async move { Err(FlowyError::new( diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs index 4920df3c51..ea0ee027b9 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/folder.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use anyhow::{anyhow, Error}; -use collab::core::collab::CollabDocState; use collab_entity::CollabType; use flowy_folder_pub::cloud::{ @@ -59,7 +58,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { _uid: i64, _collab_type: CollabType, _object_id: &str, - ) -> FutureResult { + ) -> FutureResult, Error> { FutureResult::new(async { Err(anyhow!( "Local server doesn't support get collab doc state from remote" diff --git a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs index 367d1bd732..e822240a2a 100644 --- a/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs +++ b/frontend/rust-lib/flowy-server/src/local_server/impls/user.rs @@ -1,7 +1,5 @@ use std::sync::Arc; -use anyhow::{anyhow, Error}; -use collab::core::collab::CollabDocState; use collab_entity::CollabObject; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -150,11 +148,17 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { FutureResult::new(async { Ok(vec![]) }) } - fn get_user_awareness_doc_state(&self, _uid: i64) -> FutureResult { - FutureResult::new(async { Ok(vec![]) }) + fn get_user_awareness_doc_state( + &self, + _uid: i64, + _workspace_id: &str, + _object_id: &str, + ) -> FutureResult, FlowyError> { + // must return record not found error + FutureResult::new(async { Err(FlowyError::record_not_found()) }) } - fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), Error> { + fn reset_workspace(&self, _collab_object: CollabObject) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -171,8 +175,13 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { &self, _workspace_id: &str, _objects: Vec, - ) -> FutureResult<(), Error> { - FutureResult::new(async { Err(anyhow!("local server doesn't support create collab object")) }) + ) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support batch create collab object"), + ) + }) } fn create_workspace(&self, _workspace_name: &str) -> FutureResult { @@ -214,5 +223,6 @@ fn make_user_workspace() -> UserWorkspace { name: "My Workspace".to_string(), created_at: Default::default(), workspace_database_object_id: uuid::Uuid::new_v4().to_string(), + icon: "".to_string(), } } diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index a20fe8465a..679771d162 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -5,7 +5,7 @@ use flowy_storage::ObjectStorageService; use std::sync::Arc; use anyhow::Error; -use client_api::collab_sync::collab_msg::ServerCollabMessage; +use client_api::collab_sync::ServerCollabMessage; use parking_lot::RwLock; use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] @@ -16,7 +16,6 @@ use flowy_document_pub::cloud::DocumentCloudService; use flowy_folder_pub::cloud::FolderCloudService; use flowy_user_pub::cloud::UserCloudService; use flowy_user_pub::entities::UserTokenState; -use lib_infra::future::FutureResult; pub trait AppFlowyEncryption: Send + Sync + 'static { fn get_secret(&self) -> Option; @@ -123,7 +122,7 @@ pub trait AppFlowyServer: Send + Sync + 'static { fn collab_ws_channel( &self, _object_id: &str, - ) -> FutureResult< + ) -> Result< Option<( Arc>, WSConnectStateReceiver, @@ -131,7 +130,7 @@ pub trait AppFlowyServer: Send + Sync + 'static { )>, anyhow::Error, > { - FutureResult::new(async { Ok(None) }) + Ok(None) } fn file_storage(&self) -> Option>; diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs index 3138e72f86..9bf80d8ad1 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/collab_storage.rs @@ -3,8 +3,8 @@ use std::sync::{Arc, Weak}; use anyhow::Error; use chrono::{DateTime, Utc}; -use client_api::collab_sync::collab_msg::MsgId; -use collab::core::collab::CollabDocState; +use client_api::collab_sync::MsgId; +use collab::core::collab::DocStateSource; use collab::preclude::merge_updates_v1; use collab_entity::CollabObject; use collab_plugins::cloud_storage::{ @@ -62,7 +62,7 @@ where true } - async fn get_doc_state(&self, object: &CollabObject) -> Result { + async fn get_doc_state(&self, object: &CollabObject) -> Result { let postgrest = self.server.try_get_weak_postgrest()?; let action = FetchObjectUpdateAction::new( object.object_id.clone(), @@ -70,7 +70,7 @@ where postgrest, ); let doc_state = action.run().await?; - Ok(doc_state) + Ok(DocStateSource::FromDocState(doc_state)) } async fn get_snapshots(&self, object_id: &str, limit: usize) -> Vec { diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs index afd6a2cac8..4fe1c395c4 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -1,5 +1,4 @@ use anyhow::Error; -use collab::core::collab::CollabDocState; use collab_entity::CollabType; use tokio::sync::oneshot::channel; @@ -31,7 +30,7 @@ where object_id: &str, collab_type: CollabType, _workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult>, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let object_id = object_id.to_string(); let (tx, rx) = channel(); @@ -42,7 +41,7 @@ where let updates = FetchObjectUpdateAction::new(object_id.to_string(), collab_type, postgrest) .run_with_fix_interval(5, 10) .await?; - Ok(updates) + Ok(Some(updates)) } .await, ) diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs index 869421ea75..2d2738f391 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/document.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/document.rs @@ -1,5 +1,5 @@ use anyhow::Error; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab::core::origin::CollabOrigin; use collab_document::blocks::DocumentData; use collab_document::document::Document; @@ -33,7 +33,7 @@ where &self, document_id: &str, workspace_id: &str, - ) -> FutureResult { + ) -> FutureResult, FlowyError> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let document_id = document_id.to_string(); let (tx, rx) = channel(); @@ -94,8 +94,12 @@ where let action = FetchObjectUpdateAction::new(document_id.clone(), CollabType::Document, postgrest); let doc_state = action.run_with_fix_interval(5, 10).await?; - let document = - Document::from_doc_state(CollabOrigin::Empty, doc_state, &document_id, vec![])?; + let document = Document::from_doc_state( + CollabOrigin::Empty, + DocStateSource::FromDocState(doc_state), + &document_id, + vec![], + )?; Ok(document.get_document_data().ok()) } .await, diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs index 81c19a015d..04b20fc7ed 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use anyhow::{anyhow, Error}; use chrono::{DateTime, Utc}; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; use serde_json::Value; @@ -102,8 +102,13 @@ where let doc_state = merge_updates_v1(&updates) .map_err(|err| anyhow::anyhow!("merge updates failed: {:?}", err))?; - let folder = - Folder::from_collab_doc_state(uid, CollabOrigin::Empty, doc_state, &workspace_id, vec![])?; + let folder = Folder::from_collab_doc_state( + uid, + CollabOrigin::Empty, + DocStateSource::FromDocState(doc_state), + &workspace_id, + vec![], + )?; Ok(folder.get_folder_data()) }) } @@ -137,7 +142,7 @@ where _uid: i64, collab_type: CollabType, object_id: &str, - ) -> FutureResult { + ) -> FutureResult, Error> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let object_id = object_id.to_string(); let (tx, rx) = channel(); diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs index 4dab453ddd..7964426325 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/request.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/request.rs @@ -7,7 +7,7 @@ use std::time::Duration; use anyhow::Error; use chrono::{DateTime, Utc}; -use collab::core::collab::CollabDocState; +use collab::core::collab::DocStateSource; use collab_entity::{CollabObject, CollabType}; use collab_plugins::cloud_storage::RemoteCollabSnapshot; use serde_json::Value; @@ -60,7 +60,7 @@ impl FetchObjectUpdateAction { impl Action for FetchObjectUpdateAction { type Future = Pin> + Send>>; - type Item = CollabDocState; + type Item = Vec; type Error = anyhow::Error; fn run(&mut self) -> Self::Future { @@ -284,7 +284,7 @@ pub async fn batch_get_updates_from_server( match parser_updates_form_json(record.clone(), &postgrest.secret()) { Ok(items) => { if items.is_empty() { - updates_by_oid.insert(oid.to_string(), vec![]); + updates_by_oid.insert(oid.to_string(), DocStateSource::FromDisk); } else { let updates = items .iter() @@ -293,7 +293,7 @@ pub async fn batch_get_updates_from_server( let doc_state = merge_updates_v1(&updates) .map_err(|err| anyhow::anyhow!("merge updates failed: {:?}", err))?; - updates_by_oid.insert(oid.to_string(), doc_state); + updates_by_oid.insert(oid.to_string(), DocStateSource::FromDocState(doc_state)); } }, Err(e) => { diff --git a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs index 382388558b..c5d30a8121 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -5,8 +5,8 @@ use std::pin::Pin; use std::sync::{Arc, Weak}; use std::time::Duration; -use anyhow::{anyhow, Error}; -use collab::core::collab::{CollabDocState, MutexCollab}; +use anyhow::Error; +use collab::core::collab::MutexCollab; use collab::core::origin::CollabOrigin; use collab_entity::{CollabObject, CollabType}; use parking_lot::RwLock; @@ -16,7 +16,7 @@ use tokio_retry::strategy::FixedInterval; use tokio_retry::{Action, RetryIf}; use uuid::Uuid; -use flowy_error::FlowyError; +use flowy_error::{internal_error, FlowyError}; use flowy_folder_pub::cloud::{Folder, FolderData, Workspace}; use flowy_user_pub::cloud::*; use flowy_user_pub::entities::*; @@ -248,22 +248,31 @@ where Ok(user_workspaces) }) } - fn get_user_awareness_doc_state(&self, uid: i64) -> FutureResult { + + fn get_user_awareness_doc_state( + &self, + _uid: i64, + _workspace_id: &str, + object_id: &str, + ) -> FutureResult, FlowyError> { let try_get_postgrest = self.server.try_get_weak_postgrest(); - let awareness_id = uid.to_string(); let (tx, rx) = channel(); + let object_id = object_id.to_string(); af_spawn(async move { tx.send( async move { let postgrest = try_get_postgrest?; let action = - FetchObjectUpdateAction::new(awareness_id, CollabType::UserAwareness, postgrest); + FetchObjectUpdateAction::new(object_id, CollabType::UserAwareness, postgrest); action.run_with_fix_interval(3, 3).await } .await, ) }); - FutureResult::new(async { rx.await? }) + FutureResult::new(async { + let doc_state = rx.await.map_err(internal_error)?; + doc_state.map_err(internal_error) + }) } fn receive_realtime_event(&self, json: Value) { @@ -286,7 +295,7 @@ where self.user_update_rx.write().take() } - fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error> { + fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), FlowyError> { let try_get_postgrest = self.server.try_get_weak_postgrest(); let (tx, rx) = channel(); let init_update = default_workspace_doc_state(&collab_object); @@ -347,11 +356,12 @@ where &self, _workspace_id: &str, _objects: Vec, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { FutureResult::new(async { - Err(anyhow!( - "supabase server doesn't support batch create collab" - )) + Err( + FlowyError::local_version_not_support() + .with_context("supabase server doesn't support batch create collab"), + ) }) } @@ -669,10 +679,11 @@ fn default_workspace_doc_state(collab_object: &CollabObject) -> Vec { CollabOrigin::Empty, &collab_object.object_id, vec![], + false, )); let workspace = Workspace::new(workspace_id, "My workspace".to_string(), collab_object.uid); let folder = Folder::create(collab_object.uid, collab, None, FolderData::new(workspace)); - folder.encode_collab_v1().doc_state.to_vec() + folder.encode_collab_v1().unwrap().doc_state.to_vec() } fn oauth_params_from_box_any(any: BoxAny) -> Result { diff --git a/frontend/rust-lib/flowy-server/src/supabase/define.rs b/frontend/rust-lib/flowy-server/src/supabase/define.rs index bac37a3046..ea49becc1e 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/define.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/define.rs @@ -28,7 +28,7 @@ pub(crate) const CREATED_AT: &str = "created_at"; pub fn table_name(ty: &CollabType) -> String { match ty { CollabType::DatabaseRow => format!("{}_database_row", AF_COLLAB_UPDATE_TABLE), - CollabType::Document => format!("{}_document", AF_COLLAB_UPDATE_TABLE), + CollabType::Document | CollabType::Empty => format!("{}_document", AF_COLLAB_UPDATE_TABLE), CollabType::Database => format!("{}_database", AF_COLLAB_UPDATE_TABLE), CollabType::WorkspaceDatabase => format!("{}_w_database", AF_COLLAB_UPDATE_TABLE), CollabType::Folder => format!("{}_folder", AF_COLLAB_UPDATE_TABLE), @@ -36,6 +36,14 @@ pub fn table_name(ty: &CollabType) -> String { } } -pub fn partition_key(ty: &CollabType) -> i32 { - ty.value() +pub fn partition_key(collab_type: &CollabType) -> i32 { + match collab_type { + CollabType::Document => 0, + CollabType::Database => 1, + CollabType::WorkspaceDatabase => 2, + CollabType::Folder => 3, + CollabType::DatabaseRow => 4, + CollabType::UserAwareness => 5, + CollabType::Empty => 0, + } } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs index da17de9c65..4eabe8c5c0 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/database_test.rs @@ -1,3 +1,4 @@ +use collab::core::collab::DocStateSource; use collab_entity::{CollabObject, CollabType}; use uuid::Uuid; @@ -50,7 +51,12 @@ async fn supabase_create_database_test() { .unwrap(); assert_eq!(updates_by_oid.len(), 3); - for (_, update) in updates_by_oid { - assert_eq!(update.len(), 2); + for (_, source) in updates_by_oid { + match source { + DocStateSource::FromDisk => panic!("should not be from disk"), + DocStateSource::FromDocState(doc_state) => { + assert_eq!(doc_state.len(), 2); + }, + } } } diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs index 4732f5fa94..466b728359 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/util.rs @@ -2,7 +2,7 @@ use flowy_storage::ObjectStorageService; use std::collections::HashMap; use std::sync::Arc; -use collab::core::collab::MutexCollab; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab::core::origin::CollabOrigin; use collab_plugins::cloud_storage::RemoteCollabStorage; use uuid::Uuid; @@ -122,7 +122,14 @@ pub async fn print_encryption_folder_snapshot( .pop() .unwrap(); let collab = Arc::new( - MutexCollab::new_with_doc_state(CollabOrigin::Empty, folder_id, snapshot.blob, vec![]).unwrap(), + MutexCollab::new_with_doc_state( + CollabOrigin::Empty, + folder_id, + DocStateSource::FromDocState(snapshot.blob), + vec![], + false, + ) + .unwrap(), ); let folder_data = Folder::open(uid, collab, None) .unwrap() diff --git a/frontend/rust-lib/flowy-sqlite/migrations/.gitkeep b/frontend/rust-lib/flowy-sqlite/migrations/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-03-09-031208_user_workspace_icon/down.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-03-09-031208_user_workspace_icon/down.sql new file mode 100644 index 0000000000..6adb7719f8 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2024-03-09-031208_user_workspace_icon/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE user_workspace_table DROP COLUMN icon TEXT; diff --git a/frontend/rust-lib/flowy-sqlite/migrations/2024-03-09-031208_user_workspace_icon/up.sql b/frontend/rust-lib/flowy-sqlite/migrations/2024-03-09-031208_user_workspace_icon/up.sql new file mode 100644 index 0000000000..61dfcf40b8 --- /dev/null +++ b/frontend/rust-lib/flowy-sqlite/migrations/2024-03-09-031208_user_workspace_icon/up.sql @@ -0,0 +1,2 @@ +-- Your SQL goes here +ALTER TABLE user_workspace_table ADD COLUMN icon TEXT NOT NULL DEFAULT ''; diff --git a/frontend/rust-lib/flowy-sqlite/src/schema.rs b/frontend/rust-lib/flowy-sqlite/src/schema.rs index a5ccad50f2..37c2ff8bbd 100644 --- a/frontend/rust-lib/flowy-sqlite/src/schema.rs +++ b/frontend/rust-lib/flowy-sqlite/src/schema.rs @@ -43,6 +43,7 @@ diesel::table! { uid -> BigInt, created_at -> BigInt, database_storage_id -> Text, + icon -> Text, } } diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index e12d26a3ba..f6cf50392c 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -1,7 +1,5 @@ -use anyhow::Error; -use collab::core::collab::CollabDocState; use collab_entity::{CollabObject, CollabType}; -use flowy_error::{ErrorCode, FlowyError}; +use flowy_error::{internal_error, ErrorCode, FlowyError}; use lib_infra::box_any::BoxAny; use lib_infra::conditional_send_sync_trait; use lib_infra::future::FutureResult; @@ -187,7 +185,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, user_email: String, workspace_id: String, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -196,18 +194,18 @@ pub trait UserCloudService: Send + Sync + 'static { invitee_email: String, workspace_id: String, role: Role, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } fn list_workspace_invitations( &self, filter: Option, - ) -> FutureResult, Error> { + ) -> FutureResult, FlowyError> { FutureResult::new(async { Ok(vec![]) }) } - fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), Error> { + fn accept_workspace_invitations(&self, invite_id: String) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -215,7 +213,7 @@ pub trait UserCloudService: Send + Sync + 'static { &self, user_email: String, workspace_id: String, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } @@ -224,18 +222,23 @@ pub trait UserCloudService: Send + Sync + 'static { user_email: String, workspace_id: String, role: Role, - ) -> FutureResult<(), Error> { + ) -> FutureResult<(), FlowyError> { FutureResult::new(async { Ok(()) }) } fn get_workspace_members( &self, workspace_id: String, - ) -> FutureResult, Error> { + ) -> FutureResult, FlowyError> { FutureResult::new(async { Ok(vec![]) }) } - fn get_user_awareness_doc_state(&self, uid: i64) -> FutureResult; + fn get_user_awareness_doc_state( + &self, + uid: i64, + workspace_id: &str, + object_id: &str, + ) -> FutureResult, FlowyError>; fn receive_realtime_event(&self, _json: Value) {} @@ -243,7 +246,7 @@ pub trait UserCloudService: Send + Sync + 'static { None } - fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), Error>; + fn reset_workspace(&self, collab_object: CollabObject) -> FutureResult<(), FlowyError>; fn create_collab_object( &self, @@ -256,7 +259,11 @@ pub trait UserCloudService: Send + Sync + 'static { &self, workspace_id: &str, objects: Vec, - ) -> FutureResult<(), Error>; + ) -> FutureResult<(), FlowyError>; + + fn leave_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { + FutureResult::new(async { Ok(()) }) + } } pub type UserUpdateReceiver = tokio::sync::mpsc::Receiver; @@ -269,13 +276,12 @@ pub struct UserUpdate { pub encryption_sign: String, } -pub fn uuid_from_map(map: &HashMap) -> Result { +pub fn uuid_from_map(map: &HashMap) -> Result { let uuid = map .get("uuid") .ok_or_else(|| FlowyError::new(ErrorCode::MissingAuthField, "Missing uuid field"))? .as_str(); - let uuid = Uuid::from_str(uuid)?; - Ok(uuid) + Uuid::from_str(uuid).map_err(internal_error) } #[derive(Debug)] diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index b09f801c90..16a2607c31 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -140,6 +140,8 @@ pub struct UserWorkspace { /// The database storage id is used indexing all the database views in current workspace. #[serde(rename = "database_storage_id")] pub workspace_database_object_id: String, + #[serde(default)] + pub icon: String, } impl UserWorkspace { @@ -149,6 +151,7 @@ impl UserWorkspace { name: "".to_string(), created_at: Utc::now(), workspace_database_object_id: Uuid::new_v4().to_string(), + icon: "".to_string(), } } } @@ -394,8 +397,12 @@ pub struct WorkspaceMember { pub name: String, } -pub fn awareness_oid_from_user_uuid(user_uuid: &Uuid) -> Uuid { - Uuid::new_v5(user_uuid, b"user_awareness") +/// represent the user awareness object id for the workspace. +pub fn user_awareness_object_id(user_uuid: &Uuid, workspace_id: &str) -> Uuid { + Uuid::new_v5( + user_uuid, + format!("user_awareness:{}", workspace_id).as_bytes(), + ) } #[derive(Clone, Debug)] diff --git a/frontend/rust-lib/flowy-user-pub/src/session.rs b/frontend/rust-lib/flowy-user-pub/src/session.rs index 2b742690b4..f4d45aff70 100644 --- a/frontend/rust-lib/flowy-user-pub/src/session.rs +++ b/frontend/rust-lib/flowy-user-pub/src/session.rs @@ -63,6 +63,7 @@ impl<'de> Visitor<'de> for SessionVisitor { created_at: Utc::now(), // For historical reasons, the database_storage_id is constructed by the user_id. workspace_database_object_id: STANDARD.encode(format!("{}:user:database", user_id)), + icon: "".to_owned(), }) } } diff --git a/frontend/rust-lib/flowy-user/Cargo.toml b/frontend/rust-lib/flowy-user/Cargo.toml index ec423d9bda..3b7ae02c47 100644 --- a/frontend/rust-lib/flowy-user/Cargo.toml +++ b/frontend/rust-lib/flowy-user/Cargo.toml @@ -46,6 +46,7 @@ uuid.workspace = true chrono = { workspace = true, default-features = false, features = ["clock"] } base64 = "^0.21" tokio-stream = "0.1.14" +semver = "1.0.22" [dev-dependencies] nanoid = "0.4.0" diff --git a/frontend/rust-lib/flowy-user/build.rs b/frontend/rust-lib/flowy-user/build.rs index 84b506ee31..e015eb2580 100644 --- a/frontend/rust-lib/flowy-user/build.rs +++ b/frontend/rust-lib/flowy-user/build.rs @@ -13,5 +13,11 @@ fn main() { env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri, ); + flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::TauriApp); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::TauriApp, + ); } } diff --git a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs index 18a5de7ff0..c9a705ab65 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/migrate_anon_user_collab.rs @@ -3,14 +3,14 @@ use std::ops::{Deref, DerefMut}; use std::sync::Arc; use anyhow::anyhow; -use collab::core::collab::MutexCollab; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab::core::origin::{CollabClient, CollabOrigin}; use collab::preclude::Collab; use collab_database::database::{ is_database_collab, mut_database_views_with_collab, reset_inline_view_id, }; use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_collab, RowId}; -use collab_database::user::DatabaseViewTrackerList; +use collab_database::workspace_database::DatabaseMetaList; use collab_folder::{Folder, UserId}; use collab_plugins::local_storage::kv::KVTransactionDB; use parking_lot::{Mutex, RwLock}; @@ -151,6 +151,7 @@ where &old_user.session.user_workspace.workspace_database_object_id, "phantom", vec![], + false, ); database_with_views_collab.with_origin_transact_mut(|txn| { old_collab_r_txn.load_doc_with_txn( @@ -163,9 +164,9 @@ where let new_uid = new_user_session.user_id; let new_object_id = &new_user_session.user_workspace.workspace_database_object_id; - let array = DatabaseViewTrackerList::from_collab(&database_with_views_collab); - for database_view_tracker in array.get_all_database_tracker() { - array.update_database(&database_view_tracker.database_id, |update| { + let array = DatabaseMetaList::from_collab(&database_with_views_collab); + for database_meta in array.get_all_database_meta() { + array.update_database(&database_meta.database_id, |update| { let new_linked_views = update .linked_views .iter() @@ -214,7 +215,7 @@ where let new_uid = new_user_session.user_id; let new_workspace_id = &new_user_session.user_workspace.id; - let old_folder_collab = Collab::new(old_uid, old_workspace_id, "phantom", vec![]); + let old_folder_collab = Collab::new(old_uid, old_workspace_id, "phantom", vec![], false); old_folder_collab.with_origin_transact_mut(|txn| { old_collab_r_txn.load_doc_with_txn(old_uid, old_workspace_id, txn) })?; @@ -304,8 +305,14 @@ where } let origin = CollabOrigin::Client(CollabClient::new(new_uid, "phantom")); - let new_folder_collab = Collab::new_with_doc_state(origin, new_workspace_id, vec![], vec![]) - .map_err(|err| PersistenceError::Internal(err.into()))?; + let new_folder_collab = Collab::new_with_doc_state( + origin, + new_workspace_id, + DocStateSource::FromDisk, + vec![], + false, + ) + .map_err(|err| PersistenceError::Internal(err.into()))?; let mutex_collab = Arc::new(MutexCollab::from_collab(new_folder_collab)); let new_user_id = UserId::from(new_uid); info!("migrated folder: {:?}", folder_data); @@ -450,7 +457,13 @@ where { let mut collab_by_oid = HashMap::new(); for object_id in object_ids { - let collab = Collab::new(old_user.session.user_id, object_id, "phantom", vec![]); + let collab = Collab::new( + old_user.session.user_id, + object_id, + "phantom", + vec![], + false, + ); match collab.with_origin_transact_mut(|txn| { old_collab_r_txn.load_doc_with_txn(old_user.session.user_id, &object_id, txn) }) { diff --git a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs index 6387ec7ba5..c7939e6944 100644 --- a/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs +++ b/frontend/rust-lib/flowy-user/src/anon_user/sync_supabase_user_collab.rs @@ -8,7 +8,7 @@ use collab::core::collab::MutexCollab; use collab::preclude::Collab; use collab_database::database::get_database_row_ids; use collab_database::rows::database_row_document_id_from_row_id; -use collab_database::user::{get_all_database_view_trackers, DatabaseViewTracker}; +use collab_database::workspace_database::{get_all_database_meta, DatabaseMeta}; use collab_entity::{CollabObject, CollabType}; use collab_folder::{Folder, View, ViewLayout}; use collab_plugins::local_storage::kv::KVTransactionDB; @@ -75,7 +75,7 @@ pub async fn sync_supabase_user_data_to_cloud( fn sync_view( uid: i64, folder: Arc, - database_records: Vec>, + database_metas: Vec>, workspace_id: String, device_id: String, view: Arc, @@ -84,7 +84,7 @@ fn sync_view( ) -> Pin> + Send + Sync>> { Box::pin(async move { let collab_type = collab_type_from_view_layout(&view.layout); - let object_id = object_id_from_view(&view, &database_records)?; + let object_id = object_id_from_view(&view, &database_metas)?; tracing::debug!( "sync view: {:?}:{} with object_id: {}", view.layout, @@ -180,7 +180,7 @@ fn sync_view( if let Err(err) = Box::pin(sync_view( uid, folder.clone(), - database_records.clone(), + database_metas.clone(), workspace_id.clone(), device_id.to_string(), child_view, @@ -207,13 +207,15 @@ fn get_collab_doc_state( collab_object: &CollabObject, collab_db: &Arc, ) -> Result, PersistenceError> { - let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![]); + let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); let _ = collab.with_origin_transact_mut(|txn| { collab_db .read_txn() .load_doc_with_txn(uid, &collab_object.object_id, txn) })?; - let doc_state = collab.encode_collab_v1().doc_state; + let doc_state = collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? + .doc_state; if doc_state.is_empty() { return Err(PersistenceError::UnexpectedEmptyUpdates); } @@ -226,7 +228,7 @@ fn get_database_doc_state( collab_object: &CollabObject, collab_db: &Arc, ) -> Result<(Vec, Vec), PersistenceError> { - let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![]); + let collab = Collab::new(uid, &collab_object.object_id, "phantom", vec![], false); let _ = collab.with_origin_transact_mut(|txn| { collab_db .read_txn() @@ -234,7 +236,9 @@ fn get_database_doc_state( })?; let row_ids = get_database_row_ids(&collab).unwrap_or_default(); - let doc_state = collab.encode_collab_v1().doc_state; + let doc_state = collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? + .doc_state; if doc_state.is_empty() { return Err(PersistenceError::UnexpectedEmptyUpdates); } @@ -250,14 +254,16 @@ async fn sync_folder( user_service: Arc, ) -> Result { let (folder, update) = { - let collab = Collab::new(uid, workspace_id, "phantom", vec![]); + let collab = Collab::new(uid, workspace_id, "phantom", vec![], false); // Use the temporary result to short the lifetime of the TransactionMut collab.with_origin_transact_mut(|txn| { collab_db .read_txn() .load_doc_with_txn(uid, workspace_id, txn) })?; - let doc_state = collab.encode_collab_v1().doc_state; + let doc_state = collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))? + .doc_state; ( MutexFolder::new(Folder::open( uid, @@ -297,7 +303,7 @@ async fn sync_database_views( database_views_aggregate_id: &str, collab_db: &Arc, user_service: Arc, -) -> Vec> { +) -> Vec> { let collab_object = CollabObject::new( uid, database_views_aggregate_id.to_string(), @@ -308,7 +314,7 @@ async fn sync_database_views( // Use the temporary result to short the lifetime of the TransactionMut let result = { - let collab = Collab::new(uid, database_views_aggregate_id, "phantom", vec![]); + let collab = Collab::new(uid, database_views_aggregate_id, "phantom", vec![], false); collab .with_origin_transact_mut(|txn| { collab_db @@ -317,8 +323,11 @@ async fn sync_database_views( }) .map(|_| { ( - get_all_database_view_trackers(&collab), - collab.encode_collab_v1().doc_state, + get_all_database_meta(&collab), + collab + .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) + .unwrap() + .doc_state, ) }) }; @@ -357,7 +366,7 @@ fn collab_type_from_view_layout(view_layout: &ViewLayout) -> CollabType { fn object_id_from_view( view: &Arc, - database_records: &[Arc], + database_records: &[Arc], ) -> Result { if view.layout.is_database() { match database_records diff --git a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs index 2ccbe6143b..80dcfd1b7f 100644 --- a/frontend/rust-lib/flowy-user/src/entities/user_profile.rs +++ b/frontend/rust-lib/flowy-user/src/entities/user_profile.rs @@ -228,6 +228,9 @@ pub struct UserWorkspacePB { #[pb(index = 3)] pub created_at_timestamp: i64, + + #[pb(index = 4)] + pub icon: String, } impl From for UserWorkspacePB { @@ -236,6 +239,7 @@ impl From for UserWorkspacePB { workspace_id: value.id, name: value.name, created_at_timestamp: value.created_at.timestamp(), + icon: value.icon, } } } diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 19a4dadafd..1cd15cc467 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -7,7 +7,7 @@ use lib_infra::box_any::BoxAny; use serde_json::Value; use std::sync::Weak; use std::{convert::TryInto, sync::Arc}; -use tracing::event; +use tracing::{event, trace}; use crate::entities::*; use crate::notification::{send_notification, UserNotification}; @@ -562,6 +562,8 @@ pub async fn get_all_reminder_event_handler( .into_iter() .map(ReminderPB::from) .collect::>(); + + trace!("number of reminders: {}", reminders.len()); data_result_ok(reminders.into()) } @@ -748,3 +750,13 @@ pub async fn accept_workspace_invitations_handler( manager.accept_workspace_invitation(invite_id).await?; Ok(()) } + +pub async fn leave_workspace_handler( + param: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let workspace_id = param.into_inner().workspace_id; + let manager = upgrade_manager(manager)?; + manager.leave_workspace(&workspace_id).await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-user/src/event_map.rs b/frontend/rust-lib/flowy-user/src/event_map.rs index 9cc2c01762..a9fe6cbdad 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -66,7 +66,7 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::DeleteWorkspace, delete_workspace_handler) .event(UserEvent::RenameWorkspace, rename_workspace_handler) .event(UserEvent::ChangeWorkspaceIcon, change_workspace_icon_handler) - + .event(UserEvent::LeaveWorkspace, leave_workspace_handler) .event(UserEvent::InviteWorkspaceMember, invite_workspace_member_handler) .event(UserEvent::ListWorkspaceInvitations, list_workspace_invitations_handler) .event(UserEvent::AcceptWorkspaceInvitation, accept_workspace_invitations_handler) @@ -215,14 +215,17 @@ pub enum UserEvent { #[event(input = "ChangeWorkspaceIconPB")] ChangeWorkspaceIcon = 45, + #[event(input = "UserWorkspaceIdPB")] + LeaveWorkspace = 46, + #[event(input = "WorkspaceMemberInvitationPB")] - InviteWorkspaceMember = 46, + InviteWorkspaceMember = 47, #[event(output = "RepeatedWorkspaceInvitationPB")] - ListWorkspaceInvitations = 47, + ListWorkspaceInvitations = 48, #[event(input = "AcceptWorkspaceInvitationPB")] - AcceptWorkspaceInvitation = 48, + AcceptWorkspaceInvitation = 49, } pub trait UserStatusCallback: Send + Sync + 'static { diff --git a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs index dfe4723596..172f5c1e7a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/document_empty_content.rs @@ -6,6 +6,7 @@ use collab_document::document::Document; use collab_document::document_data::default_document_data; use collab_folder::{Folder, View}; use collab_plugins::local_storage::kv::KVTransactionDB; +use semver::Version; use tracing::{event, instrument}; use collab_integrate::{CollabKVAction, CollabKVDB, PersistenceError}; @@ -24,6 +25,10 @@ impl UserDataMigration for HistoricalEmptyDocumentMigration { "historical_empty_document" } + fn applies_to_version(&self, _version: &Version) -> bool { + true + } + #[instrument(name = "HistoricalEmptyDocumentMigration", skip_all, err)] fn run( &self, @@ -80,9 +85,11 @@ where { // If the document is not exist, we don't need to migrate it. if load_collab(user_id, write_txn, &view.id).is_err() { - let collab = Arc::new(MutexCollab::new(origin.clone(), &view.id, vec![])); + let collab = Arc::new(MutexCollab::new(origin.clone(), &view.id, vec![], false)); let document = Document::create_with_data(collab, default_document_data())?; - let encode = document.get_collab().encode_collab_v1(); + let encode = document + .get_collab() + .encode_collab_v1(|_| Ok::<(), PersistenceError>(()))?; write_txn.flush_doc_with(user_id, &view.id, &encode.doc_state, &encode.state_vector)?; event!( tracing::Level::INFO, diff --git a/frontend/rust-lib/flowy-user/src/migrations/migration.rs b/frontend/rust-lib/flowy-user/src/migrations/migration.rs index 58f26130c9..26be72707a 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/migration.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/migration.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use chrono::NaiveDateTime; use diesel::{RunQueryDsl, SqliteConnection}; +use semver::Version; use collab_integrate::CollabKVDB; use flowy_error::FlowyResult; @@ -47,6 +48,7 @@ impl UserLocalDataMigration { self, migrations: Vec>, authenticator: &Authenticator, + app_version: Option, ) -> FlowyResult> { let mut applied_migrations = vec![]; let mut conn = self.sqlite_pool.get()?; @@ -57,11 +59,17 @@ impl UserLocalDataMigration { .iter() .any(|record| record.migration_name == migration.name()) { + if let Some(app_version) = app_version.as_ref() { + if !migration.applies_to_version(app_version) { + continue; + } + } + let migration_name = migration.name().to_string(); if !duplicated_names.contains(&migration_name) { migration.run(&self.session, &self.collab_db, authenticator)?; applied_migrations.push(migration.name().to_string()); - save_record(&mut conn, &migration_name); + save_migration_record(&mut conn, &migration_name); duplicated_names.push(migration_name); } else { tracing::error!("Duplicated migration name: {}", migration_name); @@ -75,6 +83,9 @@ impl UserLocalDataMigration { pub trait UserDataMigration { /// Migration with the same name will be skipped fn name(&self) -> &str; + /// Returns bool value whether the migration should be applied to the current app version + /// true if the migration should be applied, false otherwise + fn applies_to_version(&self, app_version: &Version) -> bool; fn run( &self, user: &Session, @@ -83,7 +94,7 @@ pub trait UserDataMigration { ) -> FlowyResult<()>; } -fn save_record(conn: &mut SqliteConnection, migration_name: &str) { +pub(crate) fn save_migration_record(conn: &mut SqliteConnection, migration_name: &str) { let new_record = NewUserDataMigrationRecord { migration_name: migration_name.to_string(), }; diff --git a/frontend/rust-lib/flowy-user/src/migrations/util.rs b/frontend/rust-lib/flowy-user/src/migrations/util.rs index f135cbbc96..8249ac341d 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/util.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/util.rs @@ -15,7 +15,7 @@ where R: CollabKVAction<'a>, PersistenceError: From, { - let collab = Collab::new(uid, object_id, "phantom", vec![]); + let collab = Collab::new(uid, object_id, "phantom", vec![], false); collab.with_origin_transact_mut(|txn| collab_r_txn.load_doc_with_txn(uid, &object_id, txn))?; Ok(Arc::new(MutexCollab::from_collab(collab))) } diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs index be200eb8a4..b6d5e3e8ff 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_and_favorite_v1.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; +use semver::Version; use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; @@ -22,6 +23,10 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { "workspace_favorite_v1_and_workspace_array_migration" } + fn applies_to_version(&self, _app_version: &Version) -> bool { + true + } + #[instrument(name = "FavoriteV1AndWorkspaceArrayMigration", skip_all, err)] fn run( &self, @@ -42,10 +47,12 @@ impl UserDataMigration for FavoriteV1AndWorkspaceArrayMigration { .collect::>(); if !favorite_view_ids.is_empty() { - folder.add_favorites(favorite_view_ids); + folder.add_favorite_view_ids(favorite_view_ids); } - let encode = folder.encode_collab_v1(); + let encode = folder + .encode_collab_v1() + .map_err(|err| PersistenceError::Internal(err.into()))?; write_txn.flush_doc_with( session.user_id, &session.user_workspace.id, diff --git a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs index fe90d6e1db..e15f2597b4 100644 --- a/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs +++ b/frontend/rust-lib/flowy-user/src/migrations/workspace_trash_v1.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use collab_folder::Folder; use collab_plugins::local_storage::kv::{KVTransactionDB, PersistenceError}; +use semver::Version; use tracing::instrument; use collab_integrate::{CollabKVAction, CollabKVDB}; @@ -20,6 +21,10 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { "workspace_trash_map_to_section_migration" } + fn applies_to_version(&self, _app_version: &Version) -> bool { + true + } + #[instrument(name = "WorkspaceTrashMapToSectionMigration", skip_all, err)] fn run( &self, @@ -38,10 +43,12 @@ impl UserDataMigration for WorkspaceTrashMapToSectionMigration { .collect::>(); if !trash_ids.is_empty() { - folder.add_trash(trash_ids); + folder.add_trash_view_ids(trash_ids); } - let encode = folder.encode_collab_v1(); + let encode = folder + .encode_collab_v1() + .map_err(|err| PersistenceError::Internal(err.into()))?; write_txn.flush_doc_with( session.user_id, &session.user_workspace.id, diff --git a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs index 6f560f0811..a2aad80b8b 100644 --- a/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs +++ b/frontend/rust-lib/flowy-user/src/services/authenticate_user.rs @@ -56,6 +56,10 @@ impl AuthenticateUser { Ok(session.user_id) } + pub fn device_id(&self) -> FlowyResult { + Ok(self.user_config.device_id.to_string()) + } + pub fn workspace_id(&self) -> FlowyResult { let session = self.get_session()?; Ok(session.user_workspace.id) diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs index 6783a24b04..e2f0ce57c5 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/appflowy_data_import.rs @@ -5,7 +5,7 @@ use crate::services::entities::UserPaths; use crate::services::sqlite_sql::user_sql::select_user_profile; use crate::user_manager::run_collab_data_migration; use anyhow::anyhow; -use collab::core::collab::{CollabDocState, MutexCollab}; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab::core::origin::CollabOrigin; use collab::core::transaction::DocTransactionExtension; use collab::preclude::updates::decoder::Decode; @@ -14,7 +14,7 @@ use collab_database::database::{ is_database_collab, mut_database_views_with_collab, reset_inline_view_id, }; use collab_database::rows::{database_row_document_id_from_row_id, mut_row_with_collab, RowId}; -use collab_database::user::DatabaseViewTrackerList; +use collab_database::workspace_database::DatabaseMetaList; use collab_document::document_data::default_document_collab_data; use collab_entity::CollabType; use collab_folder::{Folder, UserId, View, ViewIdentifier, ViewLayout}; @@ -26,7 +26,7 @@ use flowy_folder_pub::entities::{AppFlowyData, ImportData}; use flowy_folder_pub::folder_builder::{ParentChildViews, ViewBuilder}; use flowy_sqlite::kv::StorePreferences; use flowy_user_pub::cloud::{UserCloudService, UserCollabParams}; -use flowy_user_pub::entities::{awareness_oid_from_user_uuid, Authenticator}; +use flowy_user_pub::entities::{user_awareness_object_id, Authenticator}; use flowy_user_pub::session::Session; use parking_lot::{Mutex, RwLock}; use std::collections::{HashMap, HashSet}; @@ -80,6 +80,7 @@ pub(crate) fn get_appflowy_data_folder_import_context(path: &str) -> anyhow::Res &imported_user, imported_collab_db.clone(), imported_sqlite_db.get_pool(), + None, ); Ok(ImportContext { @@ -128,8 +129,13 @@ pub(crate) fn import_appflowy_data_folder( all_imported_object_ids.retain(|id| id != &imported_session.user_workspace.id); all_imported_object_ids .retain(|id| id != &imported_session.user_workspace.workspace_database_object_id); - all_imported_object_ids - .retain(|id| id != &awareness_oid_from_user_uuid(&imported_session.user_uuid).to_string()); + all_imported_object_ids.retain(|id| { + id != &user_awareness_object_id( + &imported_session.user_uuid, + &imported_session.user_workspace.id, + ) + .to_string() + }); // import database view tracker migrate_database_view_tracker( @@ -213,6 +219,7 @@ pub(crate) fn import_appflowy_data_folder( // create the content for the container view let import_container_doc_state = default_document_collab_data(&import_container_view_id) + .map_err(|err| PersistenceError::InvalidData(err.to_string()))? .doc_state .to_vec(); import_collab_object_with_doc_state( @@ -271,6 +278,7 @@ where &other_session.user_workspace.workspace_database_object_id, "phantom", vec![], + false, ); database_view_tracker_collab.with_origin_transact_mut(|txn| { other_collab_read_txn.load_doc_with_txn( @@ -280,11 +288,11 @@ where ) })?; - let array = DatabaseViewTrackerList::from_collab(&database_view_tracker_collab); - for database_view_tracker in array.get_all_database_tracker() { + let array = DatabaseMetaList::from_collab(&database_view_tracker_collab); + for database_meta in array.get_all_database_meta() { database_view_ids_by_database_id.insert( - old_to_new_id_map.renew_id(&database_view_tracker.database_id), - database_view_tracker + old_to_new_id_map.renew_id(&database_meta.database_id), + database_meta .linked_views .into_iter() .map(|view_id| old_to_new_id_map.renew_id(&view_id)) @@ -418,27 +426,29 @@ where W: CollabKVAction<'a>, PersistenceError: From, { - if let Ok(update) = Update::decode_v1(&collab.encode_collab_v1().doc_state) { - let doc = Doc::new(); - { - let mut txn = doc.transact_mut(); - txn.apply_update(update); - drop(txn); - } + if let Ok(encode_collab) = collab.encode_collab_v1(|_| Ok::<(), PersistenceError>(())) { + if let Ok(update) = Update::decode_v1(&encode_collab.doc_state) { + let doc = Doc::new(); + { + let mut txn = doc.transact_mut(); + txn.apply_update(update); + drop(txn); + } - let encoded_collab = doc.get_encoded_collab_v1(); - info!( - "import collab:{} with len: {}", - new_object_id, - encoded_collab.doc_state.len() - ); - if let Err(err) = w_txn.flush_doc( - new_uid, - &new_object_id, - encoded_collab.state_vector.to_vec(), - encoded_collab.doc_state.to_vec(), - ) { - error!("import collab:{} failed: {:?}", new_object_id, err); + let encoded_collab = doc.get_encoded_collab_v1(); + info!( + "import collab:{} with len: {}", + new_object_id, + encoded_collab.doc_state.len() + ); + if let Err(err) = w_txn.flush_doc( + new_uid, + &new_object_id, + encoded_collab.state_vector.to_vec(), + encoded_collab.doc_state.to_vec(), + ) { + error!("import collab:{} failed: {:?}", new_object_id, err); + } } } else { event!(tracing::Level::ERROR, "decode v1 failed"); @@ -446,7 +456,7 @@ where } fn import_collab_object_with_doc_state<'a, W>( - doc_state: CollabDocState, + doc_state: Vec, new_uid: i64, new_object_id: &str, w_txn: &'a W, @@ -455,7 +465,13 @@ where W: CollabKVAction<'a>, PersistenceError: From, { - let collab = Collab::new_with_doc_state(CollabOrigin::Empty, new_object_id, doc_state, vec![])?; + let collab = Collab::new_with_doc_state( + CollabOrigin::Empty, + new_object_id, + DocStateSource::FromDocState(doc_state), + vec![], + false, + )?; write_collab_object(&collab, new_uid, new_object_id, w_txn); Ok(()) } @@ -475,6 +491,7 @@ where &other_session.user_workspace.id, "phantom", vec![], + false, ); other_folder_collab.with_origin_transact_mut(|txn| { other_collab_read_txn.load_doc_with_txn( @@ -746,7 +763,8 @@ where .into_iter() .filter_map(|(oid, collab)| { collab - .encode_collab_v1() + .encode_collab_v1(|_| Ok::<(), PersistenceError>(())) + .ok()? .encode_to_bytes() .ok() .map(|encoded_collab| (oid, encoded_collab)) diff --git a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs index d12854f5fb..b45cc87fa9 100644 --- a/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs +++ b/frontend/rust-lib/flowy-user/src/services/data_import/importer.rs @@ -32,7 +32,7 @@ where { let mut collab_by_oid = HashMap::new(); for object_id in object_ids { - let collab = Collab::new(uid, object_id, "phantom", vec![]); + let collab = Collab::new(uid, object_id, "phantom", vec![], false); match collab .with_origin_transact_mut(|txn| collab_read_txn.load_doc_with_txn(uid, &object_id, txn)) { diff --git a/frontend/rust-lib/flowy-user/src/services/entities.rs b/frontend/rust-lib/flowy-user/src/services/entities.rs index b988feb7d0..831ef10751 100644 --- a/frontend/rust-lib/flowy-user/src/services/entities.rs +++ b/frontend/rust-lib/flowy-user/src/services/entities.rs @@ -5,6 +5,7 @@ use std::path::PathBuf; use crate::services::db::UserDBPath; use base64::engine::general_purpose::PAD; use base64::engine::GeneralPurpose; +use semver::Version; pub const URL_SAFE_ENGINE: GeneralPurpose = GeneralPurpose::new(&URL_SAFE, PAD); #[derive(Clone)] @@ -19,18 +20,26 @@ pub struct UserConfig { pub device_id: String, /// Used as the key of `Session` when saving session information to KV. pub(crate) session_cache_key: String, + pub app_version: Version, } impl UserConfig { /// The `root_dir` represents as the root of the user folders. It must be unique for each /// users. - pub fn new(name: &str, storage_path: &str, application_path: &str, device_id: &str) -> Self { + pub fn new( + name: &str, + storage_path: &str, + application_path: &str, + device_id: &str, + app_version: Version, + ) -> Self { let session_cache_key = format!("{}_session_cache", name); Self { storage_path: storage_path.to_owned(), application_path: application_path.to_owned(), session_cache_key, device_id: device_id.to_owned(), + app_version, } } diff --git a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs index e335949448..529865234b 100644 --- a/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs +++ b/frontend/rust-lib/flowy-user/src/services/sqlite_sql/workspace_sql.rs @@ -15,6 +15,7 @@ pub struct UserWorkspaceTable { pub uid: i64, pub created_at: i64, pub database_storage_id: String, + pub icon: String, } pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option { @@ -90,6 +91,7 @@ impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { uid: value.0, created_at: value.1.created_at.timestamp(), database_storage_id: value.1.workspace_database_object_id.clone(), + icon: value.1.icon.clone(), }) } } @@ -104,6 +106,7 @@ impl From for UserWorkspace { .single() .unwrap_or_default(), workspace_database_object_id: value.database_storage_id, + icon: value.icon, } } } diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs index af75c0d395..6288504a38 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -11,13 +11,14 @@ use flowy_sqlite::{query_dsl::*, DBConnection, ExpressionMethods}; use flowy_user_pub::cloud::{UserCloudServiceProvider, UserUpdate}; use flowy_user_pub::entities::*; use flowy_user_pub::workspace_service::UserWorkspaceService; +use semver::Version; use serde_json::Value; use std::string::ToString; -use std::sync::atomic::{AtomicI64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; use std::sync::{Arc, Weak}; use tokio::sync::{Mutex, RwLock}; use tokio_stream::StreamExt; -use tracing::{debug, error, event, info, instrument, warn}; +use tracing::{debug, error, event, info, instrument, trace, warn}; use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; @@ -26,7 +27,9 @@ use crate::anon_user::{migration_anon_user_on_sign_up, sync_supabase_user_data_t use crate::entities::{AuthStateChangedPB, AuthStatePB, UserProfilePB, UserSettingPB}; use crate::event_map::{DefaultUserStatusCallback, UserStatusCallback}; use crate::migrations::document_empty_content::HistoricalEmptyDocumentMigration; -use crate::migrations::migration::{UserDataMigration, UserLocalDataMigration}; +use crate::migrations::migration::{ + save_migration_record, UserDataMigration, UserLocalDataMigration, +}; use crate::migrations::workspace_and_favorite_v1::FavoriteV1AndWorkspaceArrayMigration; use crate::migrations::workspace_trash_v1::WorkspaceTrashMapToSectionMigration; use crate::migrations::AnonUser; @@ -37,7 +40,6 @@ use crate::services::data_import::importer::import_data; use crate::services::data_import::ImportContext; use crate::services::sqlite_sql::user_sql::{select_user_profile, UserTable, UserTableChangeset}; -use crate::user_manager::manager_user_awareness::UserAwarenessDataSource; use crate::user_manager::manager_user_encryption::validate_encryption_sign; use crate::user_manager::manager_user_workspace::save_user_workspaces; use crate::user_manager::user_login_state::UserAuthProcess; @@ -55,6 +57,7 @@ pub struct UserManager { auth_process: Mutex>, pub(crate) authenticate_user: Arc, refresh_user_profile_since: AtomicI64, + pub(crate) is_loading_awareness: Arc, } impl UserManager { @@ -80,6 +83,7 @@ impl UserManager { authenticate_user, refresh_user_profile_since, user_workspace_service, + is_loading_awareness: Arc::new(AtomicBool::new(false)), }); let weak_user_manager = Arc::downgrade(&user_manager); @@ -245,16 +249,20 @@ impl UserManager { self.authenticate_user.database.get_pool(session.user_id), ) { (Ok(collab_db), Ok(sqlite_pool)) => { - run_collab_data_migration(&session, &user, collab_db, sqlite_pool); + run_collab_data_migration( + &session, + &user, + collab_db, + sqlite_pool, + Some(self.authenticate_user.user_config.app_version.clone()), + ); }, _ => error!("Failed to get collab db or sqlite pool"), } self.authenticate_user.vacuum_database_if_need(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); // Init the user awareness - self - .initialize_user_awareness(&session, UserAwarenessDataSource::Local) - .await; + self.initialize_user_awareness(&session).await; user_status_callback .did_init( @@ -324,10 +332,7 @@ impl UserManager { .save_auth_data(&response, &authenticator, &session) .await?; - let _ = self - .initialize_user_awareness(&session, UserAwarenessDataSource::Remote) - .await; - + let _ = self.initialize_user_awareness(&session).await; self .user_status_callback .read() @@ -412,15 +417,10 @@ impl UserManager { ) -> FlowyResult<()> { let new_session = Session::from(&response); self.prepare_user(&new_session).await; - - let user_awareness_source = if response.is_new_user { - UserAwarenessDataSource::Local - } else { - UserAwarenessDataSource::Remote - }; self .save_auth_data(&response, authenticator, &new_session) .await?; + let _ = self.try_initial_user_awareness(&new_session).await; self .user_status_callback .read() @@ -433,11 +433,18 @@ impl UserManager { ) .await?; - self - .initialize_user_awareness(&new_session, user_awareness_source) - .await; - if response.is_new_user { + // For new user, we don't need to run the migrations + if let Ok(pool) = self + .authenticate_user + .database + .get_pool(new_session.user_id) + { + mark_all_migrations_as_applied(&pool); + } else { + error!("Failed to get pool for user {}", new_session.user_id); + } + if let Some(old_user) = migration_user { event!( tracing::Level::INFO, @@ -695,6 +702,7 @@ impl UserManager { save_user_workspaces(uid, self.db_connection(uid)?, response.user_workspaces())?; event!(tracing::Level::INFO, "Save new user profile to disk"); + self.authenticate_user.set_session(Some(session.clone()))?; self .save_user(uid, (user_profile, authenticator.clone()).into()) @@ -839,22 +847,39 @@ fn remove_user_token(uid: i64, mut conn: DBConnection) -> FlowyResult<()> { Ok(()) } +fn collab_migration_list() -> Vec> { + // ⚠️The order of migrations is crucial. If you're adding a new migration, please ensure + // it's appended to the end of the list. + vec![ + Box::new(HistoricalEmptyDocumentMigration), + Box::new(FavoriteV1AndWorkspaceArrayMigration), + Box::new(WorkspaceTrashMapToSectionMigration), + ] +} + +fn mark_all_migrations_as_applied(sqlite_pool: &Arc) { + if let Ok(mut conn) = sqlite_pool.get() { + for migration in collab_migration_list() { + save_migration_record(&mut conn, migration.name()); + } + info!("Mark all migrations as applied"); + } +} + pub(crate) fn run_collab_data_migration( session: &Session, user: &UserProfile, collab_db: Arc, sqlite_pool: Arc, + version: Option, ) { - // ⚠️The order of migrations is crucial. If you're adding a new migration, please ensure - // it's appended to the end of the list. - let migrations: Vec> = vec![ - Box::new(HistoricalEmptyDocumentMigration), - Box::new(FavoriteV1AndWorkspaceArrayMigration), - Box::new(WorkspaceTrashMapToSectionMigration), - ]; - match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool) - .run(migrations, &user.authenticator) - { + trace!("Run collab data migration: {:?}", version); + let migrations = collab_migration_list(); + match UserLocalDataMigration::new(session.clone(), collab_db, sqlite_pool).run( + migrations, + &user.authenticator, + version, + ) { Ok(applied_migrations) => { if !applied_migrations.is_empty() { info!("Did apply migrations: {:?}", applied_migrations); diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs index f7fc49803e..eeab22f7b7 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_awareness.rs @@ -1,16 +1,17 @@ +use std::sync::atomic::Ordering; use std::sync::{Arc, Weak}; use anyhow::Context; -use collab::core::collab::{CollabDocState, MutexCollab}; +use collab::core::collab::{DocStateSource, MutexCollab}; use collab_entity::reminder::Reminder; use collab_entity::CollabType; -use collab_integrate::collab_builder::CollabBuilderConfig; +use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use collab_user::core::{MutexUserAwareness, UserAwareness}; -use tracing::{error, instrument, trace}; +use tracing::{error, info, instrument, trace}; use collab_integrate::CollabKVDB; use flowy_error::{ErrorCode, FlowyError, FlowyResult}; -use flowy_user_pub::entities::awareness_oid_from_user_uuid; +use flowy_user_pub::entities::user_awareness_object_id; use crate::entities::ReminderPB; use crate::user_manager::UserManager; @@ -99,13 +100,9 @@ impl UserManager { .await } - pub async fn initialize_user_awareness( - &self, - session: &Session, - source: UserAwarenessDataSource, - ) { - match self.try_initial_user_awareness(session, source).await { - Ok(_) => trace!("User awareness initialized"), + pub async fn initialize_user_awareness(&self, session: &Session) { + match self.try_initial_user_awareness(session).await { + Ok(_) => {}, Err(e) => error!("Failed to initialize user awareness: {:?}", e), } } @@ -123,35 +120,83 @@ impl UserManager { /// # Returns /// - Returns `Ok(())` if the user's awareness is successfully initialized. /// - May return errors of type `FlowyError` if any issues arise during the initialization. - #[instrument(level = "info", skip(self, session, source), err)] - async fn try_initial_user_awareness( - &self, - session: &Session, - source: UserAwarenessDataSource, - ) -> FlowyResult<()> { - trace!("Initializing user awareness from {:?}", source); + #[instrument(level = "info", skip(self, session), err)] + pub(crate) async fn try_initial_user_awareness(&self, session: &Session) -> FlowyResult<()> { + if self.is_loading_awareness.load(Ordering::SeqCst) { + return Ok(()); + } + self.is_loading_awareness.store(true, Ordering::SeqCst); + + let object_id = + user_awareness_object_id(&session.user_uuid, &session.user_workspace.id).to_string(); + trace!("Initializing user awareness {}", object_id); let collab_db = self.get_collab_db(session.user_id)?; - let user_awareness = match source { - UserAwarenessDataSource::Local => { - let collab = self - .collab_for_user_awareness(session, collab_db, vec![]) - .await?; - MutexUserAwareness::new(UserAwareness::create(collab, None)) - }, - UserAwarenessDataSource::Remote => { - let data = self - .cloud_services + let weak_cloud_services = Arc::downgrade(&self.cloud_services); + let weak_user_awareness = Arc::downgrade(&self.user_awareness); + let weak_builder = self.collab_builder.clone(); + let cloned_is_loading = self.is_loading_awareness.clone(); + let session = session.clone(); + tokio::spawn(async move { + if cloned_is_loading.load(Ordering::SeqCst) { + return Ok(()); + } + + if let (Some(cloud_services), Some(user_awareness)) = + (weak_cloud_services.upgrade(), weak_user_awareness.upgrade()) + { + let result = cloud_services .get_user_service()? - .get_user_awareness_doc_state(session.user_id) - .await?; - trace!("Get user awareness collab: {}", data.len()); - let collab = self - .collab_for_user_awareness(session, collab_db, data) - .await?; - MutexUserAwareness::new(UserAwareness::create(collab, None)) - }, - }; - self.user_awareness.lock().await.replace(user_awareness); + .get_user_awareness_doc_state(session.user_id, &session.user_workspace.id, &object_id) + .await; + + let mut lock_awareness = user_awareness + .try_lock() + .map_err(|err| FlowyError::internal().with_context(err))?; + if lock_awareness.is_some() { + return Ok(()); + } + + let awareness = match result { + Ok(data) => { + trace!("Get user awareness collab from remote: {}", data.len()); + let collab = Self::collab_for_user_awareness( + &weak_builder, + session.user_id, + &object_id, + collab_db, + DocStateSource::FromDocState(data), + ) + .await?; + MutexUserAwareness::new(UserAwareness::create(collab, None)) + }, + Err(err) => { + if err.is_record_not_found() { + info!("User awareness not found, creating new"); + let collab = Self::collab_for_user_awareness( + &weak_builder, + session.user_id, + &object_id, + collab_db, + DocStateSource::FromDisk, + ) + .await?; + MutexUserAwareness::new(UserAwareness::create(collab, None)) + } else { + error!("Failed to fetch user awareness: {:?}", err); + return Err(err); + } + }, + }; + + trace!("User awareness initialized"); + lock_awareness.replace(awareness); + } + Ok(()) + }); + + // mark the user awareness as not loading + self.is_loading_awareness.store(false, Ordering::SeqCst); + Ok(()) } @@ -161,22 +206,22 @@ impl UserManager { /// using a collaboration builder. This instance is specifically geared towards handling /// user awareness. async fn collab_for_user_awareness( - &self, - session: &Session, + collab_builder: &Weak, + uid: i64, + object_id: &str, collab_db: Weak, - raw_data: CollabDocState, + doc_state: DocStateSource, ) -> Result, FlowyError> { - let collab_builder = self.collab_builder.upgrade().ok_or(FlowyError::new( + let collab_builder = collab_builder.upgrade().ok_or(FlowyError::new( ErrorCode::Internal, "Unexpected error: collab builder is not available", ))?; - let user_awareness_id = awareness_oid_from_user_uuid(&session.user_uuid); let collab = collab_builder .build( - session.user_id, - &user_awareness_id.to_string(), + uid, + object_id, CollabType::UserAwareness, - raw_data, + doc_state, collab_db, CollabBuilderConfig::default().sync_enable(true), ) @@ -204,9 +249,7 @@ impl UserManager { match &*user_awareness { None => { if let Ok(session) = self.get_session() { - self - .initialize_user_awareness(&session, UserAwarenessDataSource::Remote) - .await; + self.initialize_user_awareness(&session).await; } default_value }, @@ -214,12 +257,3 @@ impl UserManager { } } } - -/// Indicate using which data source to initialize the user awareness -/// If the user is not a new user, the local data source is used. Otherwise, the remote data source is used. -/// When using the remote data source, the user awareness will be initialized from the remote server. -#[derive(Debug)] -pub enum UserAwarenessDataSource { - Local, - Remote, -} diff --git a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs index 138962a87b..f08487ca73 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager_user_workspace.rs @@ -181,7 +181,40 @@ impl UserManager { .patch_workspace(workspace_id, new_workspace_name, new_workspace_icon) .await?; - Ok(()) + // save the icon and name to sqlite db + let uid = self.user_id()?; + let conn = self.db_connection(uid)?; + let mut user_workspace = match self.get_user_workspace(uid, workspace_id) { + Some(user_workspace) => user_workspace, + None => { + return Err(FlowyError::record_not_found().with_context(format!( + "Expected to find user workspace with id: {}, but not found", + workspace_id + ))); + }, + }; + + if let Some(new_workspace_name) = new_workspace_name { + user_workspace.name = new_workspace_name.to_string(); + } + if let Some(new_workspace_icon) = new_workspace_icon { + user_workspace.icon = new_workspace_icon.to_string(); + } + + save_user_workspaces(uid, conn, &[user_workspace]) + } + + pub async fn leave_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + self + .cloud_services + .get_user_service()? + .leave_workspace(workspace_id) + .await?; + + // delete workspace from local sqlite db + let uid = self.user_id()?; + let conn = self.db_connection(uid)?; + delete_user_workspaces(conn, workspace_id) } pub async fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> { @@ -335,32 +368,49 @@ pub fn save_user_workspaces( ) -> FlowyResult<()> { let user_workspaces = user_workspaces .iter() - .flat_map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace)).ok()) - .collect::>(); + .map(|user_workspace| UserWorkspaceTable::try_from((uid, user_workspace))) + .collect::, _>>()?; conn.immediate_transaction(|conn| { - for user_workspace in user_workspaces { - if let Err(err) = diesel::update( + let existing_ids = user_workspace_table::dsl::user_workspace_table + .select(user_workspace_table::id) + .load::(conn)?; + let new_ids: Vec = user_workspaces.iter().map(|w| w.id.clone()).collect(); + let ids_to_delete: Vec = existing_ids + .into_iter() + .filter(|id| !new_ids.contains(id)) + .collect(); + + // insert or update the user workspaces + for user_workspace in &user_workspaces { + let affected_rows = diesel::update( user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(user_workspace.id.clone())), + .filter(user_workspace_table::id.eq(&user_workspace.id)), ) .set(( user_workspace_table::name.eq(&user_workspace.name), user_workspace_table::created_at.eq(&user_workspace.created_at), user_workspace_table::database_storage_id.eq(&user_workspace.database_storage_id), + user_workspace_table::icon.eq(&user_workspace.icon), )) - .execute(conn) - .and_then(|rows| { - if rows == 0 { - let _ = diesel::insert_into(user_workspace_table::table) - .values(user_workspace) - .execute(conn)?; - } - Ok(()) - }) { - tracing::error!("Error saving user workspace: {:?}", err); + .execute(conn)?; + + if affected_rows == 0 { + diesel::insert_into(user_workspace_table::table) + .values(user_workspace) + .execute(conn)?; } } + + // delete the user workspaces that are not in the new list + if !ids_to_delete.is_empty() { + diesel::delete( + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq_any(ids_to_delete)), + ) + .execute(conn)?; + } + Ok::<(), FlowyError>(()) }) } diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index 32f6968c4c..eb55bfc4fa 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -11,7 +11,7 @@ use crate::module::AFPluginStateMap; use crate::runtime::AFPluginRuntime; use crate::{ errors::{DispatchError, Error, InternalError}, - module::{as_plugin_map, AFPlugin, AFPluginMap, AFPluginRequest}, + module::{plugin_map_or_crash, AFPlugin, AFPluginMap, AFPluginRequest}, response::AFPluginEventResponse, service::{AFPluginServiceFactory, Service}, }; @@ -87,7 +87,7 @@ impl AFPluginDispatcher { pub fn new(runtime: Arc, plugins: Vec) -> AFPluginDispatcher { tracing::trace!("{}", plugin_info(&plugins)); AFPluginDispatcher { - plugins: as_plugin_map(plugins), + plugins: plugin_map_or_crash(plugins), runtime, } } @@ -164,7 +164,7 @@ impl AFPluginDispatcher { let request: AFPluginRequest = request.into(); let plugins = dispatch.plugins.clone(); let service = Box::new(DispatchService { plugins }); - tracing::trace!("Async event: {:?}", &request.event); + tracing::trace!("[dispatch]: Async event: {:?}", &request.event); let service_ctx = DispatchContext { request, callback: Some(Box::new(callback)), @@ -172,7 +172,7 @@ impl AFPluginDispatcher { let handle = dispatch.runtime.spawn(async move { service.call(service_ctx).await.unwrap_or_else(|e| { - tracing::error!("Dispatch runtime error: {:?}", e); + tracing::error!("[dispatch]: runtime error: {:?}", e); InternalError::Other(format!("{:?}", e)).as_response() }) }); @@ -292,18 +292,27 @@ impl Service for DispatchService { let result = { match module_map.get(&request.event) { Some(module) => { + let event = format!("{:?}", request.event); event!( tracing::Level::TRACE, - "Handle event: {:?} by {:?}", - &request.event, - module.name + "[dispatch]: {:?} exec event:{}", + &module.name, + &event, ); let fut = module.new_service(()); let service_fut = fut.await?.call(request); - service_fut.await + let result = service_fut.await; + event!( + tracing::Level::TRACE, + "[dispatch]: {:?} exec event:{} with result: {}", + &module.name, + &event, + result.is_ok() + ); + result }, None => { - let msg = format!("Can not find the event handler. {:?}", request); + let msg = format!("[dispatch]: can not find the event handler. {:?}", request); event!(tracing::Level::ERROR, "{}", msg); Err(InternalError::HandleNotFound(msg).into()) }, diff --git a/frontend/rust-lib/lib-dispatch/src/module/module.rs b/frontend/rust-lib/lib-dispatch/src/module/module.rs index 0eb162b515..a5b2df234a 100644 --- a/frontend/rust-lib/lib-dispatch/src/module/module.rs +++ b/frontend/rust-lib/lib-dispatch/src/module/module.rs @@ -27,12 +27,16 @@ use crate::{ }; pub type AFPluginMap = Arc>>; -pub(crate) fn as_plugin_map(plugins: Vec) -> AFPluginMap { - let mut plugin_map = HashMap::new(); +pub(crate) fn plugin_map_or_crash(plugins: Vec) -> AFPluginMap { + let mut plugin_map: HashMap> = HashMap::new(); plugins.into_iter().for_each(|m| { let events = m.events(); let plugins = Arc::new(m); events.into_iter().for_each(|e| { + if plugin_map.contains_key(&e) { + let plugin_name = plugin_map.get(&e).map(|p| &p.name); + panic!("⚠️⚠️⚠️Error: {:?} is already defined in {:?}", &e, plugin_name,); + } plugin_map.insert(e, plugins.clone()); }); }); @@ -40,7 +44,7 @@ pub(crate) fn as_plugin_map(plugins: Vec) -> AFPluginMap { } #[derive(PartialEq, Eq, Hash, Debug, Clone)] -pub struct AFPluginEvent(pub String); +pub struct AFPluginEvent(String); impl std::convert::From for AFPluginEvent { fn from(t: T) -> Self { diff --git a/frontend/rust-lib/lib-infra/src/box_any.rs b/frontend/rust-lib/lib-infra/src/box_any.rs index 1822cd1a23..c471e14735 100644 --- a/frontend/rust-lib/lib-infra/src/box_any.rs +++ b/frontend/rust-lib/lib-infra/src/box_any.rs @@ -2,6 +2,7 @@ use std::any::Any; use anyhow::Result; +#[derive(Debug)] pub struct BoxAny(Box); impl BoxAny { @@ -12,6 +13,13 @@ impl BoxAny { Self(Box::new(value)) } + pub fn cloned(&self) -> Option + where + T: Clone + 'static, + { + self.0.downcast_ref::().cloned() + } + pub fn unbox_or_default(self) -> T where T: Default + 'static, diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index e9f73d3d0c..0624ec053b 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -21,10 +21,10 @@ WORKDIR /home/$user RUN sudo pacman -S --needed --noconfirm curl tar RUN curl -sSfL \ --output yay.tar.gz \ - https://github.com/Jguer/yay/releases/download/v12.0.2/yay_12.0.2_x86_64.tar.gz && \ + https://github.com/Jguer/yay/releases/download/v12.3.3/yay_12.3.3_x86_64.tar.gz && \ tar -xf yay.tar.gz && \ - sudo mv yay_12.0.2_x86_64/yay /bin && \ - rm -rf yay_12.0.2_x86_64 && \ + sudo mv yay_12.3.3_x86_64/yay /bin && \ + rm -rf yay_12.3.3_x86_64 && \ yay --version # Install Rust diff --git a/frontend/scripts/makefile/mobile.toml b/frontend/scripts/makefile/mobile.toml index 41d6888281..8e89e4c2ed 100644 --- a/frontend/scripts/makefile/mobile.toml +++ b/frontend/scripts/makefile/mobile.toml @@ -68,10 +68,10 @@ script = [ cd rust-lib/ if [ "${BUILD_FLAG}" = "debug" ]; then echo "🚀 🚀 🚀 Building Android SDK for debug" - cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi + cargo ndk -t arm64-v8a -t armeabi-v7a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi else echo "🚀 🚀 🚀 Building Android SDK for release" - cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi --release + cargo ndk -t arm64-v8a -t armeabi-v7a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi --release fi cd ../ """, @@ -85,7 +85,7 @@ private = true script = [ """ cd rust-lib/ - cargo ndk -t arm64-v8a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi + cargo ndk -t arm64-v8a -t armeabi-v7a -o ./jniLibs build --features "${FLUTTER_DESKTOP_FEATURES}" --package=dart-ffi cd ../ """, ] diff --git a/frontend/scripts/makefile/tests.toml b/frontend/scripts/makefile/tests.toml index 63b0345039..909e022fc5 100644 --- a/frontend/scripts/makefile/tests.toml +++ b/frontend/scripts/makefile/tests.toml @@ -77,7 +77,7 @@ env = { RUST_LOG = "info" } description = "Run rust-lib unit tests" script = ''' cd rust-lib -RUST_LOG=info RUST_BACKTRACE=1 cargo test --no-default-features --features "rev-sqlite" +RUST_LOG=info DISABLE_CI_TEST_LOG="true" RUST_BACKTRACE=1 cargo test --no-default-features --features "rev-sqlite" ''' diff --git a/frontend/scripts/tool/update_client_api_rev.sh b/frontend/scripts/tool/update_client_api_rev.sh index 877a046195..1af8987922 100755 --- a/frontend/scripts/tool/update_client_api_rev.sh +++ b/frontend/scripts/tool/update_client_api_rev.sh @@ -8,7 +8,7 @@ fi NEW_REV="$1" echo "New revision: $NEW_REV" -directories=("rust-lib" "appflowy_tauri/src-tauri" "appflowy_web/wasm-libs") +directories=("rust-lib" "appflowy_tauri/src-tauri" "appflowy_web/wasm-libs" "appflowy_web_app/src-tauri") for dir in "${directories[@]}"; do echo "Updating $dir" diff --git a/frontend/scripts/tool/update_collab_rev.sh b/frontend/scripts/tool/update_collab_rev.sh index 1e3b6aa632..26076df248 100755 --- a/frontend/scripts/tool/update_collab_rev.sh +++ b/frontend/scripts/tool/update_collab_rev.sh @@ -8,7 +8,7 @@ fi NEW_REV="$1" echo "New revision: $NEW_REV" -directories=("rust-lib" "appflowy_tauri/src-tauri" "appflowy_web/wasm-libs") +directories=("rust-lib" "appflowy_tauri/src-tauri" "appflowy_web/wasm-libs" "appflowy_web_app/src-tauri") for dir in "${directories[@]}"; do echo "Updating $dir" diff --git a/project.inlang.json b/project.inlang.json index 20f6e90076..3d54feeae5 100644 --- a/project.inlang.json +++ b/project.inlang.json @@ -5,10 +5,12 @@ "en", "ar-SA", "ca-ES", + "ckb-KU", "cs-CZ", "de-DE", "es-VE", "eu-ES", + "el-GR", "fa", "fr-CA", "fr-FR", @@ -21,7 +23,7 @@ "pt-BR", "pt-PT", "ru-RU", - "sv", + "sv-SE", "tr-TR", "vi", "vi-VN", @@ -41,4 +43,4 @@ "@:" ] } -} \ No newline at end of file +} diff --git a/project.inlang/settings.json b/project.inlang/settings.json index 20f6e90076..c49b392792 100644 --- a/project.inlang/settings.json +++ b/project.inlang/settings.json @@ -5,6 +5,7 @@ "en", "ar-SA", "ca-ES", + "ckb-KU", "cs-CZ", "de-DE", "es-VE", @@ -21,7 +22,7 @@ "pt-BR", "pt-PT", "ru-RU", - "sv", + "sv-SE", "tr-TR", "vi", "vi-VN", @@ -41,4 +42,4 @@ "@:" ] } -} \ No newline at end of file +}