From 792f56db79fe28800350dec6b7ef9482ca3babdc Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Fri, 2 Feb 2024 16:42:23 +0700 Subject: [PATCH 01/50] chore: remove unused build and add build bot (#4586) --- .github/workflows/build_android_apk.yaml | 99 ------------------------ .github/workflows/build_bot.yaml | 29 +++++++ .github/workflows/build_command.yml | 42 ++++++++++ 3 files changed, 71 insertions(+), 99 deletions(-) delete mode 100644 .github/workflows/build_android_apk.yaml create mode 100644 .github/workflows/build_bot.yaml create mode 100644 .github/workflows/build_command.yml diff --git a/.github/workflows/build_android_apk.yaml b/.github/workflows/build_android_apk.yaml deleted file mode 100644 index f4f4e1bf9f92c..0000000000000 --- a/.github/workflows/build_android_apk.yaml +++ /dev/null @@ -1,99 +0,0 @@ -name: Build AppFlowy Release APK - -on: workflow_dispatch - -env: - FLUTTER_VERSION: "3.18.0-0.2.pre" - RUST_TOOLCHAIN: "1.75" - -jobs: - build: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - include: - - os: ubuntu-latest - target: aarch64-linux-android - runs-on: ${{ matrix.os }} - - steps: - # the following step is required to avoid running out of space - - name: Maximize build space - if: matrix.os == '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" - - - name: Checkout source code - uses: actions/checkout@v2 - - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - target: ${{ matrix.target }} - override: true - profile: minimal - - - name: Install flutter - id: flutter - uses: subosito/flutter-action@v2 - with: - channel: "beta" - flutter-version: ${{ env.FLUTTER_VERSION }} - cache: true - - - uses: nttld/setup-ndk@v1 - id: setup-ndk - with: - ndk-version: "r24" - add-to-path: true - - - uses: Swatinem/rust-cache@v2 - with: - prefix-key: ${{ matrix.os }} - workspaces: | - frontend/rust-lib - - - uses: davidB/rust-cargo-make@v1 - with: - version: "0.36.6" - - - name: Install prerequisites - working-directory: frontend - run: | - rustup target install aarch64-linux-android - rustup target install x86_64-linux-android - cargo install --force duckscript_cli - cargo install cargo-ndk - if [ "$RUNNER_OS" == "Linux" ]; then - sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub - sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list - sudo apt-get update - sudo apt-get install -y dart curl build-essential libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev - sudo apt-get install keybinder-3.0 libnotify-dev - sudo apt-get install gcc-multilib - elif [ "$RUNNER_OS" == "Windows" ]; then - vcpkg integrate install - elif [ "$RUNNER_OS" == "macOS" ]; then - echo 'do nothing' - fi - cargo make appflowy-flutter-deps-tools - shell: bash - - - name: Build AppFlowy - working-directory: frontend - env: - ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} - run: | - cargo make --profile production-android appflowy-android - - - name: Upload APK - uses: actions/upload-artifact@v2 - with: - name: app-arm64-v8a-release.apk - path: frontend/appflowy_flutter/build/app/outputs/flutter-apk/ diff --git a/.github/workflows/build_bot.yaml b/.github/workflows/build_bot.yaml new file mode 100644 index 0000000000000..7a67f0a669072 --- /dev/null +++ b/.github/workflows/build_bot.yaml @@ -0,0 +1,29 @@ +name: Build Bot + +on: + issue_comment: + types: [created] + +jobs: + dispatch_slash_command: + runs-on: ubuntu-latest + steps: + - name: Checkout source code + uses: actions/checkout@v3 + + # get build name from pubspec.yaml + - name: Get build version + working-directory: frontend/appflowy_flutter + id: get_build_name + run: | + echo "fetching version from pubspec.yaml..." + echo "build_name=$(grep 'version: ' pubspec.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT + + + - uses: peter-evans/slash-command-dispatch@v4 + with: + token: ${{ secrets.PAT }} + commands: build + static-args: | + ref=refs/pull/${{ github.event.issue.number }}/head + build_name=${{ steps.get_build_name.outputs.build_name }} diff --git a/.github/workflows/build_command.yml b/.github/workflows/build_command.yml new file mode 100644 index 0000000000000..1648953bae575 --- /dev/null +++ b/.github/workflows/build_command.yml @@ -0,0 +1,42 @@ +name: build + +on: + repository_dispatch: + types: [build-command] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: notify appflowy_builder + run: | + platform=${{ github.event.client_payload.slash_command.args.unnamed.arg1 }} + build_name=${{ github.event.client_payload.slash_command.args.named.build_name }} + branch=${{ github.event.client_payload.slash_command.args.named.ref }} + build_type="" + arch="" + + if [ "$platform" = "android" ]; then + build_type="apk" + elif [ "$platform" = "macos" ]; then + arch="universal" + fi + + params=$(jq -n \ + --arg ref "main" \ + --arg repo "LucasXu0/AppFlowy" \ + --arg branch "$branch" \ + --arg build_name "$build_name" \ + --arg build_type "$build_type" \ + --arg arch "$arch" \ + '{ref: $ref, inputs: {repo: $repo, branch: $branch, build_name: $build_name, build_type: $build_type, arch: $arch}} | del(.inputs | .. | select(. == ""))') + + echo "params: $params" + + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/AppFlowy-IO/AppFlowy-Builder/actions/workflows/$platform.yaml/dispatches \ + -d "$params" From 274742e334f620804e122ef1d9529b0b17280fea Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sat, 3 Feb 2024 19:45:46 +0800 Subject: [PATCH 02/50] chore: bump version v0.4.6 (#4595) * chore: bump version * chore: bump client api version --- CHANGELOG.md | 4 ++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 24 ++++++------ frontend/appflowy_tauri/src-tauri/Cargo.toml | 2 +- frontend/appflowy_web/wasm-libs/Cargo.lock | 38 +++++++++++++------ frontend/appflowy_web/wasm-libs/Cargo.toml | 2 +- frontend/rust-lib/Cargo.lock | 40 +++++++++++++------- frontend/rust-lib/Cargo.toml | 2 +- 9 files changed, 74 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 705efa48362ab..c0a742c869221 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Release Notes +## Version 0.4.6 - 02/03/2024 +### Bug Fixes +- Fixed refresh token bug + ## Version 0.4.5 - 02/01/2024 ### Bug Fixes - Fixed WebSocket connection issue diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 5a6e9a2862632..3038bd7e7866a 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.4.5" +APPFLOWY_VERSION = "0.4.6" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 177bc13b4b553..567fa46e6181d 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ 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.4.5 +version: 0.4.6 environment: flutter: ">=3.18.0-0.2.pre" diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index fca085bca6144..787b0f0d0f067 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -714,7 +714,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "again", "anyhow", @@ -1200,7 +1200,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa 1.0.6", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1311,7 +1311,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -2574,7 +2574,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "futures-util", @@ -2591,7 +2591,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -3028,7 +3028,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "reqwest", @@ -4706,7 +4706,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -4729,7 +4729,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -5377,7 +5377,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -6857,7 +6857,7 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "futures-channel", "futures-util", @@ -7257,7 +7257,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index d6aa3af90b710..216adb94745cb 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -62,7 +62,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0875329a4311eca5376804a0b8e39dedddbbaadc" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23ad1c4de34c8333521b364f2e1f69695d72bb5" } # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 343926e81b72a..8eb2d45239eba 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -216,7 +216,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -534,7 +534,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "again", "anyhow", @@ -888,7 +888,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -933,7 +933,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -1646,7 +1646,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "futures-util", @@ -1663,7 +1663,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -1979,7 +1979,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "reqwest", @@ -2681,7 +2681,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", ] @@ -2701,6 +2701,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", ] @@ -2768,6 +2769,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" @@ -3171,7 +3185,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -3194,7 +3208,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -3641,7 +3655,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -4583,7 +4597,7 @@ checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "futures-channel", "futures-util", diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 805499f521248..d223d9bcc02ee 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -62,7 +62,7 @@ lto = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0875329a4311eca5376804a0b8e39dedddbbaadc" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23ad1c4de34c8333521b364f2e1f69695d72bb5" } # Please use the following script to update collab. # Working directory: frontend # diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 0279048b3a21e..8c7881bcdcd70 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "again", "anyhow", @@ -1102,7 +1102,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.8.0", + "phf 0.11.2", "smallvec", ] @@ -1235,7 +1235,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -2404,7 +2404,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "futures-util", @@ -2421,7 +2421,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -2797,7 +2797,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "reqwest", @@ -3587,7 +3587,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", ] @@ -3607,6 +3607,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", ] @@ -3674,6 +3675,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.47", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -4227,7 +4241,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -4250,7 +4264,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "bincode", @@ -4838,7 +4852,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "app-error", @@ -6013,7 +6027,7 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "futures-channel", "futures-util", @@ -6234,7 +6248,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=0875329a4311eca5376804a0b8e39dedddbbaadc#0875329a4311eca5376804a0b8e39dedddbbaadc" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index c9dc2ebe87ea6..bae379d204841 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,7 +105,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "0875329a4311eca5376804a0b8e39dedddbbaadc" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23ad1c4de34c8333521b364f2e1f69695d72bb5" } # Please use the following script to update collab. # Working directory: frontend # From 5cbc8b1e18d4fb89a6520776b157907dbf41d59d Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Sat, 3 Feb 2024 17:52:38 +0100 Subject: [PATCH 03/50] feat: calculations (#4473) * feat: initial calculation controller * fix: entities * feat: calculations * fix: review comments and support floats * fix: abstract business logic into calculations service * fix: clean calculation entities after merge * feat: react to changes to row/cell/field_type * chore: changes after merging main * feat: handle delete field * test: add grid calculations tests * fix: add validation + format numbers * refactor: get cell number * chore: bump collab * chore: fix clippy * chore: update docs * chore: update docs * chore: fmt * chore: fix flutter * chore: collab rev * fix: cleanup and hover to show * fix: localization * test: add basic rust test * fix: clippy * fix: support updating calculation on duplicate row --------- Co-authored-by: nathan --- .../database/database_field_test.dart | 7 +- .../grid/grid_calculations_test.dart | 107 ++++++ .../util/database_test_op.dart | 28 ++ .../calculations/calculation_type_ext.dart | 18 + .../calculations/calculations_listener.dart | 54 +++ .../calculations/calculations_service.dart | 46 +++ .../calculations/calculations_bloc.dart | 182 +++++++++ .../database/grid/presentation/grid_page.dart | 38 +- .../widgets/calculations/calculate_cell.dart | 137 +++++++ .../calculations/calculation_selector.dart | 61 +++ .../calculations/calculation_type_item.dart | 33 ++ .../calculations/calculations_row.dart | 46 +++ .../remove_calculation_button.dart | 34 ++ .../widgets/footer/grid_footer.dart | 12 +- .../grid/presentation/widgets/row/row.dart | 2 +- .../database/widgets/row/row_detail.dart | 8 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 15 +- frontend/appflowy_tauri/src-tauri/Cargo.toml | 14 +- frontend/appflowy_web/wasm-libs/Cargo.toml | 14 +- frontend/resources/translations/en.json | 13 +- frontend/rust-lib/Cargo.lock | 15 +- frontend/rust-lib/Cargo.toml | 14 +- frontend/rust-lib/flowy-database2/Cargo.toml | 18 +- .../calculation/calculation_changeset.rs | 81 ++++ .../calculation/calculation_entities.rs | 126 ++++++ .../src/entities/calculation/mod.rs | 5 + .../src/entities/database_entities.rs | 6 +- .../flowy-database2/src/entities/macros.rs | 21 + .../flowy-database2/src/entities/mod.rs | 2 + .../flowy-database2/src/event_handler.rs | 42 ++ .../rust-lib/flowy-database2/src/event_map.rs | 13 + frontend/rust-lib/flowy-database2/src/lib.rs | 1 + .../flowy-database2/src/notification.rs | 5 +- .../src/services/calculations/cache.rs | 6 + .../src/services/calculations/controller.rs | 359 ++++++++++++++++++ .../src/services/calculations/entities.rs | 130 +++++++ .../src/services/calculations/mod.rs | 11 + .../src/services/calculations/service.rs | 142 +++++++ .../src/services/calculations/task.rs | 42 ++ .../src/services/cell/cell_data_cache.rs | 123 +----- .../src/services/cell/cell_operation.rs | 7 +- .../src/services/database/database_editor.rs | 74 +++- .../src/services/database_view/mod.rs | 1 + .../src/services/database_view/notifier.rs | 21 +- .../database_view/view_calculations.rs | 69 ++++ .../src/services/database_view/view_editor.rs | 135 ++++++- .../services/database_view/view_operation.rs | 9 + .../src/services/database_view/views.rs | 1 + .../checkbox_type_option.rs | 9 + .../checklist_type_option/checklist.rs | 5 + .../date_type_option/date_type_option.rs | 4 + .../number_type_option/number_type_option.rs | 5 + .../select_type_option.rs | 16 +- .../text_type_option/text_type_option.rs | 4 + .../timestamp_type_option.rs | 4 + .../field/type_options/type_option_cell.rs | 10 +- .../url_type_option/url_type_option.rs | 4 + .../src/services/filter/controller.rs | 3 +- .../flowy-database2/src/services/mod.rs | 1 + .../flowy-database2/src/utils/cache.rs | 98 +++++ .../rust-lib/flowy-database2/src/utils/mod.rs | 1 + .../calculations_test/calculation_test.rs | 87 +++++ .../tests/database/calculations_test/mod.rs | 2 + .../database/calculations_test/script.rs | 74 ++++ .../flowy-database2/tests/database/mod.rs | 1 + 65 files changed, 2456 insertions(+), 220 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart create mode 100644 frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs create mode 100644 frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs create mode 100644 frontend/rust-lib/flowy-database2/src/entities/calculation/mod.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/calculations/mod.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/calculations/service.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/calculations/task.rs create mode 100644 frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs create mode 100644 frontend/rust-lib/flowy-database2/src/utils/cache.rs create mode 100644 frontend/rust-lib/flowy-database2/src/utils/mod.rs create mode 100644 frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs create mode 100644 frontend/rust-lib/flowy-database2/tests/database/calculations_test/mod.rs create mode 100644 frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs diff --git a/frontend/appflowy_flutter/integration_test/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/database/database_field_test.dart index 48da2f5de5fb9..5a41bbd9d5421 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_field_test.dart +++ b/frontend/appflowy_flutter/integration_test/database/database_field_test.dart @@ -39,12 +39,7 @@ void main() { await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); // Invoke the field editor - await tester.tapGridFieldWithName('Type'); - await tester.tapEditFieldButton(); - - await tester.tapSwitchFieldTypeButton(); - await tester.selectFieldType(FieldType.Checkbox); - await tester.dismissFieldEditor(); + await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Checkbox); await tester.assertFieldTypeWithFieldName( 'Type', diff --git a/frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart b/frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart new file mode 100644 index 0000000000000..c49d399c62b98 --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/grid/grid_calculations_test.dart @@ -0,0 +1,107 @@ +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../util/database_test_op.dart'; +import '../util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Grid Calculations', () { + testWidgets('add calculation and update cell', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + + // Change one Field to Number + await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number); + + expect(find.text('Calculate'), findsOneWidget); + + await tester.changeCalculateAtIndex(1, CalculationType.Sum); + + // Enter values in cells + await tester.editCell( + rowIndex: 0, + fieldType: FieldType.Number, + input: '100', + ); + + await tester.editCell( + rowIndex: 1, + fieldType: FieldType.Number, + input: '100', + ); + + // Dismiss edit cell + await tester.sendKeyDownEvent(LogicalKeyboardKey.enter); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.text('200'), findsOneWidget); + }); + + testWidgets('add calculations and remove row', (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); + + // Change two Fields to Number + await tester.changeFieldTypeOfFieldWithName('Type', FieldType.Number); + await tester.changeFieldTypeOfFieldWithName('Done', FieldType.Number); + + expect(find.text('Calculate'), findsNWidgets(2)); + + await tester.changeCalculateAtIndex(1, CalculationType.Sum); + await tester.changeCalculateAtIndex(2, CalculationType.Min); + + // Enter values in cells + await tester.editCell( + rowIndex: 0, + fieldType: FieldType.Number, + input: '100', + ); + await tester.editCell( + rowIndex: 1, + fieldType: FieldType.Number, + input: '150', + ); + await tester.editCell( + rowIndex: 0, + fieldType: FieldType.Number, + input: '50', + cellIndex: 1, + ); + await tester.editCell( + rowIndex: 1, + fieldType: FieldType.Number, + input: '100', + cellIndex: 1, + ); + + await tester.pumpAndSettle(); + + // Dismiss edit cell + await tester.sendKeyDownEvent(LogicalKeyboardKey.enter); + await tester.pumpAndSettle(); + + expect(find.text('250'), findsOneWidget); + expect(find.text('50'), findsNWidgets(2)); + + // Delete 1st row + await tester.hoverOnFirstRowOfGrid(); + await tester.tapRowMenuButtonInGrid(); + await tester.tapDeleteOnRowMenu(); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + expect(find.text('150'), findsNWidgets(2)); + expect(find.text('100'), findsNWidgets(2)); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index bdeb79dacfa30..cb997738c6da2 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -1,5 +1,8 @@ import 'dart:io'; +import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.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/number.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart'; @@ -720,6 +723,31 @@ extension AppFlowyDatabaseTest on WidgetTester { await pumpAndSettle(); } + Future changeFieldTypeOfFieldWithName( + String name, + FieldType type, + ) async { + await tapGridFieldWithName(name); + await tapEditFieldButton(); + + await tapSwitchFieldTypeButton(); + await selectFieldType(type); + await dismissFieldEditor(); + } + + Future changeCalculateAtIndex(int index, CalculationType type) async { + await tap(find.byType(CalculateCell).at(index)); + await pumpAndSettle(); + + await tap( + find.descendant( + of: find.byType(CalculationTypeItem), + matching: find.text(type.label), + ), + ); + await pumpAndSettle(); + } + /// Should call [tapGridFieldWithName] first. Future tapEditFieldButton() async { await tapButtonWithName(LocaleKeys.grid_field_editProperty.tr()); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart new file mode 100644 index 0000000000000..3fe2087e6b99f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculation_type_ext.dart @@ -0,0 +1,18 @@ +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:easy_localization/easy_localization.dart'; + +extension CalcTypeLabel on CalculationType { + String get label => switch (this) { + CalculationType.Average => + LocaleKeys.grid_calculationTypeLabel_average.tr(), + CalculationType.Max => LocaleKeys.grid_calculationTypeLabel_max.tr(), + CalculationType.Median => + LocaleKeys.grid_calculationTypeLabel_median.tr(), + CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(), + CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(), + _ => throw UnimplementedError( + 'Label for $this has not been implemented', + ), + }; +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart new file mode 100644 index 0000000000000..eb6939c646207 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_listener.dart @@ -0,0 +1,54 @@ +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:dartz/dartz.dart'; +import 'package:flowy_infra/notifier.dart'; + +typedef UpdateCalculationValue + = Either; + +class CalculationsListener { + CalculationsListener({required this.viewId}); + + final String viewId; + + PublishNotifier? _calculationNotifier = + PublishNotifier(); + DatabaseNotificationListener? _listener; + + void start({ + required void Function(UpdateCalculationValue) onCalculationChanged, + }) { + _calculationNotifier?.addPublishListener(onCalculationChanged); + _listener = DatabaseNotificationListener( + objectId: viewId, + handler: _handler, + ); + } + + void _handler( + DatabaseNotification ty, + Either result, + ) { + switch (ty) { + case DatabaseNotification.DidUpdateCalculation: + _calculationNotifier?.value = result.fold( + (payload) => left( + CalculationChangesetNotificationPB.fromBuffer(payload), + ), + (err) => right(err), + ); + default: + break; + } + } + + Future stop() async { + await _listener?.stop(); + _calculationNotifier?.dispose(); + _calculationNotifier = null; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart new file mode 100644 index 0000000000000..874c565c3e8c7 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/application/calculations/calculations_service.dart @@ -0,0 +1,46 @@ +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:dartz/dartz.dart'; + +class CalculationsBackendService { + const CalculationsBackendService({required this.viewId}); + + final String viewId; + + // Get Calculations (initial fetch) + Future> getCalculations() async { + final payload = DatabaseViewIdPB()..value = viewId; + + return DatabaseEventGetAllCalculations(payload).send(); + } + + Future updateCalculation( + String fieldId, + CalculationType type, { + String? calculationId, + }) async { + final payload = UpdateCalculationChangesetPB() + ..viewId = viewId + ..fieldId = fieldId + ..calculationType = type; + + if (calculationId != null) { + payload.calculationId = calculationId; + } + + await DatabaseEventUpdateCalculation(payload).send(); + } + + Future removeCalculation( + String fieldId, + String calculationId, + ) async { + final payload = RemoveCalculationChangesetPB() + ..viewId = viewId + ..fieldId = fieldId + ..calculationId = calculationId; + + await DatabaseEventRemoveCalculation(payload).send(); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart new file mode 100644 index 0000000000000..0eb60dd3eb52d --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart @@ -0,0 +1,182 @@ +import 'package:appflowy/plugins/database/application/calculations/calculations_listener.dart'; +import 'package:appflowy/plugins/database/application/calculations/calculations_service.dart'; +import 'package:appflowy/plugins/database/application/field/field_controller.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_settings_entities.pbenum.dart'; +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'calculations_bloc.freezed.dart'; + +class CalculationsBloc extends Bloc { + CalculationsBloc({ + required this.viewId, + required FieldController fieldController, + }) : _fieldController = fieldController, + _calculationsListener = CalculationsListener(viewId: viewId), + _calculationsService = CalculationsBackendService(viewId: viewId), + super(CalculationsState.initial()) { + _dispatch(); + } + + final String viewId; + final FieldController _fieldController; + final CalculationsListener _calculationsListener; + late final CalculationsBackendService _calculationsService; + + @override + Future close() async { + _fieldController.removeListener(onFieldsListener: _onReceiveFields); + await super.close(); + } + + void _dispatch() { + on((event, emit) async { + await event.when( + started: () async { + _startListening(); + await _getAllCalculations(); + + add( + CalculationsEvent.didReceiveFieldUpdate( + _fieldController.fieldInfos, + ), + ); + }, + didReceiveFieldUpdate: (fields) async { + emit( + state.copyWith( + fields: fields + .where( + (e) => + e.visibility != null && + e.visibility != FieldVisibility.AlwaysHidden, + ) + .toList(), + ), + ); + }, + didReceiveCalculationsUpdate: (calculationsMap) async { + emit( + state.copyWith( + calculationsByFieldId: calculationsMap, + ), + ); + }, + updateCalculationType: (fieldId, type, calculationId) async { + await _calculationsService.updateCalculation( + fieldId, + type, + calculationId: calculationId, + ); + }, + removeCalculation: (fieldId, calculationId) async { + await _calculationsService.removeCalculation(fieldId, calculationId); + }, + ); + }); + } + + void _startListening() { + _fieldController.addListener( + listenWhen: () => !isClosed, + onReceiveFields: _onReceiveFields, + ); + + _calculationsListener.start( + onCalculationChanged: (changesetOrFailure) { + if (isClosed) { + return; + } + + changesetOrFailure.fold( + (changeset) { + final calculationsMap = {...state.calculationsByFieldId}; + if (changeset.insertCalculations.isNotEmpty) { + for (final insert in changeset.insertCalculations) { + calculationsMap[insert.fieldId] = insert; + } + } + + if (changeset.updateCalculations.isNotEmpty) { + for (final update in changeset.updateCalculations) { + calculationsMap.removeWhere((key, _) => key == update.fieldId); + calculationsMap.addAll({update.fieldId: update}); + } + } + + if (changeset.deleteCalculations.isNotEmpty) { + for (final delete in changeset.deleteCalculations) { + calculationsMap.removeWhere((key, _) => key == delete.fieldId); + } + } + + add( + CalculationsEvent.didReceiveCalculationsUpdate( + calculationsMap, + ), + ); + }, + (_) => null, + ); + }, + ); + } + + void _onReceiveFields(List fields) => + add(CalculationsEvent.didReceiveFieldUpdate(fields)); + + Future _getAllCalculations() async { + final calculationsOrFailure = await _calculationsService.getCalculations(); + + final RepeatedCalculationsPB? calculations = + calculationsOrFailure.getLeftOrNull(); + if (calculations != null) { + final calculationMap = {}; + for (final calculation in calculations.items) { + calculationMap[calculation.fieldId] = calculation; + } + + add(CalculationsEvent.didReceiveCalculationsUpdate(calculationMap)); + } + } +} + +@freezed +class CalculationsEvent with _$CalculationsEvent { + const factory CalculationsEvent.started() = _Started; + + const factory CalculationsEvent.didReceiveFieldUpdate( + List fields, + ) = _DidReceiveFieldUpdate; + + const factory CalculationsEvent.didReceiveCalculationsUpdate( + Map calculationsByFieldId, + ) = _DidReceiveCalculationsUpdate; + + const factory CalculationsEvent.updateCalculationType( + String fieldId, + CalculationType type, { + @Default(null) String? calculationId, + }) = _UpdateCalculationType; + + const factory CalculationsEvent.removeCalculation( + String fieldId, + String calculationId, + ) = _RemoveCalculation; +} + +@freezed +class CalculationsState with _$CalculationsState { + const factory CalculationsState({ + required List fields, + required Map calculationsByFieldId, + }) = _CalculationsState; + + factory CalculationsState.initial() => const CalculationsState( + fields: [], + calculationsByFieldId: {}, + ); +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 8b26ed1e754d1..6437be29a2440 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/grid_setting_bar.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart'; @@ -12,8 +13,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/flowy_infra_ui_web.dart'; -import 'package:flowy_infra_ui/style_widget/scrolling/styled_scroll_bar.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -303,17 +303,22 @@ class _GridRows extends StatelessWidget { GridState state, List rowInfos, ) { - final children = [ - ...rowInfos.mapIndexed((index, rowInfo) { - return _renderRow( - context, - rowInfo.rowId, - isDraggable: state.reorderable, - index: index, - ); - }), - const GridRowBottomBar(key: Key('gridFooter')), - ]; + final children = rowInfos.mapIndexed((index, rowInfo) { + return _renderRow( + context, + rowInfo.rowId, + isDraggable: state.reorderable, + index: index, + ); + }).toList() + ..add(const GridRowBottomBar(key: Key('grid_footer'))) + ..add( + GridCalculationsRow( + key: const Key('grid_calculations'), + viewId: viewId, + ), + ); + return ReorderableListView.builder( /// This is a workaround related to /// https://github.com/flutter/flutter/issues/25652 @@ -434,9 +439,10 @@ class _GridFooter extends StatelessWidget { child: RichText( text: TextSpan( text: rowCountString(), - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).hintColor, - ), + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), children: [ TextSpan( text: ' $rowCount', diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart new file mode 100644 index 0000000000000..c2ff2952126ff --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart @@ -0,0 +1,137 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; +import 'package:appflowy/plugins/database/application/field/field_info.dart'; +import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra/theme_extension.dart'; +import 'package:flowy_infra_ui/flowy_infra_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class CalculateCell extends StatefulWidget { + const CalculateCell({ + super.key, + required this.fieldInfo, + required this.width, + this.calculation, + }); + + final FieldInfo fieldInfo; + final double width; + final CalculationPB? calculation; + + @override + State createState() => _CalculateCellState(); +} + +class _CalculateCellState extends State { + bool isSelected = false; + + void setIsSelected(bool selected) => setState(() => isSelected = selected); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 35, + width: widget.width, + child: AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(150, 200)), + direction: PopoverDirection.bottomWithCenterAligned, + onClose: () => setIsSelected(false), + popupBuilder: (_) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setIsSelected(true); + } + }); + + return SingleChildScrollView( + child: Column( + children: [ + if (widget.calculation != null) + RemoveCalculationButton( + onTap: () => context.read().add( + CalculationsEvent.removeCalculation( + widget.fieldInfo.id, + widget.calculation!.id, + ), + ), + ), + ...CalculationType.values.map( + (type) => CalculationTypeItem( + type: type, + onTap: () { + if (type != widget.calculation?.calculationType) { + context.read().add( + CalculationsEvent.updateCalculationType( + widget.fieldInfo.id, + type, + calculationId: widget.calculation?.id, + ), + ); + } + }, + ), + ), + ], + ), + ); + }, + child: widget.fieldInfo.fieldType == FieldType.Number + ? widget.calculation != null + ? _showCalculateValue(context) + : CalculationSelector(isSelected: isSelected) + : const SizedBox.shrink(), + ), + ); + } + + Widget _showCalculateValue(BuildContext context) { + return FlowyButton( + radius: BorderRadius.zero, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: FlowyText( + widget.calculation!.calculationType.label, + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + if (widget.calculation!.value.isNotEmpty) ...[ + const HSpace(8), + Flexible( + child: FlowyText( + _withoutTrailingZeros(widget.calculation!.value), + color: AFThemeExtension.of(context).textColor, + ), + ), + ], + const HSpace(8), + FlowySvg( + FlowySvgs.arrow_down_s, + color: Theme.of(context).hintColor, + ), + ], + ), + ); + } + + String _withoutTrailingZeros(String value) { + final regex = RegExp(r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$'); + if (regex.hasMatch(value)) { + final match = regex.firstMatch(value)!; + return match.group(1)!; + } + + return value; + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart new file mode 100644 index 0000000000000..09ea5c76c595f --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_selector.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +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/theme_extension.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; + +class CalculationSelector extends StatefulWidget { + const CalculationSelector({ + super.key, + required this.isSelected, + }); + + final bool isSelected; + + @override + State createState() => _CalculationSelectorState(); +} + +class _CalculationSelectorState extends State { + bool _isHovering = false; + + void _setHovering(bool isHovering) => + setState(() => _isHovering = isHovering); + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => _setHovering(true), + onExit: (_) => _setHovering(false), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: widget.isSelected || _isHovering ? 1 : 0, + child: FlowyButton( + radius: BorderRadius.zero, + hoverColor: AFThemeExtension.of(context).lightGreyHover, + text: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Flexible( + child: FlowyText( + LocaleKeys.grid_calculate.tr(), + color: Theme.of(context).hintColor, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(8), + FlowySvg( + FlowySvgs.arrow_down_s, + color: Theme.of(context).hintColor, + ), + ], + ), + ), + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart new file mode 100644 index 0000000000000..eb1a76fe18f20 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculation_type_item.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pbenum.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; + +class CalculationTypeItem extends StatelessWidget { + const CalculationTypeItem({ + super.key, + required this.type, + required this.onTap, + }); + + final CalculationType type; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis), + onTap: () { + onTap(); + PopoverContainer.of(context).close(); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart new file mode 100644 index 0000000000000..0d44e94190525 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart'; +import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculate_cell.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class GridCalculationsRow extends StatelessWidget { + const GridCalculationsRow({super.key, required this.viewId}); + + final String viewId; + + @override + Widget build(BuildContext context) { + final gridBloc = context.read(); + + return BlocProvider( + create: (context) => CalculationsBloc( + viewId: gridBloc.databaseController.viewId, + fieldController: gridBloc.databaseController.fieldController, + )..add(const CalculationsEvent.started()), + child: BlocBuilder( + builder: (context, state) { + return Padding( + padding: GridSize.contentInsets, + child: Row( + children: [ + ...state.fields.map( + (field) => CalculateCell( + key: Key( + '${field.id}-${state.calculationsByFieldId[field.id]?.id}', + ), + width: field.fieldSettings!.width.toDouble(), + fieldInfo: field, + calculation: state.calculationsByFieldId[field.id], + ), + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart new file mode 100644 index 0000000000000..369d27133e909 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; +import 'package:appflowy_popover/appflowy_popover.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/style_widget/button.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; + +class RemoveCalculationButton extends StatelessWidget { + const RemoveCalculationButton({ + super.key, + required this.onTap, + }); + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: GridSize.popoverItemHeight, + child: FlowyButton( + text: FlowyText.medium( + LocaleKeys.grid_calculationTypeLabel_none.tr(), + overflow: TextOverflow.ellipsis, + ), + onTap: () { + onTap(); + PopoverContainer.of(context).close(); + }, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart index 0cdda12c7e6c7..6d6840085e8e9 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/footer/grid_footer.dart @@ -1,13 +1,13 @@ +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/database/grid/application/grid_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra/theme_extension.dart'; - import 'package:flowy_infra_ui/style_widget/button.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class GridAddRowButton extends StatelessWidget { @@ -16,6 +16,12 @@ class GridAddRowButton extends StatelessWidget { @override Widget build(BuildContext context) { return FlowyButton( + radius: BorderRadius.zero, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor), + ), + ), text: FlowyText( LocaleKeys.grid_row_newRow.tr(), color: Theme.of(context).hintColor, @@ -38,7 +44,7 @@ class GridRowBottomBar extends StatelessWidget { return Container( padding: GridSize.footerContentInsets, height: GridSize.footerHeight, - margin: const EdgeInsets.only(bottom: 8, top: 8), + // margin: const EdgeInsets.only(bottom: 8, top: 8), child: const GridAddRowButton(), ); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index 78d9a8f5f199b..44a30a3384868 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -1,8 +1,8 @@ -import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'; 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/database/application/cell/cell_controller.dart'; import 'package:appflowy/plugins/database/application/field/field_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_service.dart'; diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart index 37adcbe0b3f35..f5172709fe82e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/row_detail.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/row/row_controller.dart'; import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.dart'; @@ -5,10 +7,10 @@ import 'package:appflowy/plugins/database/widgets/row/row_document.dart'; import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/user/application/reminder/reminder_bloc.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../cell/editable_cell_builder.dart'; + import 'row_banner.dart'; import 'row_property.dart'; @@ -49,9 +51,7 @@ class _RowDetailPageState extends State { rowController: widget.rowController, ), ), - BlocProvider.value( - value: getIt(), - ), + BlocProvider.value(value: getIt()), ], child: ListView( controller: scrollController, diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 787b0f0d0f067..f7821046660e9 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -816,7 +816,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "async-trait", @@ -838,7 +838,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "async-trait", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "collab", @@ -886,7 +886,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "bytes", @@ -901,7 +901,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "chrono", @@ -938,7 +938,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "async-stream", @@ -977,7 +977,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "collab", @@ -1802,6 +1802,7 @@ dependencies = [ "tokio", "tracing", "url", + "validator", ] [[package]] diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 216adb94745cb..d46b59109c39c 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -72,10 +72,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index d223d9bcc02ee..766213c6a9340 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -72,10 +72,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index aa98f8c5aecfd..8d4f9ce4dd9f9 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -657,7 +657,16 @@ "showComplete": "Show all tasks" }, "menuName": "Grid", - "referencedGridPrefix": "View of" + "referencedGridPrefix": "View of", + "calculate": "Calculate", + "calculationTypeLabel": { + "none": "None", + "average": "Average", + "max": "Max", + "median": "Median", + "min": "Min", + "sum": "Sum" + } }, "document": { "menuName": "Document", @@ -1254,4 +1263,4 @@ "userIcon": "User icon" }, "noLogFiles": "There're no log files" -} \ No newline at end of file +} diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 8c7881bcdcd70..544a65839ebb1 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -744,7 +744,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "async-trait", @@ -766,7 +766,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "async-trait", @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "collab", @@ -814,7 +814,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "bytes", @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "chrono", @@ -866,7 +866,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "async-stream", @@ -905,7 +905,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" dependencies = [ "anyhow", "collab", @@ -1790,6 +1790,7 @@ dependencies = [ "tokio", "tracing", "url", + "validator", ] [[package]] diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index bae379d204841..fdf144fe6663a 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -115,10 +115,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "d00b477a9b844d86b5caeff573ca395dc5bf7198" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } diff --git a/frontend/rust-lib/flowy-database2/Cargo.toml b/frontend/rust-lib/flowy-database2/Cargo.toml index 32badce3f2691..2d6cc2ec97d1d 100644 --- a/frontend/rust-lib/flowy-database2/Cargo.toml +++ b/frontend/rust-lib/flowy-database2/Cargo.toml @@ -14,10 +14,13 @@ collab-integrate = { workspace = true } flowy-database-pub = { workspace = true } flowy-derive.workspace = true -flowy-notification = { workspace = true } +flowy-notification = { workspace = true } parking_lot.workspace = true protobuf.workspace = true -flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "impl_from_collab_database"]} +flowy-error = { workspace = true, features = [ + "impl_from_dispatch_error", + "impl_from_collab_database", +] } lib-dispatch = { workspace = true } tokio = { workspace = true, features = ["sync"] } bytes.workspace = true @@ -26,12 +29,12 @@ serde.workspace = true serde_json.workspace = true serde_repr.workspace = true lib-infra = { workspace = true } -chrono = { workspace = true, default-features = false, features = ["clock"] } +chrono = { workspace = true, default-features = false, features = ["clock"] } rust_decimal = "1.28.1" -rusty-money = {version = "0.4.1", features = ["iso"]} +rusty-money = { version = "0.4.1", features = ["iso"] } lazy_static = "1.4.0" -indexmap = {version = "2.1.0", features = ["serde"]} -url = { version = "2"} +indexmap = { version = "2.1.0", features = ["serde"] } +url = { version = "2" } fancy-regex = "0.11.0" futures.workspace = true dashmap = "5" @@ -45,6 +48,7 @@ csv = "1.1.6" strum = "0.25" strum_macros = "0.25" lru.workspace = true +validator = { version = "0.16.0", features = ["derive"] } [dev-dependencies] event-integration = { path = "../event-integration", default-features = false } @@ -55,4 +59,4 @@ flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] \ No newline at end of file +ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] diff --git a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs new file mode 100644 index 0000000000000..a760c3cee57d8 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_changeset.rs @@ -0,0 +1,81 @@ +use flowy_derive::ProtoBuf; + +use super::{CalculationPB, CalculationType}; + +use lib_infra::validator_fn::required_not_empty_str; +use validator::Validate; + +#[derive(Default, ProtoBuf, Validate)] +pub struct UpdateCalculationChangesetPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub view_id: String, + + #[pb(index = 2, one_of)] + pub calculation_id: Option, + + #[pb(index = 3)] + #[validate(custom = "required_not_empty_str")] + pub field_id: String, + + #[pb(index = 4)] + pub calculation_type: CalculationType, +} + +#[derive(Default, ProtoBuf, Validate)] +pub struct RemoveCalculationChangesetPB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub view_id: String, + + #[pb(index = 2)] + #[validate(custom = "required_not_empty_str")] + pub field_id: String, + + #[pb(index = 3)] + #[validate(custom = "required_not_empty_str")] + pub calculation_id: String, +} + +#[derive(Debug, Default, ProtoBuf, Clone)] +pub struct CalculationChangesetNotificationPB { + #[pb(index = 1)] + pub view_id: String, + + #[pb(index = 2)] + pub insert_calculations: Vec, + + #[pb(index = 3)] + pub update_calculations: Vec, + + #[pb(index = 4)] + pub delete_calculations: Vec, +} + +impl CalculationChangesetNotificationPB { + pub fn from_insert(view_id: &str, calculations: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_calculations: calculations, + delete_calculations: Default::default(), + update_calculations: Default::default(), + } + } + pub fn from_delete(view_id: &str, calculations: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_calculations: Default::default(), + delete_calculations: calculations, + update_calculations: Default::default(), + } + } + + pub fn from_update(view_id: &str, calculations: Vec) -> Self { + Self { + view_id: view_id.to_string(), + insert_calculations: Default::default(), + delete_calculations: Default::default(), + update_calculations: calculations, + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs new file mode 100644 index 0000000000000..479156bf6a213 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/calculation/calculation_entities.rs @@ -0,0 +1,126 @@ +use std::{ + fmt::{Display, Formatter}, + sync::Arc, +}; + +use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; +use serde_repr::{Deserialize_repr, Serialize_repr}; + +use crate::{impl_into_calculation_type, services::calculations::Calculation}; + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct CalculationPB { + #[pb(index = 1)] + pub id: String, + + #[pb(index = 2)] + pub field_id: String, + + #[pb(index = 3)] + pub calculation_type: CalculationType, + + #[pb(index = 4)] + pub value: String, +} + +impl std::convert::From<&Calculation> for CalculationPB { + fn from(calculation: &Calculation) -> Self { + let calculation_type = calculation.calculation_type.into(); + + Self { + id: calculation.id.clone(), + field_id: calculation.field_id.clone(), + calculation_type, + value: calculation.value.clone(), + } + } +} + +impl std::convert::From<&Arc> for CalculationPB { + fn from(calculation: &Arc) -> Self { + let calculation_type = calculation.calculation_type.into(); + + Self { + id: calculation.id.clone(), + field_id: calculation.field_id.clone(), + calculation_type, + value: calculation.value.clone(), + } + } +} + +#[derive( + Default, Debug, Copy, Clone, PartialEq, Hash, Eq, ProtoBuf_Enum, Serialize_repr, Deserialize_repr, +)] +#[repr(u8)] +pub enum CalculationType { + #[default] + Average = 0, // Number + Max = 1, // Number + Median = 2, // Number + Min = 3, // Number + Sum = 4, // Number +} + +impl Display for CalculationType { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let value: i64 = (*self).into(); + f.write_fmt(format_args!("{}", value)) + } +} + +impl AsRef for CalculationType { + fn as_ref(&self) -> &CalculationType { + self + } +} + +impl From<&CalculationType> for CalculationType { + fn from(calculation_type: &CalculationType) -> Self { + *calculation_type + } +} + +impl CalculationType { + pub fn value(&self) -> i64 { + (*self).into() + } +} + +impl_into_calculation_type!(i64); +impl_into_calculation_type!(u8); + +impl From for i64 { + fn from(ty: CalculationType) -> Self { + (ty as u8) as i64 + } +} + +impl From<&CalculationType> for i64 { + fn from(ty: &CalculationType) -> Self { + i64::from(*ty) + } +} + +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct RepeatedCalculationsPB { + #[pb(index = 1)] + pub items: Vec, +} + +impl std::convert::From>> for RepeatedCalculationsPB { + fn from(calculations: Vec>) -> Self { + RepeatedCalculationsPB { + items: calculations + .into_iter() + .map(|rev: Arc| rev.as_ref().into()) + .collect(), + } + } +} + +impl std::convert::From> for RepeatedCalculationsPB { + fn from(items: Vec) -> Self { + Self { items } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/calculation/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/calculation/mod.rs new file mode 100644 index 0000000000000..36668a022a27e --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/entities/calculation/mod.rs @@ -0,0 +1,5 @@ +mod calculation_changeset; +mod calculation_entities; + +pub use calculation_changeset::*; +pub use calculation_entities::*; 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 2a61d6d0ed945..3a718cef299cc 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/database_entities.rs @@ -6,6 +6,9 @@ use collab_database::views::DatabaseLayout; use flowy_derive::ProtoBuf; use flowy_error::{ErrorCode, FlowyError}; +use lib_infra::validator_fn::required_not_empty_str; +use validator::Validate; + use crate::entities::parser::NotEmptyStr; use crate::entities::{DatabaseLayoutPB, FieldIdPB, RowMetaPB}; use crate::services::database::CreateDatabaseViewParams; @@ -66,9 +69,10 @@ impl AsRef for DatabaseIdPB { } } -#[derive(Clone, ProtoBuf, Default, Debug)] +#[derive(Clone, ProtoBuf, Default, Debug, Validate)] pub struct DatabaseViewIdPB { #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] pub value: String, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/macros.rs b/frontend/rust-lib/flowy-database2/src/entities/macros.rs index bb942cc78c482..03b9b2021d32a 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/macros.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/macros.rs @@ -42,3 +42,24 @@ macro_rules! impl_into_field_visibility { } }; } + +#[macro_export] +macro_rules! impl_into_calculation_type { + ($target: ident) => { + impl std::convert::From<$target> for CalculationType { + fn from(ty: $target) -> Self { + match ty { + 0 => CalculationType::Average, + 1 => CalculationType::Max, + 2 => CalculationType::Median, + 3 => CalculationType::Min, + 4 => CalculationType::Sum, + _ => { + tracing::error!("🔴 Can't parse CalculationType from value: {}", ty); + CalculationType::Average + }, + } + } + } + }; +} diff --git a/frontend/rust-lib/flowy-database2/src/entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/mod.rs index 8c0bc38063f9b..63fd455832c95 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/mod.rs @@ -1,4 +1,5 @@ mod board_entities; +pub mod calculation; mod calendar_entities; mod cell_entities; mod database_entities; @@ -19,6 +20,7 @@ mod view_entities; mod macros; pub use board_entities::*; +pub use calculation::*; pub use calendar_entities::*; pub use cell_entities::*; pub use database_entities::*; diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 5f6ec480b1760..5ce6d255d2d19 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -920,3 +920,45 @@ pub(crate) async fn update_field_settings_handler( .await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub(crate) async fn get_all_calculations_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_manager(manager)?; + let view_id = data.into_inner(); + let database_editor = manager.get_database_with_view_id(view_id.as_ref()).await?; + + let calculations = database_editor.get_all_calculations(view_id.as_ref()).await; + + data_result_ok(calculations) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn update_calculation_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let params: UpdateCalculationChangesetPB = data.into_inner(); + let editor = manager.get_database_with_view_id(¶ms.view_id).await?; + + editor.update_calculation(params).await?; + + Ok(()) +} + +#[tracing::instrument(level = "trace", skip(data, manager), err)] +pub(crate) async fn remove_calculation_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let manager = upgrade_manager(manager)?; + let params: RemoveCalculationChangesetPB = data.into_inner(); + let editor = manager.get_database_with_view_id(¶ms.view_id).await?; + + editor.remove_calculation(params).await?; + + Ok(()) +} diff --git a/frontend/rust-lib/flowy-database2/src/event_map.rs b/frontend/rust-lib/flowy-database2/src/event_map.rs index a7e2b60f4b8d0..c2e7f108022f9 100644 --- a/frontend/rust-lib/flowy-database2/src/event_map.rs +++ b/frontend/rust-lib/flowy-database2/src/event_map.rs @@ -79,6 +79,10 @@ pub fn init(database_manager: Weak) -> AFPlugin { .event(DatabaseEvent::GetFieldSettings, get_field_settings_handler) .event(DatabaseEvent::GetAllFieldSettings, get_all_field_settings_handler) .event(DatabaseEvent::UpdateFieldSettings, update_field_settings_handler) + // Calculations + .event(DatabaseEvent::GetAllCalculations, get_all_calculations_handler) + .event(DatabaseEvent::UpdateCalculation, update_calculation_handler) + .event(DatabaseEvent::RemoveCalculation, remove_calculation_handler) } /// [DatabaseEvent] defines events that are used to interact with the Grid. You could check [this](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/backend/protobuf) @@ -329,4 +333,13 @@ pub enum DatabaseEvent { /// Updates the field settings for a field in the given view #[event(input = "FieldSettingsChangesetPB")] UpdateFieldSettings = 162, + + #[event(input = "DatabaseViewIdPB", output = "RepeatedCalculationsPB")] + GetAllCalculations = 163, + + #[event(input = "UpdateCalculationChangesetPB")] + UpdateCalculation = 164, + + #[event(input = "RemoveCalculationChangesetPB")] + RemoveCalculation = 165, } diff --git a/frontend/rust-lib/flowy-database2/src/lib.rs b/frontend/rust-lib/flowy-database2/src/lib.rs index 9e9719b350c3a..df57b4b9c5bb1 100644 --- a/frontend/rust-lib/flowy-database2/src/lib.rs +++ b/frontend/rust-lib/flowy-database2/src/lib.rs @@ -8,3 +8,4 @@ pub mod notification; mod protobuf; pub mod services; pub mod template; +pub mod utils; diff --git a/frontend/rust-lib/flowy-database2/src/notification.rs b/frontend/rust-lib/flowy-database2/src/notification.rs index 5d7e7e1df44c1..f4ed4c6acd85d 100644 --- a/frontend/rust-lib/flowy-database2/src/notification.rs +++ b/frontend/rust-lib/flowy-database2/src/notification.rs @@ -50,6 +50,8 @@ pub enum DatabaseNotification { DidUpdateDatabaseSnapshotState = 85, // Trigger when the field setting is changed DidUpdateFieldSettings = 86, + // Trigger when Calculation changed + DidUpdateCalculation = 87, } impl std::convert::From for i32 { @@ -80,7 +82,8 @@ impl std::convert::From for DatabaseNotification { 82 => DatabaseNotification::DidUpdateDatabaseLayout, 83 => DatabaseNotification::DidDeleteDatabaseView, 84 => DatabaseNotification::DidMoveDatabaseViewToTrash, - 87 => DatabaseNotification::DidUpdateFieldSettings, + 86 => DatabaseNotification::DidUpdateFieldSettings, + 87 => DatabaseNotification::DidUpdateCalculation, _ => DatabaseNotification::Unknown, } } diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs new file mode 100644 index 0000000000000..d406c88f0405d --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/cache.rs @@ -0,0 +1,6 @@ +use parking_lot::RwLock; +use std::sync::Arc; + +use crate::utils::cache::AnyTypeCache; + +pub type CalculationsByFieldIdCache = Arc>>; diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs new file mode 100644 index 0000000000000..849a4f9deb915 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/controller.rs @@ -0,0 +1,359 @@ +use std::str::FromStr; +use std::sync::Arc; + +use collab::core::any_map::AnyMapExtension; +use collab_database::fields::Field; +use collab_database::rows::{Row, RowCell}; +use flowy_error::FlowyResult; +use serde::{Deserialize, Serialize}; +use tokio::sync::RwLock; + +use lib_infra::future::Fut; +use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; + +use crate::entities::{ + CalculationChangesetNotificationPB, CalculationPB, CalculationType, FieldType, +}; +use crate::services::calculations::CalculationsByFieldIdCache; +use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; +use crate::utils::cache::AnyTypeCache; + +use super::{Calculation, CalculationChangeset, CalculationsService}; + +pub trait CalculationsDelegate: Send + Sync + 'static { + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>>; + fn get_field(&self, field_id: &str) -> Option; + fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut>>; + fn update_calculation(&self, view_id: &str, calculation: Calculation); + fn remove_calculation(&self, view_id: &str, calculation_id: &str); +} + +pub struct CalculationsController { + view_id: String, + handler_id: String, + delegate: Box, + calculations_by_field_cache: CalculationsByFieldIdCache, + task_scheduler: Arc>, + calculations_service: CalculationsService, + notifier: DatabaseViewChangedNotifier, +} + +impl Drop for CalculationsController { + fn drop(&mut self) { + tracing::trace!("Drop {}", std::any::type_name::()); + } +} + +impl CalculationsController { + pub async fn new( + view_id: &str, + handler_id: &str, + delegate: T, + calculations: Vec>, + task_scheduler: Arc>, + notifier: DatabaseViewChangedNotifier, + ) -> Self + where + T: CalculationsDelegate + 'static, + { + let this = Self { + view_id: view_id.to_string(), + handler_id: handler_id.to_string(), + delegate: Box::new(delegate), + calculations_by_field_cache: AnyTypeCache::::new(), + task_scheduler, + calculations_service: CalculationsService::new(), + notifier, + }; + this.update_cache(calculations).await; + this + } + + pub async fn close(&self) { + if let Ok(mut task_scheduler) = self.task_scheduler.try_write() { + task_scheduler.unregister_handler(&self.handler_id).await; + } else { + tracing::error!("Attempt to get the lock of task_scheduler failed"); + } + } + + #[tracing::instrument(name = "schedule_filter_task", level = "trace", skip(self))] + async fn gen_task(&self, task_type: CalculationEvent, qos: QualityOfService) { + let task_id = self.task_scheduler.read().await.next_task_id(); + let task = Task::new( + &self.handler_id, + task_id, + TaskContent::Text(task_type.to_string()), + qos, + ); + self.task_scheduler.write().await.add_task(task); + } + + #[tracing::instrument( + name = "process_filter_task", + level = "trace", + skip_all, + fields(filter_result), + err + )] + pub async fn process(&self, predicate: &str) -> FlowyResult<()> { + let event_type = CalculationEvent::from_str(predicate).unwrap(); + match event_type { + CalculationEvent::RowChanged(row) => self.handle_row_changed(row).await, + CalculationEvent::CellUpdated(field_id) => self.handle_cell_changed(field_id).await, + CalculationEvent::FieldDeleted(field_id) => self.handle_field_deleted(field_id).await, + CalculationEvent::FieldTypeChanged(field_id, new_field_type) => { + self + .handle_field_type_changed(field_id, new_field_type) + .await + }, + } + + Ok(()) + } + + pub async fn did_receive_field_deleted(&self, field_id: String) { + self + .gen_task( + CalculationEvent::FieldDeleted(field_id), + QualityOfService::UserInteractive, + ) + .await + } + + async fn handle_field_deleted(&self, field_id: String) { + let calculation = self + .delegate + .get_calculation(&self.view_id, &field_id) + .await; + + if let Some(calculation) = calculation { + self + .delegate + .remove_calculation(&self.view_id, &calculation.id); + + let notification = CalculationChangesetNotificationPB::from_delete( + &self.view_id, + vec![CalculationPB::from(&calculation)], + ); + + let _ = self + .notifier + .send(DatabaseViewChanged::CalculationValueNotification( + notification, + )); + } + } + + pub async fn did_receive_field_type_changed(&self, field_id: String, new_field_type: FieldType) { + self + .gen_task( + CalculationEvent::FieldTypeChanged(field_id, new_field_type), + QualityOfService::UserInteractive, + ) + .await + } + + async fn handle_field_type_changed(&self, field_id: String, new_field_type: FieldType) { + let calculation = self + .delegate + .get_calculation(&self.view_id, &field_id) + .await; + + if let Some(calculation) = calculation { + if new_field_type != FieldType::Number { + self + .delegate + .remove_calculation(&self.view_id, &calculation.id); + + let notification = CalculationChangesetNotificationPB::from_delete( + &self.view_id, + vec![CalculationPB::from(&calculation)], + ); + + let _ = self + .notifier + .send(DatabaseViewChanged::CalculationValueNotification( + notification, + )); + } + } + } + + pub async fn did_receive_cell_changed(&self, field_id: String) { + self + .gen_task( + CalculationEvent::CellUpdated(field_id), + QualityOfService::UserInteractive, + ) + .await + } + + async fn handle_cell_changed(&self, field_id: String) { + let calculation = self + .delegate + .get_calculation(&self.view_id, &field_id) + .await; + + if let Some(calculation) = calculation { + let update = self.get_updated_calculation(calculation).await; + if let Some(update) = update { + self + .delegate + .update_calculation(&self.view_id, update.clone()); + + let notification = CalculationChangesetNotificationPB::from_update( + &self.view_id, + vec![CalculationPB::from(&update)], + ); + + let _ = self + .notifier + .send(DatabaseViewChanged::CalculationValueNotification( + notification, + )); + } + } + } + + pub async fn did_receive_row_changed(&self, row: Row) { + self + .gen_task( + CalculationEvent::RowChanged(row), + QualityOfService::UserInteractive, + ) + .await + } + + async fn handle_row_changed(&self, row: Row) { + let cells = row.cells.iter(); + + let mut updates = vec![]; + + // Iterate each cell in the row + for cell in cells { + let field_id = cell.0; + let value = cell.1.value().get("data"); + + // Only continue if there is a value in the cell + if let Some(_cell_value) = value { + let calculation = self.delegate.get_calculation(&self.view_id, field_id).await; + + if let Some(calculation) = calculation { + let update = self.get_updated_calculation(calculation.clone()).await; + + if let Some(update) = update { + updates.push(CalculationPB::from(&update)); + self.delegate.update_calculation(&self.view_id, update); + } + } + } + } + + if !updates.is_empty() { + let notification = CalculationChangesetNotificationPB::from_update(&self.view_id, updates); + + let _ = self + .notifier + .send(DatabaseViewChanged::CalculationValueNotification( + notification, + )); + } + } + + async fn get_updated_calculation(&self, calculation: Arc) -> Option { + let row_cells = self + .delegate + .get_cells_for_field(&self.view_id, &calculation.field_id) + .await; + let field = self.delegate.get_field(&calculation.field_id)?; + + if !row_cells.is_empty() { + let value = + self + .calculations_service + .calculate(&field, calculation.calculation_type, row_cells); + + if value != calculation.value { + return Some(calculation.with_value(value)); + } + } + + None + } + + pub async fn did_receive_changes( + &self, + changeset: CalculationChangeset, + ) -> Option { + let mut notification: Option = None; + + if let Some(insert) = &changeset.insert_calculation { + let row_cells: Vec> = self + .delegate + .get_cells_for_field(&self.view_id, &insert.field_id) + .await; + + let field = self.delegate.get_field(&insert.field_id)?; + + let value = self + .calculations_service + .calculate(&field, insert.calculation_type, row_cells); + + notification = Some(CalculationChangesetNotificationPB::from_insert( + &self.view_id, + vec![CalculationPB { + id: insert.id.clone(), + field_id: insert.field_id.clone(), + calculation_type: CalculationType::from(insert.calculation_type), + value, + }], + )) + } + + if let Some(delete) = &changeset.delete_calculation { + notification = Some(CalculationChangesetNotificationPB::from_delete( + &self.view_id, + vec![CalculationPB { + id: delete.id.clone(), + field_id: delete.field_id.clone(), + calculation_type: CalculationType::from(delete.calculation_type), + value: delete.value.clone(), + }], + )) + } + + notification + } + + async fn update_cache(&self, calculations: Vec>) { + for calculation in calculations { + let field_id = &calculation.field_id; + self + .calculations_by_field_cache + .write() + .insert(field_id, calculation.clone()); + } + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +enum CalculationEvent { + RowChanged(Row), + CellUpdated(String), + FieldTypeChanged(String, FieldType), + FieldDeleted(String), +} + +impl ToString for CalculationEvent { + fn to_string(&self) -> String { + serde_json::to_string(self).unwrap() + } +} + +impl FromStr for CalculationEvent { + 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/calculations/entities.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs new file mode 100644 index 0000000000000..f4502020ac0d3 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/entities.rs @@ -0,0 +1,130 @@ +use anyhow::bail; +use collab::core::any_map::AnyMapExtension; +use collab_database::views::{CalculationMap, CalculationMapBuilder}; + +use crate::entities::CalculationPB; + +#[derive(Debug, Clone)] +pub struct Calculation { + pub id: String, + pub field_id: String, + pub calculation_type: i64, + pub value: String, +} + +const CALCULATION_ID: &str = "id"; +const FIELD_ID: &str = "field_id"; +const CALCULATION_TYPE: &str = "ty"; +const CALCULATION_VALUE: &str = "calculation_value"; + +impl From for CalculationMap { + fn from(data: Calculation) -> Self { + CalculationMapBuilder::new() + .insert_str_value(CALCULATION_ID, data.id) + .insert_str_value(FIELD_ID, data.field_id) + .insert_i64_value(CALCULATION_TYPE, data.calculation_type) + .insert_str_value(CALCULATION_VALUE, data.value) + .build() + } +} + +impl std::convert::From<&CalculationPB> for Calculation { + fn from(calculation: &CalculationPB) -> Self { + let calculation_type = calculation.calculation_type.into(); + + Self { + id: calculation.id.clone(), + field_id: calculation.field_id.clone(), + calculation_type, + value: calculation.value.clone(), + } + } +} + +impl TryFrom for Calculation { + type Error = anyhow::Error; + + fn try_from(calculation: CalculationMap) -> Result { + match ( + calculation.get_str_value(CALCULATION_ID), + calculation.get_str_value(FIELD_ID), + ) { + (Some(id), Some(field_id)) => { + let value = calculation + .get_str_value(CALCULATION_VALUE) + .unwrap_or_default(); + let calculation_type = calculation + .get_i64_value(CALCULATION_TYPE) + .unwrap_or_default(); + + Ok(Calculation { + id, + field_id, + calculation_type, + value, + }) + }, + _ => { + bail!("Invalid calculation data") + }, + } + } +} + +#[derive(Clone)] +pub struct CalculationUpdatedNotification { + pub view_id: String, + + pub calculation: Calculation, +} + +impl CalculationUpdatedNotification { + pub fn new(view_id: String, calculation: Calculation) -> Self { + Self { + view_id, + calculation, + } + } +} + +impl Calculation { + pub fn none(id: String, field_id: String, calculation_type: Option) -> Self { + Self { + id, + field_id, + calculation_type: calculation_type.unwrap_or(0), + value: "".to_owned(), + } + } + + pub fn with_value(&self, value: String) -> Self { + Self { + id: self.id.clone(), + field_id: self.field_id.clone(), + calculation_type: self.calculation_type, + value, + } + } +} + +#[derive(Debug)] +pub struct CalculationChangeset { + pub(crate) insert_calculation: Option, + pub(crate) delete_calculation: Option, +} + +impl CalculationChangeset { + pub fn from_insert(calculation: Calculation) -> Self { + Self { + insert_calculation: Some(calculation), + delete_calculation: None, + } + } + + pub fn from_delete(calculation: Calculation) -> Self { + Self { + insert_calculation: None, + delete_calculation: Some(calculation), + } + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/mod.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/mod.rs new file mode 100644 index 0000000000000..4e6d9ac2543e2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/mod.rs @@ -0,0 +1,11 @@ +mod cache; +mod controller; +mod entities; +mod service; +mod task; + +pub(crate) use cache::*; +pub use controller::*; +pub use entities::*; +pub(crate) use service::*; +pub(crate) use task::*; diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs new file mode 100644 index 0000000000000..76abb05f926cc --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/service.rs @@ -0,0 +1,142 @@ +use crate::entities::{CalculationType, FieldType}; + +use crate::services::field::TypeOptionCellExt; +use collab_database::fields::Field; +use collab_database::rows::RowCell; +use std::sync::Arc; + +pub struct CalculationsService {} + +impl CalculationsService { + pub fn new() -> Self { + Self {} + } + + pub fn calculate( + &self, + field: &Field, + calculation_type: i64, + row_cells: Vec>, + ) -> String { + let ty: CalculationType = calculation_type.into(); + + match ty { + CalculationType::Average => self.calculate_average(field, row_cells), + CalculationType::Max => self.calculate_max(field, row_cells), + CalculationType::Median => self.calculate_median(field, row_cells), + CalculationType::Min => self.calculate_min(field, row_cells), + CalculationType::Sum => self.calculate_sum(field, row_cells), + } + } + + fn calculate_average(&self, field: &Field, row_cells: Vec>) -> String { + 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) + { + for row_cell in row_cells { + if let Some(cell) = &row_cell.cell { + if let Some(value) = handler.handle_numeric_cell(cell) { + sum += value; + len += 1.0; + } + } + } + } + + if len > 0.0 { + format!("{:.5}", sum / len) + } else { + "0".to_owned() + } + } + + fn calculate_median(&self, field: &Field, row_cells: Vec>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| { + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.clone() + }); + + if !values.is_empty() { + format!("{:.5}", Self::median(&values)) + } else { + "".to_owned() + } + } + + fn median(array: &Vec) -> f64 { + if (array.len() % 2) == 0 { + let left = array.len() / 2 - 1; + let right = array.len() / 2; + (array[left] + array[right]) / 2.0 + } else { + array[array.len() / 2] + } + } + + fn calculate_min(&self, field: &Field, row_cells: Vec>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| { + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.clone() + }); + + if !values.is_empty() { + let min = values.iter().min_by(|a, b| a.total_cmp(b)); + if let Some(min) = min { + return format!("{:.5}", min); + } + } + + "".to_owned() + } + + fn calculate_max(&self, field: &Field, row_cells: Vec>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| { + values.sort_by(|a, b| a.partial_cmp(b).unwrap()); + values.clone() + }); + + if !values.is_empty() { + let max = values.iter().max_by(|a, b| a.total_cmp(b)); + if let Some(max) = max { + return format!("{:.5}", max); + } + } + + "".to_owned() + } + + fn calculate_sum(&self, field: &Field, row_cells: Vec>) -> String { + let values = self.reduce_values_f64(field, row_cells, |values| values.clone()); + + if !values.is_empty() { + format!("{:.5}", values.iter().sum::()) + } else { + "".to_owned() + } + } + + fn reduce_values_f64(&self, field: &Field, row_cells: Vec>, f: F) -> T + where + F: FnOnce(&mut Vec) -> T, + { + 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) + { + for row_cell in row_cells { + if let Some(cell) = &row_cell.cell { + if let Some(value) = handler.handle_numeric_cell(cell) { + values.push(value); + } + } + } + } + + f(&mut values) + } +} diff --git a/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs b/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs new file mode 100644 index 0000000000000..b8ae249c4bb53 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/calculations/task.rs @@ -0,0 +1,42 @@ +use lib_infra::future::BoxResultFuture; +use lib_infra::priority_task::{TaskContent, TaskHandler}; +use std::sync::Arc; + +use crate::services::calculations::CalculationsController; + +pub struct CalculationsTaskHandler { + handler_id: String, + calculations_controller: Arc, +} + +impl CalculationsTaskHandler { + pub fn new(handler_id: String, calculations_controller: Arc) -> Self { + Self { + handler_id, + calculations_controller, + } + } +} + +impl TaskHandler for CalculationsTaskHandler { + fn handler_id(&self) -> &str { + &self.handler_id + } + + fn handler_name(&self) -> &str { + "CalculationsTaskHandler" + } + + fn run(&self, content: TaskContent) -> BoxResultFuture<(), anyhow::Error> { + let calculations_controller = self.calculations_controller.clone(); + Box::pin(async move { + if let TaskContent::Text(predicate) = content { + calculations_controller + .process(&predicate) + .await + .map_err(anyhow::Error::from)?; + } + Ok(()) + }) + } +} 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 03bce144da55e..30e61dd098436 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 @@ -1,126 +1,7 @@ use parking_lot::RwLock; -use std::any::{type_name, Any}; -use std::collections::HashMap; -use std::fmt::Debug; -use std::hash::Hash; use std::sync::Arc; +use crate::utils::cache::AnyTypeCache; + pub type CellCache = Arc>>; pub type CellFilterCache = Arc>>; - -#[derive(Default, Debug)] -/// The better option is use LRU cache -pub struct AnyTypeCache(HashMap); - -impl AnyTypeCache -where - TypeValueKey: Clone + Hash + Eq, -{ - pub fn new() -> Arc>> { - Arc::new(RwLock::new(AnyTypeCache(HashMap::default()))) - } - - pub fn insert(&mut self, key: &TypeValueKey, val: T) -> Option - where - T: 'static + Send + Sync, - { - self - .0 - .insert(key.clone(), TypeValue::new(val)) - .and_then(downcast_owned) - } - - pub fn remove(&mut self, key: &TypeValueKey) { - self.0.remove(key); - } - - // pub fn remove>(&mut self, key: K) -> Option - // where - // T: 'static + Send + Sync, - // { - // self.0.remove(key.as_ref()).and_then(downcast_owned) - // } - - pub fn get(&self, key: &TypeValueKey) -> Option<&T> - where - T: 'static + Send + Sync, - { - self - .0 - .get(key) - .and_then(|type_value| type_value.boxed.downcast_ref()) - } - - pub fn get_mut(&mut self, key: &TypeValueKey) -> Option<&mut T> - where - T: 'static + Send + Sync, - { - self - .0 - .get_mut(key) - .and_then(|type_value| type_value.boxed.downcast_mut()) - } - - pub fn contains(&self, key: &TypeValueKey) -> bool { - self.0.contains_key(key) - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } -} - -fn downcast_owned(type_value: TypeValue) -> Option { - type_value.boxed.downcast().ok().map(|boxed| *boxed) -} - -#[derive(Debug)] -struct TypeValue { - boxed: Box, - #[allow(dead_code)] - ty: &'static str, -} - -impl TypeValue { - pub fn new(value: T) -> Self - where - T: Send + Sync + 'static, - { - Self { - boxed: Box::new(value), - ty: type_name::(), - } - } -} - -impl std::ops::Deref for TypeValue { - type Target = Box; - - fn deref(&self) -> &Self::Target { - &self.boxed - } -} - -impl std::ops::DerefMut for TypeValue { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.boxed - } -} - -// #[cfg(test)] -// mod tests { -// use crate::services::cell::CellDataCache; -// -// #[test] -// fn test() { -// let mut ext = CellDataCache::new(); -// ext.insert("1", "a".to_string()); -// ext.insert("2", 2); -// -// let a: &String = ext.get("1").unwrap(); -// assert_eq!(a, "a"); -// -// let a: Option<&usize> = ext.get("1"); -// assert!(a.is_none()); -// } -// } 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 b53f2f458b8f8..e8d1f41ad0530 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 @@ -39,6 +39,11 @@ pub trait CellDataDecoder: TypeOption { /// Same as [CellDataDecoder::stringify_cell_data] but the input parameter is the [Cell] fn stringify_cell(&self, cell: &Cell) -> String; + + /// Decode the cell into f64 + /// Different field type has different way to decode the cell data into f64 + /// If the field type doesn't support to decode the cell data into f64, it will return None + fn numeric_cell(&self, cell: &Cell) -> Option; } pub trait CellDataChangeset: TypeOption { @@ -172,7 +177,7 @@ pub fn stringify_cell_data( .get_type_option_cell_data_handler(from_field_type) { None => "".to_string(), - Some(handler) => handler.stringify_cell_str(cell, to_field_type, field), + 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 6c552aa57347d..bde5bc2cda840 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 @@ -16,9 +16,8 @@ use lib_infra::priority_task::TaskDispatcher; use crate::entities::*; use crate::notification::{send_notification, DatabaseNotification}; -use crate::services::cell::{ - apply_cell_changeset, get_cell_protobuf, AnyTypeCache, CellCache, ToCellChangeset, -}; +use crate::services::calculations::Calculation; +use crate::services::cell::{apply_cell_changeset, get_cell_protobuf, CellCache, ToCellChangeset}; use crate::services::database::util::database_view_setting_pb_from_view; use crate::services::database::UpdatedRow; use crate::services::database_view::{ @@ -37,6 +36,7 @@ use crate::services::filter::Filter; use crate::services::group::{default_group_setting, GroupChangesets, GroupSetting, RowChangeset}; use crate::services::share::csv::{CSVExport, CSVFormat}; use crate::services::sort::Sort; +use crate::utils::cache::AnyTypeCache; #[derive(Clone)] pub struct DatabaseEditor { @@ -226,6 +226,26 @@ impl DatabaseEditor { Ok(()) } + pub async fn get_all_calculations(&self, view_id: &str) -> RepeatedCalculationsPB { + if let Ok(view_editor) = self.database_views.get_view_editor(view_id).await { + view_editor.v_get_all_calculations().await.into() + } else { + RepeatedCalculationsPB { items: vec![] } + } + } + + pub async fn update_calculation(&self, update: UpdateCalculationChangesetPB) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(&update.view_id).await?; + view_editor.v_update_calculations(update).await?; + Ok(()) + } + + pub async fn remove_calculation(&self, remove: RemoveCalculationChangesetPB) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(&remove.view_id).await?; + view_editor.v_remove_calculation(remove).await?; + Ok(()) + } + 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() @@ -310,6 +330,11 @@ impl DatabaseEditor { let notified_changeset = DatabaseFieldChangesetPB::delete(&database_id, vec![FieldIdPB::from(field_id)]); self.notify_did_update_database(notified_changeset).await?; + + for view in self.database_views.editors().await { + view.v_did_delete_field(field_id).await; + } + Ok(()) } @@ -364,6 +389,10 @@ impl DatabaseEditor { .set_field_type(new_field_type.into()) .set_type_option(new_field_type.into(), Some(transformed_type_option)); }); + + for view in self.database_views.editors().await { + view.v_did_update_field_type(field_id, new_field_type).await; + } }, } @@ -405,7 +434,12 @@ impl DatabaseEditor { match params { None => warn!("Failed to duplicate row: {}", row_id), Some(params) => { - let _ = self.create_row(view_id, None, params).await; + 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; + } + } }, } } @@ -725,7 +759,9 @@ impl DatabaseEditor { 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 { - view.v_did_update_row(&old_row, &new_row_detail).await; + view + .v_did_update_row(&old_row, &new_row_detail, field_id.to_owned()) + .await; } } @@ -1423,6 +1459,23 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { self.database.lock().remove_all_sorts(view_id); } + fn get_all_calculations(&self, view_id: &str) -> Vec> { + self + .database + .lock() + .get_all_calculations(view_id) + .into_iter() + .map(Arc::new) + .collect() + } + + fn get_calculation(&self, view_id: &str, field_id: &str) -> Option { + self + .database + .lock() + .get_calculation::(view_id, field_id) + } + fn get_all_filters(&self, view_id: &str) -> Vec> { self .database @@ -1569,6 +1622,17 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { .payload(FieldSettingsPB::from(new_field_settings)) .send() } + + fn update_calculation(&self, view_id: &str, calculation: Calculation) { + self + .database + .lock() + .update_calculation(view_id, calculation) + } + + fn remove_calculation(&self, view_id: &str, field_id: &str) { + self.database.lock().remove_calculation(view_id, field_id) + } } #[tracing::instrument(level = "trace", skip_all, err)] diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs index 6522c8e917e14..1f4649450be1f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/mod.rs @@ -6,6 +6,7 @@ pub use views::*; mod layout_deps; mod notifier; +mod view_calculations; mod view_editor; mod view_filter; mod view_group; 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 173be64d96b9e..03267a7cf0a53 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,7 +1,8 @@ #![allow(clippy::while_let_loop)] use crate::entities::{ - DatabaseViewSettingPB, FilterChangesetNotificationPB, GroupChangesPB, GroupRowsNotificationPB, - ReorderAllRowsPB, ReorderSingleRowPB, RowsVisibilityChangePB, SortChangesetNotificationPB, + CalculationChangesetNotificationPB, DatabaseViewSettingPB, FilterChangesetNotificationPB, + GroupChangesPB, GroupRowsNotificationPB, ReorderAllRowsPB, ReorderSingleRowPB, + RowsVisibilityChangePB, SortChangesetNotificationPB, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::filter::FilterResultNotification; @@ -15,6 +16,7 @@ pub enum DatabaseViewChanged { FilterNotification(FilterResultNotification), ReorderAllRowsNotification(ReorderAllRowsResult), ReorderSingleRowNotification(ReorderSingleRowResult), + CalculationValueNotification(CalculationChangesetNotificationPB), } pub type DatabaseViewChangedNotifier = broadcast::Sender; @@ -76,6 +78,12 @@ impl DatabaseViewChangedReceiverRunner { .payload(reorder_row) .send() }, + DatabaseViewChanged::CalculationValueNotification(notification) => send_notification( + ¬ification.view_id, + DatabaseNotification::DidUpdateCalculation, + ) + .payload(notification) + .send(), } }) .await; @@ -94,6 +102,15 @@ pub async fn notify_did_update_filter(notification: FilterChangesetNotificationP .send(); } +pub async fn notify_did_update_calculation(notification: CalculationChangesetNotificationPB) { + send_notification( + ¬ification.view_id, + DatabaseNotification::DidUpdateCalculation, + ) + .payload(notification) + .send(); +} + pub async fn notify_did_update_sort(notification: SortChangesetNotificationPB) { if !notification.is_empty() { send_notification(¬ification.view_id, DatabaseNotification::DidUpdateSort) diff --git a/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs b/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs new file mode 100644 index 0000000000000..f329e498a4787 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/services/database_view/view_calculations.rs @@ -0,0 +1,69 @@ +use collab_database::fields::Field; +use std::sync::Arc; + +use collab_database::rows::RowCell; +use lib_infra::future::{to_fut, Fut}; + +use crate::services::calculations::{ + Calculation, CalculationsController, CalculationsDelegate, CalculationsTaskHandler, +}; + +use crate::services::database_view::{ + gen_handler_id, DatabaseViewChangedNotifier, DatabaseViewOperation, +}; + +pub async fn make_calculations_controller( + view_id: &str, + delegate: Arc, + notifier: DatabaseViewChangedNotifier, +) -> Arc { + let calculations = delegate.get_all_calculations(view_id); + let task_scheduler = delegate.get_task_scheduler(); + let calculations_delegate = DatabaseViewCalculationsDelegateImpl(delegate.clone()); + let handler_id = gen_handler_id(); + + let calculations_controller = CalculationsController::new( + view_id, + &handler_id, + calculations_delegate, + calculations, + task_scheduler.clone(), + notifier, + ) + .await; + + let calculations_controller = Arc::new(calculations_controller); + task_scheduler + .write() + .await + .register_handler(CalculationsTaskHandler::new( + handler_id, + calculations_controller.clone(), + )); + calculations_controller +} + +struct DatabaseViewCalculationsDelegateImpl(Arc); + +impl CalculationsDelegate for DatabaseViewCalculationsDelegateImpl { + fn get_cells_for_field(&self, view_id: &str, field_id: &str) -> Fut>> { + self.0.get_cells_for_field(view_id, field_id) + } + + fn get_field(&self, field_id: &str) -> Option { + self.0.get_field(field_id) + } + + fn get_calculation(&self, view_id: &str, field_id: &str) -> Fut>> { + let calculation = self.0.get_calculation(view_id, field_id).map(Arc::new); + to_fut(async move { calculation }) + } + + fn update_calculation(&self, view_id: &str, calculation: Calculation) { + self.0.update_calculation(view_id, calculation) + } + + fn remove_calculation(&self, view_id: &str, calculation_id: &str) { + self.0.remove_calculation(view_id, calculation_id) + } +} 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 28a8ffb00cd0f..4056255026574 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,7 +2,9 @@ use std::borrow::Cow; use std::collections::HashMap; use std::sync::Arc; -use collab_database::database::{gen_database_filter_id, gen_database_sort_id}; +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::rows::{Cells, Row, RowDetail, RowId}; use collab_database::views::{DatabaseLayout, DatabaseView}; @@ -15,10 +17,12 @@ use lib_dispatch::prelude::af_spawn; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, - LayoutSettingChangeset, LayoutSettingParams, RowMetaPB, RowsChangePB, - SortChangesetNotificationPB, SortPB, UpdateFilterParams, UpdateSortParams, + LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, RowMetaPB, + RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, + UpdateFilterParams, UpdateSortParams, }; use crate::notification::{send_notification, DatabaseNotification}; +use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController}; use crate::services::cell::CellCache; use crate::services::database::{database_view_setting_pb_from_view, DatabaseRowEvent, UpdatedRow}; use crate::services::database_view::view_filter::make_filter_controller; @@ -40,12 +44,16 @@ use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowConte use crate::services::setting::CalendarLayoutSetting; use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; +use super::notify_did_update_calculation; +use super::view_calculations::make_calculations_controller; + pub struct DatabaseViewEditor { pub view_id: String, delegate: Arc, group_controller: Arc>>>, filter_controller: Arc, sort_controller: Arc>, + calculations_controller: Arc, pub notifier: DatabaseViewChangedNotifier, } @@ -87,12 +95,17 @@ impl DatabaseViewEditor { ) .await; + // Calculations + let calculations_controller = + make_calculations_controller(&view_id, delegate.clone(), notifier.clone()).await; + Ok(Self { view_id, delegate, group_controller, filter_controller, sort_controller, + calculations_controller, notifier, }) } @@ -100,6 +113,7 @@ impl DatabaseViewEditor { pub async fn close(&self) { self.sort_controller.write().await.close().await; self.filter_controller.close().await; + self.calculations_controller.close().await; } pub async fn v_get_view(&self) -> Option { @@ -148,8 +162,17 @@ impl DatabaseViewEditor { .send(); } + pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) { + self + .calculations_controller + .did_receive_row_changed(row_detail.clone().row) + .await; + } + #[tracing::instrument(level = "trace", skip_all)] pub async fn v_did_delete_row(&self, row: &Row) { + let deleted_row = row.clone(); + // Send the group notification if the current view has groups; let result = self .mut_group_controller(|group_controller, _| group_controller.did_delete_row(row)) @@ -170,15 +193,32 @@ impl DatabaseViewEditor { } } let changes = RowsChangePB::from_delete(row.id.clone().into_inner()); + send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); + + // Updating calculations for each of the Rows cells is a tedious task + // Therefore we spawn a separate task for this + let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); + af_spawn(async move { + if let Some(calculations_controller) = weak_calculations_controller.upgrade() { + calculations_controller + .did_receive_row_changed(deleted_row) + .await; + } + }); } /// Notify the view that the row has been updated. If the view has groups, /// send the group notification with [GroupRowsNotificationPB]. Otherwise, /// send the view notification with [RowsChangePB] - pub async fn v_did_update_row(&self, old_row: &Option, row_detail: &RowDetail) { + pub async fn v_did_update_row( + &self, + old_row: &Option, + 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)) @@ -211,11 +251,12 @@ impl DatabaseViewEditor { } } - // Each row update will trigger a filter and sort operation. We don't want + // Each row update will trigger a calculations, filter and sort operation. We don't want // to block the main thread, so we spawn a new task to do the work. let row_id = row_detail.row.id.clone(); let weak_filter_controller = Arc::downgrade(&self.filter_controller); let weak_sort_controller = Arc::downgrade(&self.sort_controller); + let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); af_spawn(async move { if let Some(filter_controller) = weak_filter_controller.upgrade() { filter_controller @@ -226,7 +267,13 @@ impl DatabaseViewEditor { sort_controller .read() .await - .did_receive_row_changed(row_id) + .did_receive_row_changed(row_id.clone()) + .await; + } + + if let Some(calculations_controller) = weak_calculations_controller.upgrade() { + calculations_controller + .did_receive_cell_changed(field_id) .await; } }); @@ -508,6 +555,68 @@ impl DatabaseViewEditor { Ok(()) } + pub async fn v_get_all_calculations(&self) -> Vec> { + self.delegate.get_all_calculations(&self.view_id) + } + + pub async fn v_update_calculations( + &self, + params: UpdateCalculationChangesetPB, + ) -> FlowyResult<()> { + let calculation_id = match params.calculation_id { + None => gen_database_calculation_id(), + Some(calculation_id) => calculation_id, + }; + + let calculation = Calculation::none( + calculation_id, + params.field_id, + Some(params.calculation_type.value()), + ); + + let changeset = self + .calculations_controller + .did_receive_changes(CalculationChangeset::from_insert(calculation.clone())) + .await; + + if let Some(changeset) = changeset { + if !changeset.insert_calculations.is_empty() { + for insert in changeset.insert_calculations.clone() { + let calculation: Calculation = Calculation::from(&insert); + self + .delegate + .update_calculation(¶ms.view_id, calculation); + } + } + + notify_did_update_calculation(changeset).await; + } + + Ok(()) + } + + pub async fn v_remove_calculation( + &self, + params: RemoveCalculationChangesetPB, + ) -> FlowyResult<()> { + self + .delegate + .remove_calculation(¶ms.view_id, ¶ms.calculation_id); + + let calculation = Calculation::none(params.calculation_id, params.field_id, None); + + let changeset = self + .calculations_controller + .did_receive_changes(CalculationChangeset::from_delete(calculation.clone())) + .await; + + if let Some(changeset) = changeset { + notify_did_update_calculation(changeset).await; + } + + Ok(()) + } + pub async fn v_get_all_filters(&self) -> Vec> { self.delegate.get_all_filters(&self.view_id) } @@ -912,6 +1021,20 @@ impl DatabaseViewEditor { Ok(()) } + pub async fn v_did_delete_field(&self, field_id: &str) { + self + .calculations_controller + .did_receive_field_deleted(field_id.to_owned()) + .await; + } + + pub async fn v_did_update_field_type(&self, field_id: &str, new_field_type: &FieldType) { + self + .calculations_controller + .did_receive_field_type_changed(field_id.to_owned(), new_field_type.to_owned()) + .await; + } + async fn mut_group_controller(&self, f: F) -> Option where F: FnOnce(&mut Box, Field) -> FlowyResult, 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 9c525ed3d697e..291f98ac03243 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 @@ -12,6 +12,7 @@ use lib_infra::future::{Fut, FutureResult}; use lib_infra::priority_task::TaskDispatcher; use crate::entities::{FieldType, FieldVisibility}; +use crate::services::calculations::Calculation; use crate::services::field::TypeOptionCellDataHandler; use crate::services::field_settings::FieldSettings; use crate::services::filter::Filter; @@ -80,6 +81,14 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { fn remove_all_sorts(&self, view_id: &str); + fn get_all_calculations(&self, view_id: &str) -> Vec>; + + fn get_calculation(&self, view_id: &str, field_id: &str) -> Option; + + fn update_calculation(&self, view_id: &str, calculation: Calculation); + + fn remove_calculation(&self, view_id: &str, calculation_id: &str); + fn get_all_filters(&self, view_id: &str) -> Vec>; fn delete_filter(&self, view_id: &str, filter_id: &str); 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 8859422b9c113..005cb48443a15 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 @@ -17,6 +17,7 @@ use crate::services::group::RowChangeset; pub type RowEventSender = broadcast::Sender; pub type RowEventReceiver = broadcast::Receiver; pub type EditorByViewId = HashMap>; + pub struct DatabaseViews { #[allow(dead_code)] database: Arc, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs index 7645bf2564b93..66c20137a3bb7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checkbox_type_option/checkbox_type_option.rs @@ -102,6 +102,15 @@ impl CellDataDecoder for CheckboxTypeOption { fn stringify_cell(&self, cell: &Cell) -> String { Self::CellData::from(cell).to_string() } + + fn numeric_cell(&self, cell: &Cell) -> Option { + let cell_data = self.parse_cell(cell).ok()?; + if cell_data.is_check() { + Some(1.0) + } else { + Some(0.0) + } + } } pub type CheckboxCellChangeset = String; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs index 825e39b5390e8..1169c89613c8f 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/checklist_type_option/checklist.rs @@ -164,6 +164,11 @@ impl CellDataDecoder for ChecklistTypeOption { let cell_data = self.parse_cell(cell).unwrap_or_default(); self.stringify_cell_data(cell_data) } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + // return the percentage complete if needed + None + } } impl TypeOptionCellDataFilter for ChecklistTypeOption { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs index 28921f1a2afd1..e80348c41d509 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/date_type_option/date_type_option.rs @@ -251,6 +251,10 @@ impl CellDataDecoder for DateTypeOption { let cell_data = Self::CellData::from(cell); self.stringify_cell_data(cell_data) } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } } impl CellDataChangeset for DateTypeOption { diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs index 0ee75faed71c8..2241a4c3bb7f2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/number_type_option/number_type_option.rs @@ -208,6 +208,11 @@ impl CellDataDecoder for NumberTypeOption { let cell_data = Self::CellData::from(cell); self.stringify_cell_data(cell_data) } + + fn numeric_cell(&self, cell: &Cell) -> Option { + let num_cell_data = self.parse_cell(cell).ok()?; + num_cell_data.0.parse::().ok() + } } pub type NumberCellChangeset = String; diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs index 1e6aa781a1fc7..5d82147d74cbb 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/selection_type_option/select_type_option.rs @@ -118,10 +118,10 @@ where } } -impl CellDataDecoder for T +impl CellDataDecoder for T where - T: - SelectTypeOptionSharedAction + TypeOption + TypeOptionCellDataSerde, + C: Into + for<'a> From<&'a Cell>, + T: SelectTypeOptionSharedAction + TypeOption + TypeOptionCellDataSerde, { fn decode_cell( &self, @@ -132,9 +132,9 @@ where self.parse_cell(cell) } - fn stringify_cell_data(&self, cell_data: ::CellData) -> String { + fn stringify_cell_data(&self, cell_data: C) -> String { self - .get_selected_options(cell_data) + .get_selected_options(cell_data.into()) .select_options .into_iter() .map(|option| option.name) @@ -143,9 +143,13 @@ where } fn stringify_cell(&self, cell: &Cell) -> String { - let cell_data = Self::CellData::from(cell); + let cell_data = C::from(cell); self.stringify_cell_data(cell_data) } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } } pub fn select_type_option_from_field( diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs index 0e8f48302c849..32e9096e87947 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/text_type_option/text_type_option.rs @@ -116,6 +116,10 @@ impl CellDataDecoder for RichTextTypeOption { fn stringify_cell(&self, cell: &Cell) -> String { Self::CellData::from(cell).to_string() } + + fn numeric_cell(&self, cell: &Cell) -> Option { + StrCellData::from(cell).0.parse::().ok() + } } impl CellDataChangeset for RichTextTypeOption { 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 549b777a3dcd9..ada47d3864690 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 @@ -152,6 +152,10 @@ impl CellDataDecoder for TimestampTypeOption { let cell_data = Self::CellData::from(cell); self.stringify_cell_data(cell_data) } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } } impl CellDataChangeset for TimestampTypeOption { 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 2b69b74130a63..dfe28f63a4030 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 @@ -62,7 +62,9 @@ pub trait TypeOptionCellDataHandler: Send + Sync + 'static { /// For example, the field type of the [TypeOptionCellDataHandler] is [FieldType::Date], and /// the if field_type is [FieldType::RichText], then the string would be something like "Mar 14, 2022". /// - fn stringify_cell_str(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String; + fn handle_stringify_cell(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String; + + fn handle_numeric_cell(&self, cell: &Cell) -> Option; /// Format the cell to [BoxCellData] using the passed-in [FieldType] and [Field]. /// The caller can get the cell data by calling [BoxCellData::unbox_or_none]. @@ -323,7 +325,7 @@ where /// is [FieldType::RichText], then the string will be transformed to a string that separated by comma with the /// option's name. /// - fn stringify_cell_str(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String { + fn handle_stringify_cell(&self, cell: &Cell, field_type: &FieldType, field: &Field) -> String { if self.transformable() { let cell_data = self.transform_type_option_cell(cell, field_type, field); if let Some(cell_data) = cell_data { @@ -333,6 +335,10 @@ where self.stringify_cell(cell) } + fn handle_numeric_cell(&self, cell: &Cell) -> Option { + self.numeric_cell(cell) + } + fn get_cell_data( &self, cell: &Cell, diff --git a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs index 37f93d8b6666d..b236b31018530 100644 --- a/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs +++ b/frontend/rust-lib/flowy-database2/src/services/field/type_options/url_type_option/url_type_option.rs @@ -82,6 +82,10 @@ impl CellDataDecoder for URLTypeOption { let cell_data = Self::CellData::from(cell); self.stringify_cell_data(cell_data) } + + fn numeric_cell(&self, _cell: &Cell) -> Option { + None + } } pub type URLCellChangeset = String; 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 00df7509068a3..50aa55b18aae7 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -14,10 +14,11 @@ use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatch use crate::entities::filter_entities::*; use crate::entities::{FieldType, InsertedRowPB, RowMetaPB}; -use crate::services::cell::{AnyTypeCache, CellCache, CellFilterCache}; +use crate::services::cell::{CellCache, CellFilterCache}; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::field::*; use crate::services::filter::{Filter, FilterChangeset, FilterResult, FilterResultNotification}; +use crate::utils::cache::AnyTypeCache; pub trait FilterDelegate: Send + Sync + 'static { fn get_filter(&self, view_id: &str, filter_id: &str) -> Fut>>; diff --git a/frontend/rust-lib/flowy-database2/src/services/mod.rs b/frontend/rust-lib/flowy-database2/src/services/mod.rs index 3faf7725a11b9..3f5770691442d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/services/mod.rs @@ -1,3 +1,4 @@ +pub mod calculations; pub mod cell; pub mod database; pub mod database_view; diff --git a/frontend/rust-lib/flowy-database2/src/utils/cache.rs b/frontend/rust-lib/flowy-database2/src/utils/cache.rs new file mode 100644 index 0000000000000..5f9bda50c996b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/utils/cache.rs @@ -0,0 +1,98 @@ +use parking_lot::RwLock; +use std::any::{type_name, Any}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::sync::Arc; + +#[derive(Default, Debug)] +/// The better option is use LRU cache +pub struct AnyTypeCache(HashMap); + +impl AnyTypeCache +where + TypeValueKey: Clone + Hash + Eq, +{ + pub fn new() -> Arc>> { + Arc::new(RwLock::new(AnyTypeCache(HashMap::default()))) + } + + pub fn insert(&mut self, key: &TypeValueKey, val: T) -> Option + where + T: 'static + Send + Sync, + { + self + .0 + .insert(key.clone(), TypeValue::new(val)) + .and_then(downcast_owned) + } + + pub fn remove(&mut self, key: &TypeValueKey) { + self.0.remove(key); + } + + pub fn get(&self, key: &TypeValueKey) -> Option<&T> + where + T: 'static + Send + Sync, + { + self + .0 + .get(key) + .and_then(|type_value| type_value.boxed.downcast_ref()) + } + + pub fn get_mut(&mut self, key: &TypeValueKey) -> Option<&mut T> + where + T: 'static + Send + Sync, + { + self + .0 + .get_mut(key) + .and_then(|type_value| type_value.boxed.downcast_mut()) + } + + pub fn contains(&self, key: &TypeValueKey) -> bool { + self.0.contains_key(key) + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +fn downcast_owned(type_value: TypeValue) -> Option { + type_value.boxed.downcast().ok().map(|boxed| *boxed) +} + +#[derive(Debug)] +struct TypeValue { + boxed: Box, + #[allow(dead_code)] + ty: &'static str, +} + +impl TypeValue { + pub fn new(value: T) -> Self + where + T: Send + Sync + 'static, + { + Self { + boxed: Box::new(value), + ty: type_name::(), + } + } +} + +impl std::ops::Deref for TypeValue { + type Target = Box; + + fn deref(&self) -> &Self::Target { + &self.boxed + } +} + +impl std::ops::DerefMut for TypeValue { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.boxed + } +} diff --git a/frontend/rust-lib/flowy-database2/src/utils/mod.rs b/frontend/rust-lib/flowy-database2/src/utils/mod.rs new file mode 100644 index 0000000000000..a5c08fdb0d2c2 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/src/utils/mod.rs @@ -0,0 +1 @@ +pub mod cache; diff --git a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs new file mode 100644 index 0000000000000..2800596900b0b --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/calculation_test.rs @@ -0,0 +1,87 @@ +use std::sync::Arc; + +use crate::database::calculations_test::script::{CalculationScript::*, DatabaseCalculationTest}; + +use collab_database::fields::Field; +use flowy_database2::entities::{CalculationType, FieldType, UpdateCalculationChangesetPB}; + +#[tokio::test] +async fn calculations_test() { + let mut test = DatabaseCalculationTest::new().await; + + let expected_sum = 25.00000; + let expected_min = 1.00000; + let expected_average = 5.00000; + let expected_max = 14.00000; + let expected_median = 3.00000; + + let view_id = &test.view_id; + let number_fields = test + .fields + .clone() + .into_iter() + .filter(|field| field.field_type == FieldType::Number as i64) + .collect::>>(); + let field_id = &number_fields.first().unwrap().id; + + let calculation_id = "calc_id".to_owned(); + let scripts = vec![ + // Insert Sum calculation first time + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Sum, + }, + }, + AssertCalculationValue { + expected: expected_sum, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Min, + }, + }, + AssertCalculationValue { + expected: expected_min, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Average, + }, + }, + AssertCalculationValue { + expected: expected_average, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id.clone()), + calculation_type: CalculationType::Max, + }, + }, + AssertCalculationValue { + expected: expected_max, + }, + InsertCalculation { + payload: UpdateCalculationChangesetPB { + view_id: view_id.to_owned(), + field_id: field_id.to_owned(), + calculation_id: Some(calculation_id), + calculation_type: CalculationType::Median, + }, + }, + AssertCalculationValue { + expected: expected_median, + }, + ]; + test.run_scripts(scripts).await; +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/mod.rs new file mode 100644 index 0000000000000..258788eb1c8c0 --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/mod.rs @@ -0,0 +1,2 @@ +mod calculation_test; +mod script; diff --git a/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs new file mode 100644 index 0000000000000..978acd84633fc --- /dev/null +++ b/frontend/rust-lib/flowy-database2/tests/database/calculations_test/script.rs @@ -0,0 +1,74 @@ +use tokio::sync::broadcast::Receiver; + +use flowy_database2::entities::UpdateCalculationChangesetPB; +use flowy_database2::services::database_view::DatabaseViewChanged; + +use crate::database::database_editor::DatabaseEditorTest; + +pub enum CalculationScript { + InsertCalculation { + payload: UpdateCalculationChangesetPB, + }, + AssertCalculationValue { + expected: f64, + }, +} + +pub struct DatabaseCalculationTest { + inner: DatabaseEditorTest, + recv: Option>, +} + +impl DatabaseCalculationTest { + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { + inner: editor_test, + recv: None, + } + } + + pub fn view_id(&self) -> String { + self.view_id.clone() + } + + 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: CalculationScript) { + match script { + CalculationScript::InsertCalculation { payload } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id()) + .await + .unwrap(), + ); + self.editor.update_calculation(payload).await.unwrap(); + }, + CalculationScript::AssertCalculationValue { expected } => { + let calculations = self.editor.get_all_calculations(&self.view_id()).await; + let calculation = calculations.items.first().unwrap(); + assert_eq!(calculation.value, format!("{:.5}", expected)); + }, + } + } +} + +impl std::ops::Deref for DatabaseCalculationTest { + type Target = DatabaseEditorTest; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl std::ops::DerefMut for DatabaseCalculationTest { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} diff --git a/frontend/rust-lib/flowy-database2/tests/database/mod.rs b/frontend/rust-lib/flowy-database2/tests/database/mod.rs index 5d916806d7e03..5333d54c337fe 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/mod.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/mod.rs @@ -1,4 +1,5 @@ mod block_test; +mod calculations_test; mod cell_test; mod database_editor; mod field_settings_test; From 8c1d0106ddadaabc584e98f15afedd718eb32e3c Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 4 Feb 2024 00:55:05 +0800 Subject: [PATCH 04/50] chore: inkwell mobile quick action sheets (#4576) * chore: inkwell mobile quick action sheets * chore: more subtle tap effect * chore: improve code style --- .../bottom_sheet_view_item_body.dart | 46 +++++------- .../bottom_sheet/bottom_sheet_view_page.dart | 46 +++++------- .../mobile_card_detail_screen.dart | 74 ++++++++----------- .../view/database_view_quick_actions.dart | 16 ++-- .../flowy_mobile_quick_action_button.dart | 44 +++++++---- 5 files changed, 107 insertions(+), 119 deletions(-) diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart index 2e869875f9e6e..2b8209987c03b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item_body.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/widgets/widgets.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra_ui/widget/separated_flex.dart'; import 'package:flutter/material.dart'; enum MobileViewItemBottomSheetBodyAction { @@ -25,55 +26,46 @@ class MobileViewItemBottomSheetBody extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( + return SeparatedColumn( crossAxisAlignment: CrossAxisAlignment.stretch, + separatorBuilder: () => const Divider( + height: 8.5, + thickness: 0.5, + ), children: [ - FlowyOptionTile.text( + MobileQuickActionButton( text: LocaleKeys.button_rename.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_rename_s, - ), - showTopBorder: false, + icon: FlowySvgs.m_rename_s, onTap: () => onAction( MobileViewItemBottomSheetBodyAction.rename, ), ), - FlowyOptionTile.text( + MobileQuickActionButton( text: isFavorite ? LocaleKeys.button_removeFromFavorites.tr() : LocaleKeys.button_addToFavorites.tr(), - leftIcon: FlowySvg( - size: const Size(20, 20), - isFavorite - ? FlowySvgs.m_favorite_selected_lg - : FlowySvgs.m_favorite_unselected_lg, - color: isFavorite ? Colors.yellow : null, - ), - showTopBorder: false, + icon: isFavorite + ? FlowySvgs.m_favorite_selected_lg + : FlowySvgs.m_favorite_unselected_lg, + iconColor: isFavorite ? Colors.yellow : null, onTap: () => onAction( isFavorite ? MobileViewItemBottomSheetBodyAction.removeFromFavorites : MobileViewItemBottomSheetBodyAction.addToFavorites, ), ), - FlowyOptionTile.text( + MobileQuickActionButton( text: LocaleKeys.button_duplicate.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_duplicate_s, - ), - showTopBorder: false, + icon: FlowySvgs.m_duplicate_s, onTap: () => onAction( MobileViewItemBottomSheetBodyAction.duplicate, ), ), - FlowyOptionTile.text( + MobileQuickActionButton( text: LocaleKeys.button_delete.tr(), textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.m_delete_s, - color: Theme.of(context).colorScheme.error, - ), - showTopBorder: false, + icon: FlowySvgs.m_delete_s, + iconColor: Theme.of(context).colorScheme.error, onTap: () => onAction( MobileViewItemBottomSheetBodyAction.delete, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart index d3d23ddbbd86a..99ea534b093a8 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_page.dart @@ -1,9 +1,10 @@ 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/widgets/widgets.dart'; +import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.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'; enum MobileViewBottomSheetBodyAction { @@ -84,55 +85,46 @@ class MobileViewBottomSheetBody extends StatelessWidget { @override Widget build(BuildContext context) { final isFavorite = view.isFavorite; - return Column( + return SeparatedColumn( crossAxisAlignment: CrossAxisAlignment.stretch, + separatorBuilder: () => const Divider( + height: 8.5, + thickness: 0.5, + ), children: [ - FlowyOptionTile.text( + MobileQuickActionButton( text: LocaleKeys.button_rename.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_rename_s, - ), - showTopBorder: false, + icon: FlowySvgs.m_rename_s, onTap: () => onAction( MobileViewBottomSheetBodyAction.rename, ), ), - FlowyOptionTile.text( + MobileQuickActionButton( text: isFavorite ? LocaleKeys.button_removeFromFavorites.tr() : LocaleKeys.button_addToFavorites.tr(), - leftIcon: FlowySvg( - size: const Size(20, 20), - isFavorite - ? FlowySvgs.m_favorite_selected_lg - : FlowySvgs.m_favorite_unselected_lg, - color: isFavorite ? Colors.yellow : null, - ), - showTopBorder: false, + icon: isFavorite + ? FlowySvgs.m_favorite_selected_lg + : FlowySvgs.m_favorite_unselected_lg, + iconColor: isFavorite ? Colors.yellow : null, onTap: () => onAction( isFavorite ? MobileViewBottomSheetBodyAction.removeFromFavorites : MobileViewBottomSheetBodyAction.addToFavorites, ), ), - FlowyOptionTile.text( + MobileQuickActionButton( text: LocaleKeys.button_duplicate.tr(), - leftIcon: const FlowySvg( - FlowySvgs.m_duplicate_s, - ), - showTopBorder: false, + icon: FlowySvgs.m_duplicate_s, onTap: () => onAction( MobileViewBottomSheetBodyAction.duplicate, ), ), - FlowyOptionTile.text( + MobileQuickActionButton( text: LocaleKeys.button_delete.tr(), textColor: Theme.of(context).colorScheme.error, - leftIcon: FlowySvg( - FlowySvgs.m_delete_s, - color: Theme.of(context).colorScheme.error, - ), - showTopBorder: false, + icon: FlowySvgs.m_delete_s, + iconColor: Theme.of(context).colorScheme.error, onTap: () => onAction( MobileViewBottomSheetBodyAction.delete, ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index dbaeea10fc7ed..28b50454db33b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -135,54 +135,44 @@ class _MobileRowDetailPageState extends State { builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: MobileQuickActionButton( - onTap: () { - final rowId = _bloc.state.currentRowId; - if (rowId == null) { - return; - } - RowBackendService.duplicateRow(viewId, rowId); - context - ..pop() - ..pop(); - Fluttertoast.showToast( - msg: LocaleKeys.board_cardDuplicated.tr(), - gravity: ToastGravity.BOTTOM, - ); - }, - icon: FlowySvgs.copy_s, - text: LocaleKeys.button_duplicate.tr(), - ), + MobileQuickActionButton( + onTap: () => + _performAction(viewId, _bloc.state.currentRowId, false), + icon: FlowySvgs.copy_s, + text: LocaleKeys.button_duplicate.tr(), ), - const Divider(height: 9), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: MobileQuickActionButton( - onTap: () { - final rowId = _bloc.state.currentRowId; - if (rowId == null) { - return; - } - RowBackendService.deleteRow(viewId, rowId); - context - ..pop() - ..pop(); - Fluttertoast.showToast( - msg: LocaleKeys.board_cardDeleted.tr(), - gravity: ToastGravity.BOTTOM, - ); - }, - icon: FlowySvgs.m_delete_m, - text: LocaleKeys.button_delete.tr(), - color: Theme.of(context).colorScheme.error, - ), + const Divider(height: 8.5, thickness: 0.5), + MobileQuickActionButton( + onTap: () => _performAction(viewId, _bloc.state.currentRowId, true), + text: LocaleKeys.button_delete.tr(), + textColor: Theme.of(context).colorScheme.error, + icon: FlowySvgs.m_delete_m, + iconColor: Theme.of(context).colorScheme.error, ), ], ), ); } + + void _performAction(String viewId, String? rowId, bool deleteRow) { + if (rowId == null) { + return; + } + + deleteRow + ? RowBackendService.deleteRow(viewId, rowId) + : RowBackendService.duplicateRow(viewId, rowId); + + context + ..pop() + ..pop(); + Fluttertoast.showToast( + msg: deleteRow + ? LocaleKeys.board_cardDeleted.tr() + : LocaleKeys.board_cardDuplicated.tr(), + gravity: ToastGravity.BOTTOM, + ); + } } class RowDetailFab extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart index 468730e2f1c1b..0caf6e4a55100 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_view_quick_actions.dart @@ -72,18 +72,16 @@ class MobileDatabaseViewQuickActions extends StatelessWidget { _Action action, VoidCallback onTap, ) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: MobileQuickActionButton( - icon: action.icon, - text: action.label, - color: action.color(context), - onTap: onTap, - ), + return MobileQuickActionButton( + icon: action.icon, + text: action.label, + textColor: action.color(context), + iconColor: action.color(context), + onTap: onTap, ); } - Widget _divider() => const Divider(height: 9); + Widget _divider() => const Divider(height: 8.5, thickness: 0.5); } enum _Action { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart index 0d6efbe42cbd2..a267085dbc92e 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart @@ -8,28 +8,44 @@ class MobileQuickActionButton extends StatelessWidget { required this.onTap, required this.icon, required this.text, - this.color, + this.textColor, + this.iconColor, }); final VoidCallback onTap; final FlowySvgData icon; final String text; - final Color? color; + final Color? textColor; + final Color? iconColor; @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: Container( - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Row( - children: [ - FlowySvg(icon, size: const Size.square(20), color: color), - const HSpace(8), - FlowyText(text, fontSize: 15, color: color), - ], + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + splashColor: Colors.transparent, + child: Container( + height: 44, + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + FlowySvg( + icon, + size: const Size.square(20), + color: iconColor, + ), + const HSpace(12), + Expanded( + child: FlowyText( + text, + fontSize: 15, + color: textColor, + ), + ), + ], + ), ), ), ); From ca93cb20ecdfe6ec756049aa56bb5e72b7a141e8 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 4 Feb 2024 00:55:44 +0800 Subject: [PATCH 05/50] chore: revamp mobile database toolbar (#4579) * chore: revamp mobile database toolbar * chore: code cleanup --- .../mobile_card_detail_screen.dart | 1 + .../database/view/database_field_list.dart | 21 ++- .../view/edit_database_view_screen.dart | 1 + .../tab_bar/mobile/mobile_tab_bar_header.dart | 177 +++++++----------- .../setting/mobile_database_controls.dart | 80 +++----- .../resources/flowy_icons/16x/arrow_tight.svg | 5 + 6 files changed, 106 insertions(+), 179 deletions(-) create mode 100644 frontend/resources/flowy_icons/16x/arrow_tight.svg diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart index 28b50454db33b..2a1e8ca22b75b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart @@ -81,6 +81,7 @@ class _MobileRowDetailPageState extends State { child: Scaffold( appBar: FlowyAppBar( leadingType: FlowyAppBarLeadingType.close, + showDivider: false, actions: [ AppBarMoreButton( onTap: (_) => _showCardActions(context), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart index d1322d6b903a0..ae1948ac16c26 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/database_field_list.dart @@ -22,15 +22,18 @@ class MobileDatabaseFieldList extends StatelessWidget { const MobileDatabaseFieldList({ super.key, required this.databaseController, + required this.canCreate, }); final DatabaseController databaseController; + final bool canCreate; @override Widget build(BuildContext context) { return _MobileDatabaseFieldListBody( databaseController: databaseController, viewId: context.read().state.view.id, + canCreate: canCreate, ); } } @@ -39,10 +42,12 @@ class _MobileDatabaseFieldListBody extends StatelessWidget { const _MobileDatabaseFieldListBody({ required this.databaseController, required this.viewId, + required this.canCreate, }); final DatabaseController databaseController; final String viewId; + final bool canCreate; @override Widget build(BuildContext context) { @@ -113,13 +118,15 @@ class _MobileDatabaseFieldListBody extends StatelessWidget { .add(DatabasePropertyEvent.moveField(from, to)); }, header: firstCell, - footer: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _divider(), - _NewDatabaseFieldTile(viewId: viewId), - ], - ), + footer: canCreate + ? Column( + mainAxisSize: MainAxisSize.min, + children: [ + _divider(), + _NewDatabaseFieldTile(viewId: viewId), + ], + ) + : null, itemCount: cells.length, itemBuilder: (context, index) => cells[index], ); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart index 2c1c7859bbd5e..9603f28803c10 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart @@ -240,6 +240,7 @@ class DatabaseViewSettingTile extends StatelessWidget { value: context.read(), child: MobileDatabaseFieldList( databaseController: databaseController, + canCreate: true, ), ); }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart index eb32bbe103c4e..043d5802e57f4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/tab_bar/mobile/mobile_tab_bar_header.dart @@ -1,7 +1,11 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart'; +import 'package:appflowy/mobile/presentation/database/view/database_view_list.dart'; import 'package:appflowy/plugins/base/emoji/emoji_text.dart'; import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart'; import 'package:appflowy/plugins/database/widgets/setting/mobile_database_controls.dart'; +import 'package:appflowy/workspace/application/view/view_bloc.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:collection/collection.dart'; @@ -24,10 +28,11 @@ class _MobileTabBarHeaderState extends State { return Padding( padding: const EdgeInsets.symmetric(vertical: 14), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Expanded(child: _DatabaseViewList()), - const HSpace(10), + HSpace(GridSize.leadingHeaderPadding), + const _DatabaseViewSelectorButton(), + const Spacer(), BlocBuilder( builder: (context, state) { final currentView = state.tabBars.firstWhereIndexedOrNull( @@ -51,131 +56,77 @@ class _MobileTabBarHeaderState extends State { } } -class _DatabaseViewList extends StatelessWidget { - const _DatabaseViewList(); +class _DatabaseViewSelectorButton extends StatelessWidget { + const _DatabaseViewSelectorButton(); @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final currentView = state.tabBars.firstWhereIndexedOrNull( + final tabBar = state.tabBars.firstWhereIndexedOrNull( (index, tabBar) => index == state.selectedIndex, ); - if (currentView == null) { + if (tabBar == null) { return const SizedBox.shrink(); } - final children = state.tabBars.mapIndexed((index, tabBar) { - return Padding( - padding: EdgeInsetsDirectional.only( - start: index == 0 ? 0 : 2, - end: 2, + return TextButton( + style: ButtonStyle( + padding: const MaterialStatePropertyAll( + EdgeInsets.fromLTRB(12, 8, 8, 8), ), - child: _DatabaseViewListItem( - tabBar: tabBar, - isSelected: currentView.viewId == tabBar.viewId, + maximumSize: const MaterialStatePropertyAll(Size(200, 48)), + minimumSize: const MaterialStatePropertyAll(Size(48, 0)), + shape: const MaterialStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(12)), + ), ), - ); - }).toList(); - - children.insert(0, HSpace(GridSize.leadingHeaderPadding)); - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row(children: children), - ); - }, - ); - } -} - -class _DatabaseViewListItem extends StatefulWidget { - const _DatabaseViewListItem({ - required this.tabBar, - required this.isSelected, - }); - - final DatabaseTabBar tabBar; - final bool isSelected; - - @override - State<_DatabaseViewListItem> createState() => _DatabaseViewListItemState(); -} - -class _DatabaseViewListItemState extends State<_DatabaseViewListItem> { - late final MaterialStatesController statesController; - - @override - void initState() { - super.initState(); - statesController = MaterialStatesController( - {if (widget.isSelected) MaterialState.selected}, - ); - } - - @override - void didUpdateWidget(covariant oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.isSelected != oldWidget.isSelected) { - statesController.update(MaterialState.selected, widget.isSelected); - } - } - - @override - Widget build(BuildContext context) { - return TextButton( - statesController: statesController, - style: ButtonStyle( - padding: const MaterialStatePropertyAll( - EdgeInsets.symmetric(horizontal: 12, vertical: 7), - ), - maximumSize: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return const Size(150, 48); - } - return const Size(120, 48); - }), - minimumSize: const MaterialStatePropertyAll(Size(48, 0)), - shape: const MaterialStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(12)), - ), - ), - backgroundColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return const Color(0x0F212729); - } - return Colors.transparent; - }), - overlayColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.transparent; - } - return Theme.of(context).colorScheme.secondary; - }), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - _buildViewIconButton(context, widget.tabBar.view), - const HSpace(6), - Flexible( - child: FlowyText( - widget.tabBar.view.name, - fontSize: 14, - fontWeight: widget.isSelected ? FontWeight.w500 : FontWeight.w400, - overflow: TextOverflow.ellipsis, + backgroundColor: const MaterialStatePropertyAll(Color(0x0F212729)), + overlayColor: MaterialStatePropertyAll( + Theme.of(context).colorScheme.secondary, ), ), - ], - ), - onPressed: () { - if (!widget.isSelected) { - context - .read() - .add(DatabaseTabBarEvent.selectView(widget.tabBar.viewId)); - } + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildViewIconButton(context, tabBar.view), + const HSpace(6), + Flexible( + child: FlowyText.medium( + tabBar.view.name, + fontSize: 13, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(8), + const FlowySvg( + FlowySvgs.arrow_tight_s, + size: Size.square(10), + ), + ], + ), + onPressed: () { + showMobileBottomSheet( + context, + showDivider: false, + builder: (_) { + return MultiBlocProvider( + providers: [ + BlocProvider.value( + value: context.read(), + ), + BlocProvider.value( + value: context.read(), + ), + ], + child: const MobileDatabaseViewList(), + ); + }, + ); + }, + ); }, ); } 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 37f3816d03396..e7207c7f534d6 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 @@ -1,10 +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/mobile/presentation/database/view/database_view_list.dart'; -import 'package:appflowy/mobile/presentation/database/view/edit_database_view_screen.dart'; +import 'package:appflowy/mobile/presentation/database/view/database_field_list.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/grid/application/filter/filter_menu_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/sort/sort_menu_bloc.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; @@ -58,62 +56,26 @@ class MobileDatabaseControls extends StatelessWidget { return const SizedBox.shrink(); } - return Row( - children: [ - _DatabaseControlButton( - icon: FlowySvgs.settings_s, - onTap: () { - showMobileBottomSheet( - context, - showHeader: true, - showDoneButton: true, - title: LocaleKeys.grid_settings_editView.tr(), - enableDraggableScrollable: true, - initialChildSize: 0.98, - minChildSize: 0.98, - maxChildSize: 0.98, - builder: (_) { - return BlocProvider( - create: (_) { - return ViewBloc( - view: context - .read() - .state - .tabBarControllerByViewId[controller.viewId]! - .view, - )..add(const ViewEvent.initial()); - }, - child: MobileEditDatabaseViewScreen( - databaseController: controller, - ), - ); - }, - ); - }, - ), - _DatabaseControlButton( - icon: FlowySvgs.align_left_s, - onTap: () { - showMobileBottomSheet( - context, - showDivider: false, - builder: (_) { - return MultiBlocProvider( - providers: [ - BlocProvider.value( - value: context.read(), - ), - BlocProvider.value( - value: context.read(), - ), - ], - child: const MobileDatabaseViewList(), - ); - }, - ); - }, - ), - ], + return _DatabaseControlButton( + icon: FlowySvgs.m_field_hide_s, + onTap: () => showMobileBottomSheet( + context, + resizeToAvoidBottomInset: false, + showDragHandle: true, + showHeader: true, + showBackButton: true, + title: LocaleKeys.grid_settings_properties.tr(), + showDivider: true, + builder: (_) { + return BlocProvider.value( + value: context.read(), + child: MobileDatabaseFieldList( + databaseController: controller, + canCreate: false, + ), + ); + }, + ), ); }, ), diff --git a/frontend/resources/flowy_icons/16x/arrow_tight.svg b/frontend/resources/flowy_icons/16x/arrow_tight.svg new file mode 100644 index 0000000000000..5c4996a65d52b --- /dev/null +++ b/frontend/resources/flowy_icons/16x/arrow_tight.svg @@ -0,0 +1,5 @@ + + + + + From d117cd6b5bb03ef39f26abee8c8fdfc5a2d898a2 Mon Sep 17 00:00:00 2001 From: Yijing Huang Date: Sat, 3 Feb 2024 10:56:50 -0600 Subject: [PATCH 06/50] chore: delete placeholder code and clean code (#4503) --- .../details_placeholder_page.dart | 99 ------------------- .../home/mobile_home_trash_page.dart | 19 ++-- .../lib/mobile/presentation/presentation.dart | 1 - .../setting/notifications_setting_group.dart | 2 +- .../mobile_workspace_start_screen.dart | 6 +- .../appearance/mobile_appearance.dart | 1 - 6 files changed, 14 insertions(+), 114 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart deleted file mode 100644 index cef64bc19cb08..0000000000000 --- a/frontend/appflowy_flutter/lib/mobile/presentation/details_placeholder_page.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; - -// TODO(yijing): delete this after implementing the real screen inside bottom navigation bar. -/// For demonstration purposes -class DetailsPlaceholderScreen extends StatefulWidget { - /// Constructs a [DetailsScreen]. - const DetailsPlaceholderScreen({ - required this.label, - this.param, - this.extra, - this.withScaffold = true, - super.key, - }); - - /// The label to display in the center of the screen. - final String label; - - /// Optional param - final String? param; - - /// Optional extra object - final Object? extra; - - /// Wrap in scaffold - final bool withScaffold; - - @override - State createState() => DetailsPlaceholderScreenState(); -} - -/// The state for DetailsScreen -class DetailsPlaceholderScreenState extends State { - int _counter = 0; - - @override - Widget build(BuildContext context) { - if (widget.withScaffold) { - return Scaffold( - appBar: AppBar( - title: Text('Details Screen - ${widget.label}'), - ), - body: _build(context), - ); - } else { - return Container( - color: Theme.of(context).scaffoldBackgroundColor, - child: _build(context), - ); - } - } - - Widget _build(BuildContext context) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Details for ${widget.label} - Counter: $_counter', - style: Theme.of(context).textTheme.titleLarge, - ), - const Padding(padding: EdgeInsets.all(4)), - TextButton( - onPressed: () { - setState(() { - _counter++; - }); - }, - child: const Text('Increment counter'), - ), - const Padding(padding: EdgeInsets.all(8)), - if (widget.param != null) - Text( - 'Parameter: ${widget.param!}', - style: Theme.of(context).textTheme.titleMedium, - ), - const Padding(padding: EdgeInsets.all(8)), - if (widget.extra != null) - Text( - 'Extra: ${widget.extra!}', - style: Theme.of(context).textTheme.titleMedium, - ), - if (!widget.withScaffold) ...[ - const Padding(padding: EdgeInsets.all(16)), - TextButton( - onPressed: () { - GoRouter.of(context).pop(); - }, - child: const Text( - '< Back', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18), - ), - ), - ], - ], - ), - ); - } -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart index 7df84cd04fa19..a9c2f1b9331fb 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart @@ -164,23 +164,23 @@ class _DeletedFilesListView extends StatelessWidget { padding: const EdgeInsets.symmetric(vertical: 8), child: ListView.builder( itemBuilder: (context, index) { - final object = state.objects[index]; + final deletedFile = state.objects[index]; + return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), child: ListTile( - // TODO(Yijing): implement file type after TrashPB has file type + // TODO: show different file type icon, implement this feature after TrashPB has file type field leading: FlowySvg( FlowySvgs.document_s, size: const Size.square(24), color: theme.colorScheme.onSurface, ), title: Text( - object.name, + deletedFile.name, style: theme.textTheme.labelMedium ?.copyWith(color: theme.colorScheme.onBackground), ), horizontalTitleGap: 0, - // TODO(yiing): needs improve by container/surface theme color tileColor: theme.colorScheme.onSurface.withOpacity(0.1), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -188,7 +188,6 @@ class _DeletedFilesListView extends StatelessWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ - // TODO(yijing): extract icon button IconButton( splashRadius: 20, icon: FlowySvg( @@ -199,10 +198,10 @@ class _DeletedFilesListView extends StatelessWidget { onPressed: () { context .read() - .add(TrashEvent.putback(object.id)); + .add(TrashEvent.putback(deletedFile.id)); Fluttertoast.showToast( msg: - '${object.name} ${LocaleKeys.trash_mobile_isRestored.tr()}', + '${deletedFile.name} ${LocaleKeys.trash_mobile_isRestored.tr()}', gravity: ToastGravity.BOTTOM, ); }, @@ -215,10 +214,12 @@ class _DeletedFilesListView extends StatelessWidget { color: theme.colorScheme.onSurface, ), onPressed: () { - context.read().add(TrashEvent.delete(object)); + context + .read() + .add(TrashEvent.delete(deletedFile)); Fluttertoast.showToast( msg: - '${object.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}', + '${deletedFile.name} ${LocaleKeys.trash_mobile_isDeleted.tr()}', gravity: ToastGravity.BOTTOM, ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart b/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart index 909b4dbdd85a5..69a3ae503c9d0 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/presentation.dart @@ -1,4 +1,3 @@ -export 'details_placeholder_page.dart'; export 'editor/mobile_editor_screen.dart'; export 'home/home.dart'; export 'mobile_bottom_navigation_bar.dart'; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart index f0892a5f50b77..8813afc3b24df 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/notifications_setting_group.dart @@ -15,7 +15,7 @@ class NotificationsSettingGroup extends StatefulWidget { } class _NotificationsSettingGroupState extends State { - // TODO(yijing):remove this after notification page is implemented + // TODO:remove this after notification page is implemented bool isPushNotificationOn = false; @override diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart index 9a3ec4f5ba5f1..6c95d1076cf63 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart @@ -8,7 +8,7 @@ import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -// TODO(yijing): needs refactor when multiple workspaces are supported +// TODO: needs refactor when multiple workspaces are supported class MobileWorkspaceStartScreen extends StatefulWidget { const MobileWorkspaceStartScreen({ super.key, @@ -64,7 +64,7 @@ class _MobileWorkspaceStartScreenState const VSpace(spacing * 4), DropdownMenu( width: size.width - 100, - // TODO(yijing): The following code cause the bad state error, need to fix it + // TODO: The following code cause the bad state error, need to fix it // initialSelection: widget.workspaceState.workspaces.first, label: const Text('Workspace'), controller: controller, @@ -76,7 +76,7 @@ class _MobileWorkspaceStartScreenState }, ), const Spacer(), - // TODO(yijing): needs to implement create workspace in the future + // TODO: needs to implement create workspace in the future // TextButton( // child: Text( // LocaleKeys.workspace_create.tr(), 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 c3863d5721bc4..8900ae2256667 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 @@ -64,7 +64,6 @@ class MobileAppearance extends BaseAppearance { brightness: brightness, primary: _primaryColor, onPrimary: Colors.black, - // TODO(yijing): add color later secondary: const Color(0xff2d2d2d), //temp onSecondary: Colors.white, tertiary: const Color(0xff858585), // temp From 250f29f3254e0abc6c6625af2e233ad527a3f9a8 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sun, 4 Feb 2024 05:49:28 +0800 Subject: [PATCH 07/50] chore: clean up sort and filter code (#4585) * refactor: port away from extra SortType struct * refactor: add validator to flowy_database and clean up unused structs * refactor: port away from extra FilterType struct * chore: analysis options * fix: clippy and dart/ts compile * fix: tauri build --------- Co-authored-by: nathan --- .../application/sort/sort_service.dart | 4 +- .../application/database/sort/sort_service.ts | 2 - frontend/rust-lib/Cargo.lock | 18 +- .../src/entities/filter_entities/util.rs | 49 +- .../src/entities/group_entities/group.rs | 6 +- .../src/entities/setting_entities.rs | 68 +- .../src/entities/sort_entities.rs | 96 +-- .../flowy-database2/src/event_handler.rs | 13 +- .../src/services/database/database_editor.rs | 8 +- .../src/services/database_view/view_editor.rs | 67 +- .../src/services/filter/controller.rs | 17 +- .../src/services/filter/entities.rs | 69 +- .../src/services/sort/controller.rs | 26 +- .../src/services/sort/entities.rs | 48 +- .../tests/database/filter_test/script.rs | 686 +++++++++++------- .../database/filter_test/text_filter_test.rs | 8 +- .../tests/database/sort_test/script.rs | 14 +- .../database/sort_test/single_sort_test.rs | 1 - 18 files changed, 537 insertions(+), 663 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart index 871bb9d86fb6a..e0b9e591e5439 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart @@ -81,10 +81,8 @@ class SortBackendService { required FieldType fieldType, }) { final deleteSortPayload = DeleteSortPayloadPB.create() - ..fieldId = fieldId ..sortId = sortId - ..viewId = viewId - ..fieldType = fieldType; + ..viewId = viewId; final payload = DatabaseSettingChangesetPB.create() ..viewId = viewId diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts index be444a545e072..b7d32448ac934 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_service.ts @@ -57,8 +57,6 @@ export async function deleteSort(viewId: string, sort: Sort): Promise { delete_sort: { view_id: viewId, sort_id: sort.id, - field_id: sort.fieldId, - field_type: sort.fieldType, }, }); diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 544a65839ebb1..95e07c80c1aa5 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1102,7 +1102,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf 0.11.2", + "phf 0.8.0", "smallvec", ] @@ -3588,7 +3588,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros 0.8.0", + "phf_macros", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3608,7 +3608,6 @@ 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", ] @@ -3676,19 +3675,6 @@ 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.47", -] - [[package]] name = "phf_shared" version = "0.8.0" 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 9426fb0620aea..d12706b81f0a2 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 @@ -6,6 +6,7 @@ use collab_database::fields::Field; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; +use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::{ @@ -13,7 +14,7 @@ use crate::entities::{ NumberFilterPB, SelectOptionFilterPB, TextFilterPB, }; use crate::services::field::SelectOptionIds; -use crate::services::filter::{Filter, FilterType}; +use crate::services::filter::Filter; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct FilterPB { @@ -73,60 +74,28 @@ impl std::convert::From> for RepeatedFilterPB { } } -#[derive(ProtoBuf, Debug, Default, Clone)] +#[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, #[pb(index = 2)] pub field_type: FieldType, #[pb(index = 3)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub filter_id: String, #[pb(index = 4)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, } -impl TryInto for DeleteFilterPayloadPB { - 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 = NotEmptyStr::parse(self.filter_id) - .map_err(|_| ErrorCode::UnexpectedEmpty)? - .0; - - let filter_type = FilterType { - filter_id: filter_id.clone(), - field_id, - field_type: self.field_type, - }; - - Ok(DeleteFilterParams { - view_id, - filter_id, - filter_type, - }) - } -} - -#[derive(Debug)] -pub struct DeleteFilterParams { - pub view_id: String, - pub filter_id: String, - pub filter_type: FilterType, -} - -#[derive(ProtoBuf, Debug, Default, Clone)] +#[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateFilterPayloadPB { #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub field_id: String, #[pb(index = 2)] @@ -134,12 +103,14 @@ pub struct UpdateFilterPayloadPB { /// Create a new filter if the filter_id is None #[pb(index = 3, one_of)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub filter_id: Option, #[pb(index = 4)] pub data: Vec, #[pb(index = 5)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, } 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 d1fe052697a2b..05cc0c272395d 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 @@ -2,6 +2,7 @@ use std::convert::TryInto; use flowy_derive::ProtoBuf; use flowy_error::ErrorCode; +use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::RowMetaPB; @@ -130,15 +131,18 @@ pub struct GroupByFieldParams { pub view_id: String, } -#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateGroupPB { #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub group_id: String, #[pb(index = 3)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub field_id: String, #[pb(index = 4, one_of)] 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 7bd4284ecbfa3..1305986ffbab2 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -5,12 +5,12 @@ use strum_macros::EnumIter; use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; use flowy_error::ErrorCode; +use validator::Validate; use crate::entities::parser::NotEmptyStr; use crate::entities::{ - CalendarLayoutSettingPB, DeleteFilterParams, DeleteFilterPayloadPB, DeleteSortParams, - DeleteSortPayloadPB, RepeatedFieldSettingsPB, RepeatedFilterPB, RepeatedGroupSettingPB, - RepeatedSortPB, UpdateFilterParams, UpdateFilterPayloadPB, UpdateGroupPB, UpdateSortParams, + CalendarLayoutSettingPB, DeleteFilterPayloadPB, DeleteSortPayloadPB, RepeatedFieldSettingsPB, + RepeatedFilterPB, RepeatedGroupSettingPB, RepeatedSortPB, UpdateFilterPayloadPB, UpdateGroupPB, UpdateSortPayloadPB, }; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; @@ -69,84 +69,36 @@ impl std::convert::From for DatabaseLayout { } } -#[derive(Default, ProtoBuf)] +#[derive(Default, Validate, ProtoBuf)] pub struct DatabaseSettingChangesetPB { #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2, one_of)] pub layout_type: Option, #[pb(index = 3, one_of)] + #[validate] pub update_filter: Option, #[pb(index = 4, one_of)] + #[validate] pub delete_filter: Option, #[pb(index = 5, one_of)] + #[validate] pub update_group: Option, #[pb(index = 6, one_of)] + #[validate] pub update_sort: Option, #[pb(index = 7, one_of)] + #[validate] pub delete_sort: Option, } -impl TryInto for DatabaseSettingChangesetPB { - type Error = ErrorCode; - - fn try_into(self) -> Result { - let view_id = NotEmptyStr::parse(self.view_id) - .map_err(|_| ErrorCode::ViewIdIsInvalid)? - .0; - - let insert_filter = match self.update_filter { - None => None, - Some(payload) => Some(payload.try_into()?), - }; - - let delete_filter = match self.delete_filter { - None => None, - Some(payload) => Some(payload.try_into()?), - }; - - let alert_sort = match self.update_sort { - None => None, - Some(payload) => Some(payload.try_into()?), - }; - - let delete_sort = match self.delete_sort { - None => None, - Some(payload) => Some(payload.try_into()?), - }; - - Ok(DatabaseSettingChangesetParams { - view_id, - layout_type: self.layout_type.map(|ty| ty.into()), - insert_filter, - delete_filter, - alert_sort, - delete_sort, - }) - } -} - -pub struct DatabaseSettingChangesetParams { - pub view_id: String, - pub layout_type: Option, - pub insert_filter: Option, - pub delete_filter: Option, - pub alert_sort: Option, - pub delete_sort: Option, -} - -impl DatabaseSettingChangesetParams { - pub fn is_filter_changed(&self) -> bool { - self.insert_filter.is_some() || self.delete_filter.is_some() - } -} - #[derive(Debug, Eq, PartialEq, Default, ProtoBuf, Clone)] pub struct DatabaseLayoutSettingPB { #[pb(index = 1)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs index e177b83209cfe..4814a8979207b 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs @@ -1,9 +1,8 @@ use flowy_derive::{ProtoBuf, ProtoBuf_Enum}; -use flowy_error::ErrorCode; +use validator::Validate; -use crate::entities::parser::NotEmptyStr; use crate::entities::FieldType; -use crate::services::sort::{Sort, SortCondition, SortType}; +use crate::services::sort::{Sort, SortCondition}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct SortPB { @@ -91,12 +90,14 @@ impl std::convert::From for SortCondition { } } -#[derive(ProtoBuf, Debug, Default, Clone)] +#[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct UpdateSortPayloadPB { #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub field_id: String, #[pb(index = 3)] @@ -110,95 +111,14 @@ pub struct UpdateSortPayloadPB { pub condition: SortConditionPB, } -impl TryInto for UpdateSortPayloadPB { - 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 sort_id = match self.sort_id { - None => None, - Some(sort_id) => Some( - NotEmptyStr::parse(sort_id) - .map_err(|_| ErrorCode::SortIdIsEmpty)? - .0, - ), - }; - - Ok(UpdateSortParams { - view_id, - field_id, - sort_id, - field_type: self.field_type, - condition: self.condition.into(), - }) - } -} - -#[derive(Debug)] -pub struct UpdateSortParams { - pub view_id: String, - pub field_id: String, - /// Create a new sort if the sort is None - pub sort_id: Option, - pub field_type: FieldType, - pub condition: SortCondition, -} - -#[derive(ProtoBuf, Debug, Default, Clone)] +#[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct DeleteSortPayloadPB { #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub view_id: String, #[pb(index = 2)] - pub field_id: String, - - #[pb(index = 3)] - pub field_type: FieldType, - - #[pb(index = 4)] - pub sort_id: String, -} - -impl TryInto for DeleteSortPayloadPB { - 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 sort_id = NotEmptyStr::parse(self.sort_id) - .map_err(|_| ErrorCode::UnexpectedEmpty)? - .0; - - let sort_type = SortType { - sort_id: sort_id.clone(), - field_id, - field_type: self.field_type, - }; - - Ok(DeleteSortParams { - view_id, - sort_type, - sort_id, - }) - } -} - -#[derive(Debug, Clone)] -pub struct DeleteSortParams { - pub view_id: String, - pub sort_type: SortType, + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] pub sort_id: String, } diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 5ce6d255d2d19..7c25519d5276d 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -87,27 +87,30 @@ pub(crate) async fn update_database_setting_handler( manager: AFPluginState>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; - let params: DatabaseSettingChangesetParams = data.into_inner().try_into()?; + let params = data.try_into_inner()?; let editor = manager.get_database_with_view_id(¶ms.view_id).await?; - if let Some(update_filter) = params.insert_filter { - editor.create_or_update_filter(update_filter).await?; + if let Some(update_filter) = params.update_filter { + editor + .create_or_update_filter(update_filter.try_into()?) + .await?; } if let Some(delete_filter) = params.delete_filter { editor.delete_filter(delete_filter).await?; } - if let Some(update_sort) = params.alert_sort { + if let Some(update_sort) = params.update_sort { let _ = editor.create_or_update_sort(update_sort).await?; } + if let Some(delete_sort) = params.delete_sort { editor.delete_sort(delete_sort).await?; } if let Some(layout_type) = params.layout_type { editor - .update_view_layout(¶ms.view_id, layout_type) + .update_view_layout(¶ms.view_id, layout_type.into()) .await?; } Ok(()) 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 bde5bc2cda840..0b42c4e2d460e 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 @@ -208,19 +208,19 @@ impl DatabaseEditor { Ok(()) } - pub async fn delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { + 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?; Ok(()) } - pub async fn create_or_update_sort(&self, params: UpdateSortParams) -> FlowyResult { + pub async fn create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult { let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; - let sort = view_editor.v_insert_sort(params).await?; + let sort = view_editor.insert_or_update_sort(params).await?; Ok(sort) } - pub async fn delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> { + pub async fn delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; view_editor.v_delete_sort(params).await?; Ok(()) 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 4056255026574..35ce86767445c 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 @@ -15,11 +15,11 @@ use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::af_spawn; use crate::entities::{ - CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterParams, - DeleteSortParams, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, + CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterPayloadPB, + DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, - UpdateFilterParams, UpdateSortParams, + UpdateFilterParams, UpdateSortPayloadPB, }; use crate::notification::{send_notification, DatabaseNotification}; use crate::services::calculations::{Calculation, CalculationChangeset, CalculationsController}; @@ -38,11 +38,11 @@ use crate::services::database_view::{ }; use crate::services::field_settings::FieldSettings; use crate::services::filter::{ - Filter, FilterChangeset, FilterController, FilterType, UpdatedFilterType, + Filter, FilterChangeset, FilterContext, FilterController, UpdatedFilter, }; use crate::services::group::{GroupChangesets, GroupController, MoveGroupRowContext, RowChangeset}; use crate::services::setting::CalendarLayoutSetting; -use crate::services::sort::{DeletedSortType, Sort, SortChangeset, SortController, SortType}; +use crate::services::sort::{Sort, SortChangeset, SortController}; use super::notify_did_update_calculation; use super::view_calculations::make_calculations_controller; @@ -499,7 +499,7 @@ impl DatabaseViewEditor { } #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn v_insert_sort(&self, params: UpdateSortParams) -> FlowyResult { + pub async fn insert_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult { let is_exist = params.sort_id.is_some(); let sort_id = match params.sort_id { None => gen_database_sort_id(), @@ -510,18 +510,18 @@ impl DatabaseViewEditor { id: sort_id, field_id: params.field_id.clone(), field_type: params.field_type, - condition: params.condition, + condition: params.condition.into(), }; - let sort_type = SortType::from(&sort); + let mut sort_controller = self.sort_controller.write().await; self.delegate.insert_sort(&self.view_id, sort.clone()); let changeset = if is_exist { sort_controller - .did_receive_changes(SortChangeset::from_update(sort_type)) + .apply_changeset(SortChangeset::from_update(sort.clone())) .await } else { sort_controller - .did_receive_changes(SortChangeset::from_insert(sort_type)) + .apply_changeset(SortChangeset::from_insert(sort.clone())) .await }; drop(sort_controller); @@ -529,18 +529,17 @@ impl DatabaseViewEditor { Ok(sort) } - pub async fn v_delete_sort(&self, params: DeleteSortParams) -> FlowyResult<()> { + pub async fn v_delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { let notification = self .sort_controller .write() .await - .did_receive_changes(SortChangeset::from_delete(DeletedSortType::from( - params.clone(), - ))) + .apply_changeset(SortChangeset::from_delete(params.sort_id.clone())) .await; self.delegate.remove_sort(&self.view_id, ¶ms.sort_id); notify_did_update_sort(notification).await; + Ok(()) } @@ -635,25 +634,20 @@ impl DatabaseViewEditor { condition: params.condition, content: params.content, }; - let filter_type = FilterType::from(&filter); let filter_controller = self.filter_controller.clone(); let changeset = if is_exist { - let old_filter_type = self - .delegate - .get_filter(&self.view_id, &filter.id) - .map(|field| FilterType::from(&field)); + let old_filter = self.delegate.get_filter(&self.view_id, &filter.id); - self.delegate.insert_filter(&self.view_id, filter); + self.delegate.insert_filter(&self.view_id, filter.clone()); filter_controller - .did_receive_changes(FilterChangeset::from_update(UpdatedFilterType::new( - old_filter_type, - filter_type, + .did_receive_changes(FilterChangeset::from_update(UpdatedFilter::new( + old_filter, filter, ))) .await } else { - self.delegate.insert_filter(&self.view_id, filter); + self.delegate.insert_filter(&self.view_id, filter.clone()); filter_controller - .did_receive_changes(FilterChangeset::from_insert(filter_type)) + .did_receive_changes(FilterChangeset::from_insert(filter)) .await }; drop(filter_controller); @@ -665,16 +659,20 @@ impl DatabaseViewEditor { } #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn v_delete_filter(&self, params: DeleteFilterParams) -> FlowyResult<()> { - let filter_type = params.filter_type; + 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_type.clone())) + .did_receive_changes(FilterChangeset::from_delete(filter_context.clone())) .await; self .delegate - .delete_filter(&self.view_id, &filter_type.filter_id); + .delete_filter(&self.view_id, ¶ms.filter_id); if changeset.is_some() { notify_did_update_filter(changeset.unwrap()).await; } @@ -799,11 +797,12 @@ impl DatabaseViewEditor { .delegate .get_filter_by_field_id(&self.view_id, field_id) { - let mut old = FilterType::from(&filter); - old.field_type = FieldType::from(old_field.field_type); - let new = FilterType::from(&filter); - let filter_type = UpdatedFilterType::new(Some(old), new); - let filter_changeset = FilterChangeset::from_update(filter_type); + let old = Filter { + field_type: FieldType::from(old_field.field_type), + ..filter.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 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 50aa55b18aae7..983dfef1a887c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/controller.rs @@ -237,7 +237,7 @@ impl FilterController { let mut notification: Option = None; if let Some(filter_type) = &changeset.insert_filter { - if let Some(filter) = self.filter_from_filter_id(&filter_type.filter_id).await { + if let Some(filter) = self.filter_from_filter_id(&filter_type.id).await { notification = Some(FilterChangesetNotificationPB::from_insert( &self.view_id, vec![filter], @@ -245,7 +245,7 @@ impl FilterController { } if let Some(filter) = self .delegate - .get_filter(&self.view_id, &filter_type.filter_id) + .get_filter(&self.view_id, &filter_type.id) .await { self.refresh_filters(vec![filter]).await; @@ -255,9 +255,9 @@ impl FilterController { 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.filter_id) + .filter_from_filter_id(&updated_filter_type.new.id) .await; - let old_filter = self.filter_from_filter_id(&old_filter_type.filter_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); @@ -282,14 +282,17 @@ impl FilterController { } } - if let Some(filter_type) = &changeset.delete_filter { - if let Some(filter) = self.filter_from_filter_id(&filter_type.filter_id).await { + 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_type.field_id); + self + .cell_filter_cache + .write() + .remove(&filter_context.field_id); } self 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 f1a5dcd44ad59..27e220b0c277d 100644 --- a/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/filter/entities.rs @@ -3,7 +3,7 @@ use collab::core::any_map::AnyMapExtension; use collab_database::rows::RowId; use collab_database::views::{FilterMap, FilterMapBuilder}; -use crate::entities::{DeleteFilterParams, FieldType, FilterPB, InsertedRowPB}; +use crate::entities::{FieldType, FilterPB, InsertedRowPB}; #[derive(Debug, Clone)] pub struct Filter { @@ -63,66 +63,56 @@ impl TryFrom for Filter { } #[derive(Debug)] pub struct FilterChangeset { - pub(crate) insert_filter: Option, - pub(crate) update_filter: Option, - pub(crate) delete_filter: Option, + pub(crate) insert_filter: Option, + pub(crate) update_filter: Option, + pub(crate) delete_filter: Option, } #[derive(Debug)] -pub struct UpdatedFilterType { - pub old: Option, - pub new: FilterType, +pub struct UpdatedFilter { + pub old: Option, + pub new: Filter, } -impl UpdatedFilterType { - pub fn new(old: Option, new: FilterType) -> UpdatedFilterType { +impl UpdatedFilter { + pub fn new(old: Option, new: Filter) -> UpdatedFilter { Self { old, new } } } impl FilterChangeset { - pub fn from_insert(filter_type: FilterType) -> Self { + pub fn from_insert(filter: Filter) -> Self { Self { - insert_filter: Some(filter_type), + insert_filter: Some(filter), update_filter: None, delete_filter: None, } } - pub fn from_update(filter_type: UpdatedFilterType) -> Self { + pub fn from_update(filter: UpdatedFilter) -> Self { Self { insert_filter: None, - update_filter: Some(filter_type), + update_filter: Some(filter), delete_filter: None, } } - pub fn from_delete(filter_type: FilterType) -> Self { + pub fn from_delete(filter_context: FilterContext) -> Self { Self { insert_filter: None, update_filter: None, - delete_filter: Some(filter_type), + delete_filter: Some(filter_context), } } } -#[derive(Hash, Eq, PartialEq, Debug, Clone)] -pub struct FilterType { +#[derive(Debug, Clone)] +pub struct FilterContext { pub filter_id: String, pub field_id: String, pub field_type: FieldType, } -impl std::convert::From<&Filter> for FilterType { - fn from(filter: &Filter) -> Self { - Self { - filter_id: filter.id.clone(), - field_id: filter.field_id.clone(), - field_type: filter.field_type, - } - } -} - -impl std::convert::From<&FilterPB> for FilterType { +impl From<&FilterPB> for FilterContext { fn from(filter: &FilterPB) -> Self { Self { filter_id: filter.id.clone(), @@ -132,29 +122,6 @@ impl std::convert::From<&FilterPB> for FilterType { } } -// #[derive(Hash, Eq, PartialEq, Debug, Clone)] -// pub struct InsertedFilterType { -// pub field_id: String, -// pub filter_id: Option, -// pub field_type: FieldType, -// } -// -// impl std::convert::From<&Filter> for InsertedFilterType { -// fn from(params: &Filter) -> Self { -// Self { -// field_id: params.field_id.clone(), -// filter_id: Some(params.id.clone()), -// field_type: params.field_type.clone(), -// } -// } -// } - -impl std::convert::From<&DeleteFilterParams> for FilterType { - fn from(params: &DeleteFilterParams) -> Self { - params.filter_type.clone() - } -} - #[derive(Clone, Debug)] pub struct FilterResultNotification { pub view_id: String, 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 31e10d084b5e6..88f2440b46ce2 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -179,39 +179,25 @@ impl SortController { } #[tracing::instrument(level = "trace", skip(self))] - pub async fn did_receive_changes( - &mut self, - changeset: SortChangeset, - ) -> SortChangesetNotificationPB { + pub async fn apply_changeset(&mut self, changeset: SortChangeset) -> SortChangesetNotificationPB { let mut notification = SortChangesetNotificationPB::new(self.view_id.clone()); + if let Some(insert_sort) = changeset.insert_sort { - if let Some(sort) = self - .delegate - .get_sort(&self.view_id, &insert_sort.sort_id) - .await - { + if let Some(sort) = self.delegate.get_sort(&self.view_id, &insert_sort.id).await { notification.insert_sorts.push(sort.as_ref().into()); self.sorts.push(sort); } } - if let Some(delete_sort_type) = changeset.delete_sort { - if let Some(index) = self - .sorts - .iter() - .position(|sort| sort.id == delete_sort_type.sort_id) - { + if let Some(sort_id) = changeset.delete_sort { + if let Some(index) = self.sorts.iter().position(|sort| sort.id == sort_id) { let sort = self.sorts.remove(index); notification.delete_sorts.push(sort.as_ref().into()); } } if let Some(update_sort) = changeset.update_sort { - if let Some(updated_sort) = self - .delegate - .get_sort(&self.view_id, &update_sort.sort_id) - .await - { + if let Some(updated_sort) = self.delegate.get_sort(&self.view_id, &update_sort.id).await { notification.update_sorts.push(updated_sort.as_ref().into()); if let Some(index) = self .sorts 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 ff64bbf11bab9..fe262877d516c 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -5,7 +5,7 @@ use collab::core::any_map::AnyMapExtension; use collab_database::rows::RowId; use collab_database::views::{SortMap, SortMapBuilder}; -use crate::entities::{DeleteSortParams, FieldType}; +use crate::entities::FieldType; #[derive(Debug, Clone)] pub struct Sort { @@ -98,23 +98,6 @@ impl From for SortCondition { } } -#[derive(Hash, Eq, PartialEq, Debug, Clone)] -pub struct SortType { - pub sort_id: String, - pub field_id: String, - pub field_type: FieldType, -} - -impl From<&Sort> for SortType { - fn from(data: &Sort) -> Self { - Self { - sort_id: data.id.clone(), - field_id: data.field_id.clone(), - field_type: data.field_type, - } - } -} - #[derive(Clone)] pub struct ReorderAllRowsResult { pub view_id: String, @@ -140,13 +123,13 @@ pub struct ReorderSingleRowResult { #[derive(Debug)] pub struct SortChangeset { - pub(crate) insert_sort: Option, - pub(crate) update_sort: Option, - pub(crate) delete_sort: Option, + pub(crate) insert_sort: Option, + pub(crate) update_sort: Option, + pub(crate) delete_sort: Option, } impl SortChangeset { - pub fn from_insert(sort: SortType) -> Self { + pub fn from_insert(sort: Sort) -> Self { Self { insert_sort: Some(sort), update_sort: None, @@ -154,7 +137,7 @@ impl SortChangeset { } } - pub fn from_update(sort: SortType) -> Self { + pub fn from_update(sort: Sort) -> Self { Self { insert_sort: None, update_sort: Some(sort), @@ -162,26 +145,11 @@ impl SortChangeset { } } - pub fn from_delete(deleted_sort: DeletedSortType) -> Self { + pub fn from_delete(sort_id: String) -> Self { Self { insert_sort: None, update_sort: None, - delete_sort: Some(deleted_sort), - } - } -} - -#[derive(Debug)] -pub struct DeletedSortType { - pub sort_type: SortType, - pub sort_id: String, -} - -impl std::convert::From for DeletedSortType { - fn from(params: DeleteSortParams) -> Self { - Self { - sort_type: params.sort_type, - sort_id: params.sort_id, + delete_sort: Some(sort_id), } } } 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 423761d17652a..1518398719930 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 @@ -1,321 +1,445 @@ -#![cfg_attr(rustfmt, rustfmt::skip)] -#![allow(clippy::all)] #![allow(dead_code)] -#![allow(unused_imports)] use std::time::Duration; -use bytes::Bytes; -use collab_database::rows::{Row, RowId}; -use futures::TryFutureExt; +use collab_database::rows::RowId; +use flowy_database2::services::filter::FilterContext; use tokio::sync::broadcast::Receiver; -use flowy_database2::entities::{CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterParams, FieldType, FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, SelectOptionPB, TextFilterConditionPB, TextFilterPB, UpdateFilterParams, UpdateFilterPayloadPB}; +use flowy_database2::entities::{ + CheckboxFilterConditionPB, CheckboxFilterPB, ChecklistFilterConditionPB, ChecklistFilterPB, + DatabaseViewSettingPB, DateFilterConditionPB, DateFilterPB, DeleteFilterPayloadPB, FieldType, + FilterPB, NumberFilterConditionPB, NumberFilterPB, SelectOptionConditionPB, SelectOptionFilterPB, + TextFilterConditionPB, TextFilterPB, UpdateFilterParams, UpdateFilterPayloadPB, +}; use flowy_database2::services::database_view::DatabaseViewChanged; -use flowy_database2::services::field::SelectOption; -use flowy_database2::services::filter::FilterType; use lib_dispatch::prelude::af_spawn; use crate::database::database_editor::DatabaseEditorTest; pub struct FilterRowChanged { - pub(crate) showing_num_of_rows: usize, - pub(crate) hiding_num_of_rows: usize, + pub(crate) showing_num_of_rows: usize, + pub(crate) hiding_num_of_rows: usize, } pub enum FilterScript { - UpdateTextCell { - row_id: RowId, - text: String, - changed: Option, - }, - UpdateChecklistCell{ - row_id: RowId, - selected_option_ids: Vec, - }, - UpdateSingleSelectCell { - row_id: RowId, - option_id: String, - changed: Option, - }, - InsertFilter { - payload: UpdateFilterPayloadPB, - }, - CreateTextFilter { - condition: TextFilterConditionPB, - content: String, - changed: Option, - }, - UpdateTextFilter { - filter: FilterPB, - condition: TextFilterConditionPB, - content: String, - changed: Option, - }, - CreateNumberFilter { - condition: NumberFilterConditionPB, - content: String, - changed: Option, - }, - CreateCheckboxFilter { - condition: CheckboxFilterConditionPB, - 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_id: String, - filter_type: FilterType, - changed: Option, - }, - AssertFilterContent { - filter_id: String, - condition: i64, - content: String - }, - AssertNumberOfVisibleRows { - expected: usize, - }, - #[allow(dead_code)] - AssertGridSetting { - expected_setting: DatabaseViewSettingPB, - }, - Wait { millisecond: u64 } + UpdateTextCell { + row_id: RowId, + text: String, + changed: Option, + }, + UpdateChecklistCell { + row_id: RowId, + selected_option_ids: Vec, + }, + UpdateSingleSelectCell { + row_id: RowId, + option_id: String, + changed: Option, + }, + InsertFilter { + payload: UpdateFilterPayloadPB, + }, + CreateTextFilter { + condition: TextFilterConditionPB, + content: String, + changed: Option, + }, + UpdateTextFilter { + filter: FilterPB, + condition: TextFilterConditionPB, + content: String, + changed: Option, + }, + CreateNumberFilter { + condition: NumberFilterConditionPB, + content: String, + changed: Option, + }, + CreateCheckboxFilter { + condition: CheckboxFilterConditionPB, + 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, + changed: Option, + }, + AssertFilterContent { + filter_id: String, + condition: i64, + content: String, + }, + AssertNumberOfVisibleRows { + expected: usize, + }, + #[allow(dead_code)] + AssertGridSetting { + expected_setting: DatabaseViewSettingPB, + }, + Wait { + millisecond: u64, + }, } pub struct DatabaseFilterTest { - inner: DatabaseEditorTest, - recv: Option>, + inner: DatabaseEditorTest, + recv: Option>, } impl DatabaseFilterTest { - pub async fn new() -> Self { - let editor_test = DatabaseEditorTest::new_grid().await; - Self { - inner: editor_test, - recv: None, - } - } - - 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 run_scripts(&mut self, scripts: Vec) { - for script in scripts { - self.run_script(script).await; - } + pub async fn new() -> Self { + let editor_test = DatabaseEditorTest::new_grid().await; + Self { + inner: editor_test, + recv: None, } + } - pub async fn run_script(&mut self, script: FilterScript) { - match script { - FilterScript::UpdateTextCell { row_id, text, changed} => { - self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); - self.assert_future_changed(changed).await; - self.update_text_cell(row_id, &text).await.unwrap(); - } - FilterScript::UpdateChecklistCell { row_id, selected_option_ids } => { - self.set_checklist_cell( row_id, selected_option_ids).await.unwrap(); - } - FilterScript::UpdateSingleSelectCell { row_id, option_id, changed} => { - self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); - 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, 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::RichText); - let text_filter= TextFilterPB { - condition, - content - }; - let payload = - UpdateFilterPayloadPB::new( - & self.view_id(), - &field, text_filter); - self.insert_filter(payload).await; - } - FilterScript::UpdateTextFilter { filter, condition, 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.into(), - 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 - }; + pub fn view_id(&self) -> String { + self.view_id.clone() + } - 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 }; - let payload = - UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); - self.insert_filter(payload).await; - } - FilterScript::CreateSingleSelectFilter { condition, option_ids, 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::SingleSelect); - let filter = SelectOptionFilterPB { condition, option_ids }; - let payload = - UpdateFilterPayloadPB::new(& self.view_id(), &field, filter); - self.insert_filter(payload).await; - } - FilterScript::CreateChecklistFilter { 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::Checklist); - // let type_option = self.get_checklist_type_option(&field_rev.id); - let filter = ChecklistFilterPB { condition }; - let payload = - UpdateFilterPayloadPB::new(& self.view_id(), &field, filter); - self.insert_filter(payload).await; - } - 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); + pub async fn get_all_filters(&self) -> Vec { + self.editor.get_all_filters(&self.view_id).await.items + } - } - FilterScript::DeleteFilter { filter_id, filter_type ,changed} => { - self.recv = Some(self.editor.subscribe_view_changed(&self.view_id()).await.unwrap()); - self.assert_future_changed(changed).await; - let params = DeleteFilterParams { filter_id, view_id: self.view_id(),filter_type }; - let _ = self.editor.delete_filter(params).await.unwrap(); - } - FilterScript::AssertGridSetting { expected_setting } => { - let setting = self.editor.get_database_view_setting(&self.view_id).await.unwrap(); - assert_eq!(expected_setting, setting); - } - FilterScript::AssertNumberOfVisibleRows { expected } => { - let grid = self.editor.get_database_data(&self.view_id).await.unwrap(); - assert_eq!(grid.rows.len(), expected); - } - FilterScript::Wait { millisecond } => { - tokio::time::sleep(Duration::from_millis(millisecond)).await; - } - } + pub async fn run_scripts(&mut self, scripts: Vec) { + for script in scripts { + self.run_script(script).await; } + } - async fn assert_future_changed(&mut self, change: Option) { - if change.is_none() {return;} - let change = change.unwrap(); - let mut receiver = self.recv.take().unwrap(); - af_spawn(async move { - match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await { - Ok(changed) => { - match changed.unwrap() { DatabaseViewChanged::FilterNotification(notification) => { - assert_eq!(notification.visible_rows.len(), change.showing_num_of_rows, "visible rows not match"); - assert_eq!(notification.invisible_rows.len(), change.hiding_num_of_rows, "invisible rows not match"); - } - _ => {} - } - }, - Err(e) => { - panic!("Process filter task timeout: {:?}", e); - } - } - }); - + pub async fn run_script(&mut self, script: FilterScript) { + match script { + FilterScript::UpdateTextCell { + row_id, + text, + changed, + } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id()) + .await + .unwrap(), + ); + self.assert_future_changed(changed).await; + self.update_text_cell(row_id, &text).await.unwrap(); + }, + FilterScript::UpdateChecklistCell { + row_id, + selected_option_ids, + } => { + self + .set_checklist_cell(row_id, selected_option_ids) + .await + .unwrap(); + }, + FilterScript::UpdateSingleSelectCell { + row_id, + option_id, + changed, + } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id()) + .await + .unwrap(), + ); + 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, + 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::RichText); + let text_filter = TextFilterPB { condition, content }; + let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, text_filter); + self.insert_filter(payload).await; + }, + FilterScript::UpdateTextFilter { + filter, + condition, + 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, + }; + 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, + }; + let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); + self.insert_filter(payload).await; + }, + FilterScript::CreateSingleSelectFilter { + condition, + option_ids, + 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::SingleSelect); + let filter = SelectOptionFilterPB { + condition, + option_ids, + }; + let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); + self.insert_filter(payload).await; + }, + FilterScript::CreateChecklistFilter { 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::Checklist); + let filter = ChecklistFilterPB { condition }; + let payload = UpdateFilterPayloadPB::new(&self.view_id(), &field, filter); + self.insert_filter(payload).await; + }, + 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); + }, + FilterScript::DeleteFilter { + filter_context, + changed, + } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id()) + .await + .unwrap(), + ); + 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, + }; + self.editor.delete_filter(params).await.unwrap(); + }, + FilterScript::AssertGridSetting { expected_setting } => { + let setting = self + .editor + .get_database_view_setting(&self.view_id) + .await + .unwrap(); + assert_eq!(expected_setting, setting); + }, + FilterScript::AssertNumberOfVisibleRows { expected } => { + let grid = self.editor.get_database_data(&self.view_id).await.unwrap(); + assert_eq!(grid.rows.len(), expected); + }, + FilterScript::Wait { millisecond } => { + tokio::time::sleep(Duration::from_millis(millisecond)).await; + }, } + } - async fn insert_filter(&self, payload: UpdateFilterPayloadPB) { - let params: UpdateFilterParams = payload.try_into().unwrap(); - let _ = self.editor.create_or_update_filter(params).await.unwrap(); + async fn assert_future_changed(&mut self, change: Option) { + if change.is_none() { + return; } + let change = change.unwrap(); + let mut receiver = self.recv.take().unwrap(); + af_spawn(async move { + match tokio::time::timeout(Duration::from_secs(2), receiver.recv()).await { + Ok(changed) => { + if let DatabaseViewChanged::FilterNotification(notification) = changed.unwrap() { + assert_eq!( + notification.visible_rows.len(), + change.showing_num_of_rows, + "visible rows not match" + ); + assert_eq!( + notification.invisible_rows.len(), + change.hiding_num_of_rows, + "invisible rows not match" + ); + } + }, + Err(e) => { + panic!("Process filter task timeout: {:?}", e); + }, + } + }); + } + async fn insert_filter(&self, payload: UpdateFilterPayloadPB) { + let params: UpdateFilterParams = payload.try_into().unwrap(); + self.editor.create_or_update_filter(params).await.unwrap(); + } } - impl std::ops::Deref for DatabaseFilterTest { - type Target = DatabaseEditorTest; + type Target = DatabaseEditorTest; - fn deref(&self) -> &Self::Target { - &self.inner - } + fn deref(&self) -> &Self::Target { + &self.inner + } } impl std::ops::DerefMut for DatabaseFilterTest { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.inner - } + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } } 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 517def3f8bb47..3c4940d261561 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,7 @@ use flowy_database2::entities::{ FieldType, TextFilterConditionPB, TextFilterPB, UpdateFilterPayloadPB, }; -use flowy_database2::services::filter::FilterType; +use flowy_database2::services::filter::FilterContext; use crate::database::filter_test::script::FilterScript::*; use crate::database::filter_test::script::*; @@ -44,8 +44,7 @@ async fn grid_filter_text_is_not_empty_test() { test .run_scripts(vec![ DeleteFilter { - filter_id: filter.id.clone(), - filter_type: FilterType::from(&filter), + filter_context: FilterContext::from(&filter), changed: Some(FilterRowChanged { showing_num_of_rows: 1, hiding_num_of_rows: 0, @@ -208,8 +207,7 @@ async fn grid_filter_delete_test() { test .run_scripts(vec![ DeleteFilter { - filter_id: filter.id.clone(), - filter_type: FilterType::from(&filter), + filter_context: FilterContext::from(&filter), changed: None, }, AssertFilterCount { count: 0 }, 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 c2ce322d860d5..21d7f0557d42a 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 @@ -7,10 +7,10 @@ use collab_database::rows::RowId; use futures::stream::StreamExt; use tokio::sync::broadcast::Receiver; -use flowy_database2::entities::{DeleteSortParams, FieldType, UpdateSortParams}; +use flowy_database2::entities::{DeleteSortPayloadPB, FieldType, UpdateSortPayloadPB}; use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::database_view::DatabaseViewChanged; -use flowy_database2::services::sort::{Sort, SortCondition, SortType}; +use flowy_database2::services::sort::{Sort, SortCondition}; use crate::database::database_editor::DatabaseEditorTest; @@ -20,7 +20,6 @@ pub enum SortScript { condition: SortCondition, }, DeleteSort { - sort: Sort, sort_id: String, }, AssertCellContentOrder { @@ -71,17 +70,17 @@ impl DatabaseSortTest { .await .unwrap(), ); - let params = UpdateSortParams { + let params = UpdateSortPayloadPB { view_id: self.view_id.clone(), field_id: field.id.clone(), sort_id: None, field_type: FieldType::from(field.field_type), - condition, + condition: condition.into(), }; let sort_rev = self.editor.create_or_update_sort(params).await.unwrap(); self.current_sort_rev = Some(sort_rev); }, - SortScript::DeleteSort { sort, sort_id } => { + SortScript::DeleteSort { sort_id } => { self.recv = Some( self .editor @@ -89,9 +88,8 @@ impl DatabaseSortTest { .await .unwrap(), ); - let params = DeleteSortParams { + let params = DeleteSortPayloadPB { view_id: self.view_id.clone(), - sort_type: SortType::from(&sort), sort_id, }; self.editor.delete_sort(params).await.unwrap(); diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs index 209abb2e63162..a3eac1db8d391 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs @@ -93,7 +93,6 @@ async fn sort_text_by_ascending_and_delete_sort_test() { let sort = test.current_sort_rev.as_ref().unwrap(); let scripts = vec![ DeleteSort { - sort: sort.clone(), sort_id: sort.id.clone(), }, AssertCellContentOrder { From 08938b8c70cfdbdcf614cef4bd0349d3d0ce0214 Mon Sep 17 00:00:00 2001 From: Zack <33050391+speed2exe@users.noreply.github.com> Date: Sun, 4 Feb 2024 05:49:45 +0800 Subject: [PATCH 08/50] feat: cloud workspace api (#4469) * feat: workspace api * feat: added cloud apis for add and delete workspace * feat: add and delete workspace event handlers * chore: rust fmt * chore: save user workspace * test: add test * test: add test * chore: add to gitignore * feat: update api add name to workspace * chore: cargo clippy and rename to create * chore: add envrc and direnv to gitignore * chore: change name to create workspace instead of add workspace * chore: update client api rev * feat: add create workspace impl * chore: restore gitignore to original * test: fix create workspace event test * fix: change delete workspace input * fix: compile * fix: create workspace test * feat: add error code for request payload too large * chore: remove cargo backup files * feat: add is async option for upload file handler * chore: update client api version --------- Co-authored-by: nathan --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 1 + frontend/rust-lib/Cargo.lock | 1 + .../event-integration/src/document_event.rs | 7 +++ .../rust-lib/event-integration/src/lib.rs | 2 +- .../event-integration/src/user_event.rs | 48 +++++++++++++- .../tests/database/supabase_test/helper.rs | 2 +- .../tests/folder/local_test/folder_test.rs | 2 +- .../tests/folder/local_test/test.rs | 8 +-- .../tests/folder/supabase_test/helper.rs | 2 +- .../tests/user/af_cloud_test/mod.rs | 1 + .../user/af_cloud_test/workspace_test.rs | 52 +++++++++++++++ .../user/local_test/user_profile_test.rs | 9 +-- .../tests/user/supabase_test/auth_test.rs | 3 +- .../flowy-core/src/integrate/trait_impls.rs | 20 +++--- .../rust-lib/flowy-core/src/integrate/user.rs | 2 +- .../rust-lib/flowy-database-pub/src/cloud.rs | 8 +-- .../rust-lib/flowy-database2/src/manager.rs | 28 +++++---- .../src/services/database/database_editor.rs | 18 +++++- .../rust-lib/flowy-document/src/entities.rs | 3 + .../flowy-document/src/event_handler.rs | 5 +- .../rust-lib/flowy-document/src/manager.rs | 19 ++++-- frontend/rust-lib/flowy-error/src/code.rs | 3 + .../flowy-error/src/impl_from/cloud.rs | 1 + .../rust-lib/flowy-folder-pub/src/cloud.rs | 6 +- frontend/rust-lib/flowy-folder/src/manager.rs | 28 ++++----- frontend/rust-lib/flowy-server/Cargo.toml | 1 + .../src/af_cloud/impls/database.rs | 6 +- .../flowy-server/src/af_cloud/impls/folder.rs | 34 ++++++++-- .../af_cloud/impls/user/cloud_service_impl.rs | 28 ++++++++- .../src/local_server/impls/database.rs | 6 +- .../src/local_server/impls/folder.rs | 4 +- .../src/local_server/impls/user.rs | 18 ++++++ frontend/rust-lib/flowy-server/src/server.rs | 4 +- .../flowy-server/src/supabase/api/database.rs | 6 +- .../flowy-server/src/supabase/api/folder.rs | 4 +- .../flowy-server/src/supabase/api/user.rs | 18 ++++++ .../tests/supabase_test/database_test.rs | 2 +- .../tests/supabase_test/folder_test.rs | 10 +-- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 9 ++- .../rust-lib/flowy-user/src/entities/mod.rs | 4 +- .../{workspace_member.rs => workspace.rs} | 7 +++ .../rust-lib/flowy-user/src/event_handler.rs | 24 ++++++- frontend/rust-lib/flowy-user/src/event_map.rs | 11 +++- .../src/services/sqlite_sql/workspace_sql.rs | 63 ++++++++++++++++++- .../user_manager/manager_user_workspace.rs | 45 +++++++++---- .../scripts/tool/update_client_api_rev.sh | 2 +- 46 files changed, 457 insertions(+), 128 deletions(-) create mode 100644 frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs rename frontend/rust-lib/flowy-user/src/entities/{workspace_member.rs => workspace.rs} (93%) diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index f7821046660e9..9c32f11e39295 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -1995,6 +1995,7 @@ dependencies = [ "collab", "collab-document", "collab-entity", + "collab-folder", "collab-plugins", "flowy-database-pub", "flowy-document-pub", diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 95e07c80c1aa5..cb83e6e90cdfb 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1987,6 +1987,7 @@ dependencies = [ "collab", "collab-document", "collab-entity", + "collab-folder", "collab-plugins", "dotenv", "flowy-database-pub", diff --git a/frontend/rust-lib/event-integration/src/document_event.rs b/frontend/rust-lib/event-integration/src/document_event.rs index da839f09b6336..c4904042efbc3 100644 --- a/frontend/rust-lib/event-integration/src/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document_event.rs @@ -18,6 +18,13 @@ use crate::event_builder::EventBuilder; use crate::EventIntegrationTest; impl EventIntegrationTest { + pub async fn create_document(&self, name: &str) -> ViewPB { + let current_workspace = self.get_current_workspace().await; + self + .create_and_open_document(¤t_workspace.id, name.to_string(), vec![]) + .await + } + pub async fn create_and_open_document( &self, parent_id: &str, diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index 7eaed452e9e6b..06057937acef6 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -114,7 +114,7 @@ impl EventIntegrationTest { let uid = self.get_user_profile().await?.id; let doc_state = server .folder_service() - .get_collab_doc_state_f(&workspace_id, uid, collay_type, oid) + .get_folder_doc_state(&workspace_id, uid, collay_type, oid) .await?; Ok(doc_state) diff --git a/frontend/rust-lib/event-integration/src/user_event.rs b/frontend/rust-lib/event-integration/src/user_event.rs index 9274267f7f1ff..768a6e1a5c433 100644 --- a/frontend/rust-lib/event-integration/src/user_event.rs +++ b/frontend/rust-lib/event-integration/src/user_event.rs @@ -16,9 +16,9 @@ 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, ImportAppFlowyDataPB, OauthSignInPB, SignInUrlPB, - SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, UpdateUserProfilePayloadPB, - UserProfilePB, + AuthenticatorPB, CloudSettingPB, CreateWorkspacePB, ImportAppFlowyDataPB, OauthSignInPB, + RepeatedUserWorkspacePB, SignInUrlPB, SignInUrlPayloadPB, SignUpPayloadPB, UpdateCloudConfigPB, + UpdateUserProfilePayloadPB, UserProfilePB, UserWorkspaceIdPB, UserWorkspacePB, }; use flowy_user::errors::{FlowyError, FlowyResult}; use flowy_user::event_map::UserEvent; @@ -211,6 +211,48 @@ impl EventIntegrationTest { None => Ok(()), } } + + pub async fn create_workspace(&self, name: &str) -> UserWorkspacePB { + let payload = CreateWorkspacePB { + name: name.to_string(), + }; + EventBuilder::new(self.clone()) + .event(CreateWorkspace) + .payload(payload) + .async_send() + .await + .parse::() + } + + pub async fn get_all_workspaces(&self) -> RepeatedUserWorkspacePB { + EventBuilder::new(self.clone()) + .event(GetAllWorkspace) + .async_send() + .await + .parse::() + } + + pub async fn delete_workspace(&self, workspace_id: &str) { + let payload = UserWorkspaceIdPB { + workspace_id: workspace_id.to_string(), + }; + EventBuilder::new(self.clone()) + .event(DeleteWorkspace) + .payload(payload) + .async_send() + .await; + } + + pub async fn open_workspace(&self, workspace_id: &str) { + let payload = UserWorkspaceIdPB { + workspace_id: workspace_id.to_string(), + }; + EventBuilder::new(self.clone()) + .event(OpenWorkspace) + .payload(payload) + .async_send() + .await; + } } #[derive(Clone)] 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 c067ef63b801b..73488492da3a2 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 @@ -70,7 +70,7 @@ impl FlowySupabaseDatabaseTest { let workspace_id = self.user_manager.workspace_id().unwrap(); let cloud_service = self.database_manager.get_cloud_service().clone(); cloud_service - .get_collab_doc_state_db(database_id, CollabType::Database, &workspace_id) + .get_database_object_doc_state(database_id, CollabType::Database, &workspace_id) .await .unwrap() } diff --git a/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs b/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs index fe4d64311f7f1..7523f5ab0fc3e 100644 --- a/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs +++ b/frontend/rust-lib/event-integration/tests/folder/local_test/folder_test.rs @@ -68,7 +68,7 @@ async fn update_parent_view_test() { } #[tokio::test] -async fn app_create_with_view() { +async fn create_sub_views_test() { let mut test = FolderTest::new().await; let mut app = test.parent_view.clone(); test 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 b227d984073fe..0c67bf7373647 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 @@ -11,14 +11,14 @@ async fn create_workspace_event_test() { name: "my second workspace".to_owned(), desc: "".to_owned(), }; - let resp = EventBuilder::new(test) + let view_pb = EventBuilder::new(test) .event(flowy_folder::event_map::FolderEvent::CreateWorkspace) .payload(request) .async_send() .await - .error() - .unwrap(); - assert_eq!(resp.code, ErrorCode::NotSupportYet); + .parse::(); + + assert_eq!(view_pb.parent_view_id, "my second workspace".to_owned()); } // #[tokio::test] 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 863a41b1378a0..c00f4750f71e8 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 @@ -51,7 +51,7 @@ impl FlowySupabaseFolderTest { pub async fn get_collab_update(&self, workspace_id: &str) -> Vec { let cloud_service = self.folder_manager.get_cloud_service().clone(); cloud_service - .get_collab_doc_state_f( + .get_folder_doc_state( workspace_id, self.user_manager.user_id().unwrap(), CollabType::Folder, 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 645fd4399ad39..d6b6b4b382858 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,3 +2,4 @@ mod anon_user_test; mod auth_test; mod import_af_data_folder_test; mod member_test; +mod workspace_test; 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 new file mode 100644 index 0000000000000..18b367e619c59 --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/user/af_cloud_test/workspace_test.rs @@ -0,0 +1,52 @@ +use std::time::Duration; + +use event_integration::user_event::user_localhost_af_cloud; +use event_integration::EventIntegrationTest; +use flowy_user::entities::RepeatedUserWorkspacePB; +use flowy_user::protobuf::UserNotification; + +use crate::util::receive_with_timeout; + +#[tokio::test] +async fn af_cloud_create_workspace_test() { + 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.items; + assert_eq!(workspaces.len(), 1); + + test.create_workspace("my second workspace").await; + let _workspaces = test.get_all_workspaces().await.items; + + let a = user_profile_pb.id.to_string(); + let rx = test + .notification_sender + .subscribe::(&a, UserNotification::DidUpdateUserWorkspaces as i32); + let workspaces = receive_with_timeout(rx, Duration::from_secs(30)) + .await + .unwrap() + .items; + + assert_eq!(workspaces.len(), 2); + assert_eq!(workspaces[1].name, "my second workspace".to_string()); +} + +#[tokio::test] +async fn af_cloud_open_workspace_test() { + user_localhost_af_cloud().await; + let test = EventIntegrationTest::new().await; + let _ = test.af_cloud_sign_up().await; + + let workspace = test.create_workspace("my second workspace").await; + test.open_workspace(&workspace.workspace_id).await; + + test.create_document("my first document").await; + test.create_document("my second document").await; + + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 3); + // the first view is the default get started view + assert_eq!(views[1].name, "my first document".to_string()); + assert_eq!(views[2].name, "my second document".to_string()); +} diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs index 17274059b3fb1..74a9658373391 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/user_profile_test.rs @@ -1,13 +1,8 @@ -use nanoid::nanoid; - +use crate::user::local_test::helper::*; use event_integration::{event_builder::EventBuilder, EventIntegrationTest}; use flowy_user::entities::{AuthenticatorPB, UpdateUserProfilePayloadPB, UserProfilePB}; use flowy_user::{errors::ErrorCode, event_map::UserEvent::*}; - -use crate::user::local_test::helper::*; - -// use serial_test::*; - +use nanoid::nanoid; #[tokio::test] async fn user_profile_get_failed() { let sdk = EventIntegrationTest::new().await; 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 c153bad9367fa..f42671cb1c436 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 @@ -388,9 +388,8 @@ async fn migrate_anon_data_on_cloud_signup() { json!("Row document") ); } - assert!(cloud_service - .get_collab_doc_state_db(&database_id, CollabType::Database, &workspace_id) + .get_database_object_doc_state(&database_id, CollabType::Database, &workspace_id) .await .is_ok()); } 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 7bdb2220d7bea..7970f1d924361 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -181,7 +181,7 @@ impl FolderCloudService for ServerProvider { }) } - fn get_collab_doc_state_f( + fn get_folder_doc_state( &self, workspace_id: &str, uid: i64, @@ -194,12 +194,12 @@ impl FolderCloudService for ServerProvider { FutureResult::new(async move { server? .folder_service() - .get_collab_doc_state_f(&workspace_id, uid, collab_type, &object_id) + .get_folder_doc_state(&workspace_id, uid, collab_type, &object_id) .await }) } - fn batch_create_collab_object_f( + fn batch_create_folder_collab_objects( &self, workspace_id: &str, objects: Vec, @@ -209,7 +209,7 @@ impl FolderCloudService for ServerProvider { FutureResult::new(async move { server? .folder_service() - .batch_create_collab_object_f(&workspace_id, objects) + .batch_create_folder_collab_objects(&workspace_id, objects) .await }) } @@ -223,7 +223,7 @@ impl FolderCloudService for ServerProvider { } impl DatabaseCloudService for ServerProvider { - fn get_collab_doc_state_db( + fn get_database_object_doc_state( &self, object_id: &str, collab_type: CollabType, @@ -235,12 +235,12 @@ impl DatabaseCloudService for ServerProvider { FutureResult::new(async move { server? .database_service() - .get_collab_doc_state_db(&database_id, collab_type, &workspace_id) + .get_database_object_doc_state(&database_id, collab_type, &workspace_id) .await }) } - fn batch_get_collab_doc_state_db( + fn batch_get_database_object_doc_state( &self, object_ids: Vec, object_ty: CollabType, @@ -251,12 +251,12 @@ impl DatabaseCloudService for ServerProvider { FutureResult::new(async move { server? .database_service() - .batch_get_collab_doc_state_db(object_ids, object_ty, &workspace_id) + .batch_get_database_object_doc_state(object_ids, object_ty, &workspace_id) .await }) } - fn get_collab_snapshots( + fn get_database_collab_object_snapshots( &self, object_id: &str, limit: usize, @@ -266,7 +266,7 @@ impl DatabaseCloudService for ServerProvider { FutureResult::new(async move { server? .database_service() - .get_collab_snapshots(&database_id, limit) + .get_database_collab_object_snapshots(&database_id, limit) .await }) } diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index 55bee99174347..5caf2374b270f 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -154,7 +154,7 @@ impl UserStatusCallback for UserStatusCallbackImpl { // for initializing a default workspace differs depending on the sign-up method used. let data_source = match folder_manager .cloud_service - .get_collab_doc_state_f( + .get_folder_doc_state( &user_workspace.id, user_profile.uid, CollabType::Folder, diff --git a/frontend/rust-lib/flowy-database-pub/src/cloud.rs b/frontend/rust-lib/flowy-database-pub/src/cloud.rs index ebf96b03f879f..5e1bb5e1c9e75 100644 --- a/frontend/rust-lib/flowy-database-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-database-pub/src/cloud.rs @@ -12,23 +12,21 @@ pub type CollabDocStateByOid = HashMap; /// Each kind of server should implement this trait. Check out the [AppFlowyServerProvider] of /// [flowy-server] crate for more information. pub trait DatabaseCloudService: Send + Sync { - /// The suffix 'db' in the method name serves as a workaround to avoid naming conflicts with the existing method `get_collab_doc_state`. - fn get_collab_doc_state_db( + fn get_database_object_doc_state( &self, object_id: &str, collab_type: CollabType, workspace_id: &str, ) -> FutureResult; - /// The suffix 'db' in the method name serves as a workaround to avoid naming conflicts with the existing method `get_collab_doc_state`. - fn batch_get_collab_doc_state_db( + fn batch_get_database_object_doc_state( &self, object_ids: Vec, object_ty: CollabType, workspace_id: &str, ) -> FutureResult; - fn get_collab_snapshots( + fn get_database_collab_object_snapshots( &self, object_id: &str, limit: usize, diff --git a/frontend/rust-lib/flowy-database2/src/manager.rs b/frontend/rust-lib/flowy-database2/src/manager.rs index 9df2b69402c4b..7dc17d9e8d636 100644 --- a/frontend/rust-lib/flowy-database2/src/manager.rs +++ b/frontend/rust-lib/flowy-database2/src/manager.rs @@ -76,16 +76,21 @@ impl DatabaseManager { } } + /// When initialize with new workspace, all the resources will be cleared. pub async fn initialize( &self, uid: i64, workspace_id: String, workspace_database_object_id: String, ) -> FlowyResult<()> { - // Clear all existing tasks + // 1. Clear all existing tasks self.task_scheduler.write().await.clear_task(); - // Release all existing editors + // 2. Release all existing editors + for (_, editor) in self.editors.lock().await.iter() { + editor.close().await; + } self.editors.lock().await.clear(); + // 3. Clear the workspace database *self.workspace_database.write().await = None; let collab_db = self.user.collab_db(uid)?; @@ -95,22 +100,22 @@ impl DatabaseManager { cloud_service: self.cloud_service.clone(), }; let config = CollabPersistenceConfig::new().snapshot_per_update(100); - let mut collab_raw_data = CollabDocState::default(); + let mut workspace_database_doc_state = CollabDocState::default(); // 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"); match self .cloud_service - .get_collab_doc_state_db( + .get_database_object_doc_state( &workspace_database_object_id, CollabType::WorkspaceDatabase, &workspace_id, ) .await { - Ok(updates) => { - collab_raw_data = updates; + Ok(remote_doc_state) => { + workspace_database_doc_state = remote_doc_state; }, Err(err) => { return Err(FlowyError::record_not_found().with_context(format!( @@ -132,13 +137,12 @@ impl DatabaseManager { &workspace_database_object_id, CollabType::WorkspaceDatabase, collab_db.clone(), - collab_raw_data, + 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)); - Ok(()) } @@ -234,7 +238,7 @@ impl DatabaseManager { if let Some(database_id) = database_id { let mut editors = self.editors.lock().await; if let Some(editor) = editors.get(&database_id) { - editor.close_view_editor(view_id).await; + editor.close_view(view_id).await; } } @@ -350,7 +354,7 @@ impl DatabaseManager { let database_id = self.get_database_id_with_view_id(view_id).await?; let snapshots = self .cloud_service - .get_collab_snapshots(&database_id, limit) + .get_database_collab_object_snapshots(&database_id, limit) .await? .into_iter() .map(|snapshot| DatabaseSnapshotPB { @@ -423,7 +427,7 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { }, Some(cloud_service) => { let updates = cloud_service - .get_collab_doc_state_db(&object_id, object_ty, &workspace_id) + .get_database_object_doc_state(&object_id, object_ty, &workspace_id) .await?; Ok(updates) }, @@ -446,7 +450,7 @@ impl DatabaseCollabService for UserDatabaseCollabServiceImpl { }, Some(cloud_service) => { let updates = cloud_service - .batch_get_collab_doc_state_db(object_ids, object_ty, &workspace_id) + .batch_get_database_object_doc_state(object_ids, object_ty, &workspace_id) .await?; Ok(updates) }, 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 0b42c4e2d460e..4c6f2e757ac80 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 @@ -115,12 +115,24 @@ impl DatabaseEditor { /// Returns bool value indicating whether the database is empty. /// #[tracing::instrument(level = "debug", skip_all)] - pub async fn close_view_editor(&self, view_id: &str) -> bool { + 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 + } + + #[tracing::instrument(level = "debug", skip_all)] + pub async fn close(&self) { if let Some(database) = self.database.try_lock() { let _ = database.flush(); } - - self.database_views.close_view(view_id).await + for view in self.database_views.editors().await { + view.close().await; + } } pub async fn get_layout_type(&self, view_id: &str) -> DatabaseLayout { diff --git a/frontend/rust-lib/flowy-document/src/entities.rs b/frontend/rust-lib/flowy-document/src/entities.rs index f462b427ee99a..8fc07c1597ea6 100644 --- a/frontend/rust-lib/flowy-document/src/entities.rs +++ b/frontend/rust-lib/flowy-document/src/entities.rs @@ -73,6 +73,9 @@ pub struct UploadFileParamsPB { #[pb(index = 2)] #[validate(custom = "required_valid_path")] pub local_file_path: String, + + #[pb(index = 3)] + pub is_async: bool, } #[derive(Default, ProtoBuf, Validate)] diff --git a/frontend/rust-lib/flowy-document/src/event_handler.rs b/frontend/rust-lib/flowy-document/src/event_handler.rs index b42d70352e188..b8176f79a7d2e 100644 --- a/frontend/rust-lib/flowy-document/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document/src/event_handler.rs @@ -411,10 +411,13 @@ pub(crate) async fn upload_file_handler( let AFPluginData(UploadFileParamsPB { workspace_id, local_file_path, + is_async, }) = params; let manager = upgrade_document(manager)?; - let url = manager.upload_file(workspace_id, &local_file_path).await?; + let url = manager + .upload_file(workspace_id, &local_file_path, is_async) + .await?; Ok(AFPluginData(UploadedFilePB { url, diff --git a/frontend/rust-lib/flowy-document/src/manager.rs b/frontend/rust-lib/flowy-document/src/manager.rs index 04963efe37757..78cc0c493a432 100644 --- a/frontend/rust-lib/flowy-document/src/manager.rs +++ b/frontend/rust-lib/flowy-document/src/manager.rs @@ -254,18 +254,25 @@ impl DocumentManager { &self, workspace_id: String, local_file_path: &str, + is_async: bool, ) -> FlowyResult { let (object_identity, object_value) = object_from_disk(&workspace_id, local_file_path).await?; let storage_service = self.storage_service_upgrade()?; let url = storage_service.get_object_url(object_identity).await?; - // let the upload happen in the background let clone_url = url.clone(); - af_spawn(async move { - if let Err(e) = storage_service.put_object(clone_url, object_value).await { - error!("upload file failed: {}", e); - } - }); + + match is_async { + false => storage_service.put_object(clone_url, object_value).await?, + true => { + // let the upload happen in the background + af_spawn(async move { + if let Err(e) = storage_service.put_object(clone_url, object_value).await { + error!("upload file failed: {}", e); + } + }); + }, + } Ok(url) } diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index f3e0632d6b511..1b02f368b436c 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -268,6 +268,9 @@ pub enum ErrorCode { #[error("AppFlowy data folder import error")] AppFlowyDataFolderImportError = 89, + + #[error("Cloud request payload too large")] + CloudRequestPayloadTooLarge = 90, } 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 b59154d27c66b..c45bfb16c1dc3 100644 --- a/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs +++ b/frontend/rust-lib/flowy-error/src/impl_from/cloud.rs @@ -20,6 +20,7 @@ impl From for FlowyError { AppErrorCode::NotLoggedIn => ErrorCode::UserUnauthorized, AppErrorCode::NotEnoughPermissions => ErrorCode::NotEnoughPermissions, AppErrorCode::NetworkError => ErrorCode::HttpError, + AppErrorCode::PayloadTooLarge => ErrorCode::CloudRequestPayloadTooLarge, _ => 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 cec12091e36cb..316795ca590c3 100644 --- a/frontend/rust-lib/flowy-folder-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-folder-pub/src/cloud.rs @@ -30,8 +30,7 @@ pub trait FolderCloudService: Send + Sync + 'static { limit: usize, ) -> FutureResult, Error>; - /// The suffix 'f' in the method name serves as a workaround to avoid naming conflicts with the existing method `get_collab_doc_state`. - fn get_collab_doc_state_f( + fn get_folder_doc_state( &self, workspace_id: &str, uid: i64, @@ -39,8 +38,7 @@ pub trait FolderCloudService: Send + Sync + 'static { object_id: &str, ) -> FutureResult; - /// The suffix 'f' in the method name serves as a workaround to avoid naming conflicts with the existing method `get_collab_doc_state`. - fn batch_create_collab_object_f( + fn batch_create_folder_collab_objects( &self, workspace_id: &str, objects: Vec, diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 4a45130ee277b..701e36a8da83d 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -8,7 +8,7 @@ use collab_folder::{ Folder, FolderData, Section, SectionItem, TrashInfo, View, ViewLayout, ViewUpdate, Workspace, }; use parking_lot::{Mutex, RwLock}; -use tracing::{error, event, info, instrument, Level}; +use tracing::{error, info, instrument}; use collab_integrate::collab_builder::{AppFlowyCollabBuilder, CollabBuilderConfig}; use collab_integrate::{CollabKVDB, CollabPersistenceConfig}; @@ -156,16 +156,8 @@ impl FolderManager { ) -> FlowyResult<()> { let folder_doc_state = self .cloud_service - .get_collab_doc_state_f(workspace_id, user_id, CollabType::Folder, workspace_id) + .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) .await?; - - event!( - Level::INFO, - "Get folder updates via {}, number of updates: {}", - self.cloud_service.service_name(), - folder_doc_state.len() - ); - if let Err(err) = self .initialize( user_id, @@ -176,10 +168,7 @@ impl FolderManager { { // If failed to open folder with remote data, open from local disk. After open from the local // disk. the data will be synced to the remote server. - error!( - "Failed to initialize folder with error {}, fallback to use local data", - err - ); + error!("initialize folder with error {:?}, fallback local", err); self .initialize( user_id, @@ -213,7 +202,7 @@ impl FolderManager { // when the user signs up for the first time. let result = self .cloud_service - .get_collab_doc_state_f(workspace_id, user_id, CollabType::Folder, workspace_id) + .get_folder_doc_state(workspace_id, user_id, CollabType::Folder, workspace_id) .await .map_err(FlowyError::from); @@ -249,8 +238,13 @@ impl FolderManager { pub async fn clear(&self, _user_id: i64) {} #[tracing::instrument(level = "info", skip_all, err)] - pub async fn create_workspace(&self, _params: CreateWorkspaceParams) -> FlowyResult { - Err(FlowyError::not_support()) + pub async fn create_workspace(&self, params: CreateWorkspaceParams) -> FlowyResult { + let uid = self.user.user_id()?; + let new_workspace = self + .cloud_service + .create_workspace(uid, ¶ms.name) + .await?; + Ok(new_workspace) } #[tracing::instrument(level = "info", skip_all, err)] diff --git a/frontend/rust-lib/flowy-server/Cargo.toml b/frontend/rust-lib/flowy-server/Cargo.toml index a7f1baba9403b..4cfc90ae7515f 100644 --- a/frontend/rust-lib/flowy-server/Cargo.toml +++ b/frontend/rust-lib/flowy-server/Cargo.toml @@ -29,6 +29,7 @@ collab = { version = "0.1.0" } collab-plugins = { version = "0.1.0"} collab-document = { version = "0.1.0" } collab-entity = { version = "0.1.0" } +collab-folder = { version = "0.1.0" } hex = "0.4.3" postgrest = "1.0" lib-infra = { workspace = true } 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 48e1fd376d36b..ad5c7ce5cf01f 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 @@ -18,7 +18,7 @@ impl DatabaseCloudService for AFCloudDatabaseCloudServiceImpl where T: AFServer, { - fn get_collab_doc_state_db( + fn get_database_object_doc_state( &self, object_id: &str, collab_type: CollabType, @@ -48,7 +48,7 @@ where }) } - fn batch_get_collab_doc_state_db( + fn batch_get_database_object_doc_state( &self, object_ids: Vec, object_ty: CollabType, @@ -90,7 +90,7 @@ where }) } - fn get_collab_snapshots( + fn get_database_collab_object_snapshots( &self, _object_id: &str, _limit: usize, 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 9faf77cab8e89..dcc1b8aa3a4a8 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 @@ -1,8 +1,11 @@ -use anyhow::{anyhow, Error}; -use client_api::entity::{CollabParams, QueryCollab, QueryCollabParams}; +use anyhow::Error; +use client_api::entity::{ + workspace_dto::CreateWorkspaceParam, CollabParams, QueryCollab, QueryCollabParams, +}; use collab::core::collab::CollabDocState; use collab::core::origin::CollabOrigin; use collab_entity::CollabType; +use collab_folder::RepeatedViewIdentifier; use flowy_error::FlowyError; use flowy_folder_pub::cloud::{ @@ -19,8 +22,27 @@ impl FolderCloudService for AFCloudFolderCloudServiceImpl where T: AFServer, { - fn create_workspace(&self, _uid: i64, _name: &str) -> FutureResult { - FutureResult::new(async move { Err(anyhow!("Not support yet")) }) + fn create_workspace(&self, _uid: i64, name: &str) -> FutureResult { + let try_get_client = self.0.try_get_client(); + let cloned_name = name.to_string(); + FutureResult::new(async move { + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(cloned_name), + }) + .await?; + + Ok(Workspace { + id: new_workspace.workspace_id.to_string(), + name: new_workspace.workspace_name, + created_at: new_workspace.created_at.timestamp(), + child_views: RepeatedViewIdentifier::new(vec![]), + created_by: Some(new_workspace.owner_uid), + last_edited_time: new_workspace.created_at.timestamp(), + last_edited_by: Some(new_workspace.owner_uid), + }) + }) } fn open_workspace(&self, workspace_id: &str) -> FutureResult<(), Error> { @@ -88,7 +110,7 @@ where FutureResult::new(async move { Ok(vec![]) }) } - fn get_collab_doc_state_f( + fn get_folder_doc_state( &self, workspace_id: &str, _uid: i64, @@ -116,7 +138,7 @@ where }) } - fn batch_create_collab_object_f( + fn batch_create_folder_collab_objects( &self, workspace_id: &str, objects: Vec, 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 97c788cfc9f21..57de02de3174a 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 @@ -2,7 +2,9 @@ use std::collections::HashMap; use std::sync::Arc; use anyhow::{anyhow, Error}; -use client_api::entity::workspace_dto::{CreateWorkspaceMember, WorkspaceMemberChangeset}; +use client_api::entity::workspace_dto::{ + CreateWorkspaceMember, CreateWorkspaceParam, WorkspaceMemberChangeset, +}; use client_api::entity::{AFRole, AFWorkspace, AuthProvider, CollabParams, CreateCollabParams}; use client_api::{Client, ClientConfiguration}; use collab::core::collab::CollabDocState; @@ -294,6 +296,30 @@ where Ok(()) }) } + + fn create_workspace(&self, workspace_name: &str) -> FutureResult { + let try_get_client = self.server.try_get_client(); + let workspace_name_owned = workspace_name.to_owned(); + FutureResult::new(async move { + let client = try_get_client?; + let new_workspace = client + .create_workspace(CreateWorkspaceParam { + workspace_name: Some(workspace_name_owned), + }) + .await?; + Ok(to_user_workspace(new_workspace)) + }) + } + + fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError> { + let try_get_client = self.server.try_get_client(); + let workspace_id_owned = workspace_id.to_owned(); + FutureResult::new(async move { + let client = try_get_client?; + client.delete_workspace(&workspace_id_owned).await?; + Ok(()) + }) + } } async fn get_admin_client(client: &Arc) -> FlowyResult { 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 7560b24f94d4a..9092c967a9f74 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 @@ -8,7 +8,7 @@ use lib_infra::future::FutureResult; pub(crate) struct LocalServerDatabaseCloudServiceImpl(); impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { - fn get_collab_doc_state_db( + fn get_database_object_doc_state( &self, _object_id: &str, _collab_type: CollabType, @@ -17,7 +17,7 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { FutureResult::new(async move { Ok(vec![]) }) } - fn batch_get_collab_doc_state_db( + fn batch_get_database_object_doc_state( &self, _object_ids: Vec, _object_ty: CollabType, @@ -26,7 +26,7 @@ impl DatabaseCloudService for LocalServerDatabaseCloudServiceImpl { FutureResult::new(async move { Ok(CollabDocStateByOid::default()) }) } - fn get_collab_snapshots( + fn get_database_collab_object_snapshots( &self, _object_id: &str, _limit: usize, 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 01efb5a51b234..4920df3c5132b 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 @@ -53,7 +53,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { FutureResult::new(async move { Ok(vec![]) }) } - fn get_collab_doc_state_f( + fn get_folder_doc_state( &self, _workspace_id: &str, _uid: i64, @@ -67,7 +67,7 @@ impl FolderCloudService for LocalServerFolderCloudServiceImpl { }) } - fn batch_create_collab_object_f( + fn batch_create_folder_collab_objects( &self, _workspace_id: &str, _objects: Vec, 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 3fd3fe71319a1..feb63ddc383f8 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 @@ -174,6 +174,24 @@ impl UserCloudService for LocalServerUserAuthServiceImpl { ) -> FutureResult<(), Error> { FutureResult::new(async { Err(anyhow!("local server doesn't support create collab object")) }) } + + fn create_workspace(&self, _workspace_name: &str) -> FutureResult { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support mulitple workspaces"), + ) + }) + } + + fn delete_workspace(&self, _workspace_id: &str) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("local server doesn't support mulitple workspaces"), + ) + }) + } } fn make_user_workspace() -> UserWorkspace { diff --git a/frontend/rust-lib/flowy-server/src/server.rs b/frontend/rust-lib/flowy-server/src/server.rs index be5b39d86a7fc..483cd4f8daa40 100644 --- a/frontend/rust-lib/flowy-server/src/server.rs +++ b/frontend/rust-lib/flowy-server/src/server.rs @@ -1,9 +1,11 @@ +use client_api::ws::ConnectState; +use client_api::ws::WSConnectStateReceiver; +use client_api::ws::WebSocketChannel; use flowy_storage::ObjectStorageService; use std::sync::Arc; use anyhow::Error; use client_api::collab_sync::collab_msg::CollabMessage; -use client_api::ws::{ConnectState, WSConnectStateReceiver, WebSocketChannel}; use parking_lot::RwLock; use tokio_stream::wrappers::WatchStream; #[cfg(feature = "enable_supabase")] 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 c18cd3635eb6c..afd6a2cac8da1 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/database.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/database.rs @@ -26,7 +26,7 @@ impl DatabaseCloudService for SupabaseDatabaseServiceImpl where T: SupabaseServerService, { - fn get_collab_doc_state_db( + fn get_database_object_doc_state( &self, object_id: &str, collab_type: CollabType, @@ -50,7 +50,7 @@ where FutureResult::new(async { rx.await? }) } - fn batch_get_collab_doc_state_db( + fn batch_get_database_object_doc_state( &self, object_ids: Vec, object_ty: CollabType, @@ -72,7 +72,7 @@ where FutureResult::new(async { rx.await? }) } - fn get_collab_snapshots( + fn get_database_collab_object_snapshots( &self, object_id: &str, limit: usize, 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 6b7f309af144a..81c19a015dad9 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/folder.rs @@ -131,7 +131,7 @@ where }) } - fn get_collab_doc_state_f( + fn get_folder_doc_state( &self, _workspace_id: &str, _uid: i64, @@ -154,7 +154,7 @@ where FutureResult::new(async { rx.await? }) } - fn batch_create_collab_object_f( + fn batch_create_folder_collab_objects( &self, _workspace_id: &str, _objects: Vec, 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 73b15d664a2d8..1307787c73ab7 100644 --- a/frontend/rust-lib/flowy-server/src/supabase/api/user.rs +++ b/frontend/rust-lib/flowy-server/src/supabase/api/user.rs @@ -354,6 +354,24 @@ where )) }) } + + fn create_workspace(&self, _workspace_name: &str) -> FutureResult { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("supabase server doesn't support mulitple workspaces"), + ) + }) + } + + fn delete_workspace(&self, _workspace_id: &str) -> FutureResult<(), FlowyError> { + FutureResult::new(async { + Err( + FlowyError::local_version_not_support() + .with_context("supabase server doesn't support mulitple workspaces"), + ) + }) + } } pub struct CreateCollabAction { 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 ad6d4eedf83a4..da17de9c65e65 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 @@ -45,7 +45,7 @@ async fn supabase_create_database_test() { } let updates_by_oid = database_service - .batch_get_collab_doc_state_db(row_ids, CollabType::DatabaseRow, "fake_workspace_id") + .batch_get_database_object_doc_state(row_ids, CollabType::DatabaseRow, "fake_workspace_id") .await .unwrap(); diff --git a/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs index a8a27ce4d5229..5d7922a1e4d42 100644 --- a/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs +++ b/frontend/rust-lib/flowy-server/tests/supabase_test/folder_test.rs @@ -69,7 +69,7 @@ async fn supabase_get_folder_test() { // let updates = collab_service.get_all_updates(&collab_object).await.unwrap(); let updates = folder_service - .get_collab_doc_state_f( + .get_folder_doc_state( &user.latest_workspace.id, user.user_id, CollabType::Folder, @@ -86,7 +86,7 @@ async fn supabase_get_folder_test() { .unwrap(); } let updates = folder_service - .get_collab_doc_state_f( + .get_folder_doc_state( &user.latest_workspace.id, user.user_id, CollabType::Folder, @@ -157,7 +157,7 @@ async fn supabase_duplicate_updates_test() { .await .unwrap(); let first_init_sync_update = folder_service - .get_collab_doc_state_f( + .get_folder_doc_state( &user.latest_workspace.id, user.user_id, CollabType::Folder, @@ -179,7 +179,7 @@ async fn supabase_duplicate_updates_test() { .await .unwrap(); let second_init_sync_update = folder_service - .get_collab_doc_state_f( + .get_folder_doc_state( &user.latest_workspace.id, user.user_id, CollabType::Folder, @@ -271,7 +271,7 @@ async fn supabase_diff_state_vector_test() { let old_version_doc = Doc::new(); let map = { old_version_doc.get_or_insert_map("map") }; let doc_state = folder_service - .get_collab_doc_state_f( + .get_folder_doc_state( &user.latest_workspace.id, user.user_id, CollabType::Folder, diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index c986c261b728c..e5e37ed5e55b1 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -176,9 +176,16 @@ pub trait UserCloudService: Send + Sync + 'static { fn open_workspace(&self, workspace_id: &str) -> FutureResult; - /// Return the all the workspaces of the user + /// Return the all the workspaces of the user fn get_all_workspace(&self, uid: i64) -> FutureResult, FlowyError>; + /// Creates a new workspace for the user. + /// Returns the new workspace if successful + fn create_workspace(&self, workspace_name: &str) -> FutureResult; + + /// Deletes a workspace owned by the user. + fn delete_workspace(&self, workspace_id: &str) -> FutureResult<(), FlowyError>; + fn add_workspace_member( &self, user_email: String, diff --git a/frontend/rust-lib/flowy-user/src/entities/mod.rs b/frontend/rust-lib/flowy-user/src/entities/mod.rs index 46cab3b6255b8..2b49fd8bdd270 100644 --- a/frontend/rust-lib/flowy-user/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-user/src/entities/mod.rs @@ -4,7 +4,7 @@ pub use realtime::*; pub use reminder::*; pub use user_profile::*; pub use user_setting::*; -pub use workspace_member::*; +pub use workspace::*; pub mod auth; pub mod date_time; @@ -14,4 +14,4 @@ pub mod realtime; mod reminder; mod user_profile; mod user_setting; -mod workspace_member; +mod workspace; diff --git a/frontend/rust-lib/flowy-user/src/entities/workspace_member.rs b/frontend/rust-lib/flowy-user/src/entities/workspace.rs similarity index 93% rename from frontend/rust-lib/flowy-user/src/entities/workspace_member.rs rename to frontend/rust-lib/flowy-user/src/entities/workspace.rs index e67a785133ca6..daef940819cd8 100644 --- a/frontend/rust-lib/flowy-user/src/entities/workspace_member.rs +++ b/frontend/rust-lib/flowy-user/src/entities/workspace.rs @@ -109,3 +109,10 @@ pub struct UserWorkspaceIdPB { #[validate(custom = "required_not_empty_str")] pub workspace_id: String, } + +#[derive(ProtoBuf, Default, Clone, Validate)] +pub struct CreateWorkspacePB { + #[pb(index = 1)] + #[validate(custom = "required_not_empty_str")] + pub name: String, +} diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index 6896c6ab2a5e2..c86d810a0a159 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -466,7 +466,7 @@ pub async fn get_all_workspace_handler( ) -> DataResult { let manager = upgrade_manager(manager)?; let uid = manager.get_session()?.user_id; - let user_workspaces = manager.get_all_user_workspaces(uid)?; + let user_workspaces = manager.get_all_user_workspaces(uid).await?; data_result_ok(user_workspaces.into()) } @@ -652,3 +652,25 @@ pub async fn update_workspace_member_handler( .await?; Ok(()) } + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn create_workspace_handler( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let data = data.try_into_inner()?; + let manager = upgrade_manager(manager)?; + let new_workspace = manager.add_workspace(&data.name).await?; + data_result_ok(new_workspace.into()) +} + +#[tracing::instrument(level = "debug", skip_all, err)] +pub async fn delete_workspace_handler( + delete_workspace_param: AFPluginData, + manager: AFPluginState>, +) -> Result<(), FlowyError> { + let workspace_id = delete_workspace_param.try_into_inner()?.workspace_id; + let manager = upgrade_manager(manager)?; + manager.delete_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 5e03387fb928c..6dbeb877b2081 100644 --- a/frontend/rust-lib/flowy-user/src/event_map.rs +++ b/frontend/rust-lib/flowy-user/src/event_map.rs @@ -38,7 +38,6 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::OauthSignIn, oauth_sign_in_handler) .event(UserEvent::GenerateSignInURL, gen_sign_in_url_handler) .event(UserEvent::GetOauthURLWithProvider, sign_in_with_provider_handler) - .event(UserEvent::GetAllWorkspace, get_all_workspace_handler) .event(UserEvent::OpenWorkspace, open_workspace_handler) .event(UserEvent::UpdateNetworkState, update_network_state_handler) .event(UserEvent::OpenAnonUser, open_anon_user_handler) @@ -59,6 +58,10 @@ pub fn init(user_manager: Weak) -> AFPlugin { .event(UserEvent::RemoveWorkspaceMember, delete_workspace_member_handler) .event(UserEvent::GetWorkspaceMember, get_workspace_member_handler) .event(UserEvent::UpdateWorkspaceMember, update_workspace_member_handler) + // Workspace + .event(UserEvent::GetAllWorkspace, get_all_workspace_handler) + .event(UserEvent::CreateWorkspace, create_workspace_handler) + .event(UserEvent::DeleteWorkspace, delete_workspace_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -191,6 +194,12 @@ pub enum UserEvent { #[event(input = "ImportAppFlowyDataPB")] ImportAppFlowyDataFolder = 41, + + #[event(output = "CreateWorkspacePB")] + CreateWorkspace = 42, + + #[event(input = "UserWorkspaceIdPB")] + DeleteWorkspace = 43, } pub trait UserStatusCallback: Send + Sync + 'static { 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 9c9eebd5983dd..e335949448128 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 @@ -1,10 +1,11 @@ -use std::convert::TryFrom; - use chrono::{TimeZone, Utc}; - +use diesel::{RunQueryDsl, SqliteConnection}; use flowy_error::FlowyError; use flowy_sqlite::schema::user_workspace_table; +use flowy_sqlite::DBConnection; +use flowy_sqlite::{query_dsl::*, ExpressionMethods}; use flowy_user_pub::entities::UserWorkspace; +use std::convert::TryFrom; #[derive(Clone, Default, Queryable, Identifiable, Insertable)] #[diesel(table_name = user_workspace_table)] @@ -16,6 +17,62 @@ pub struct UserWorkspaceTable { pub database_storage_id: String, } +pub fn get_user_workspace_op(workspace_id: &str, mut conn: DBConnection) -> Option { + user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::id.eq(workspace_id)) + .first::(&mut *conn) + .ok() + .map(UserWorkspace::from) +} + +pub fn get_all_user_workspace_op( + user_id: i64, + mut conn: DBConnection, +) -> Result, FlowyError> { + let rows = user_workspace_table::dsl::user_workspace_table + .filter(user_workspace_table::uid.eq(user_id)) + .load::(&mut *conn)?; + Ok(rows.into_iter().map(UserWorkspace::from).collect()) +} + +/// Remove all existing workspaces for given user and insert the new ones. +/// +#[allow(dead_code)] +pub fn save_user_workspaces_op( + uid: i64, + mut conn: DBConnection, + user_workspaces: &[UserWorkspace], +) -> Result<(), FlowyError> { + conn.immediate_transaction(|conn| { + delete_existing_workspaces(uid, conn)?; + insert_new_workspaces_op(uid, user_workspaces, conn)?; + Ok(()) + }) +} + +#[allow(dead_code)] +fn delete_existing_workspaces(uid: i64, conn: &mut SqliteConnection) -> Result<(), FlowyError> { + diesel::delete( + user_workspace_table::dsl::user_workspace_table.filter(user_workspace_table::uid.eq(uid)), + ) + .execute(conn)?; + Ok(()) +} + +pub fn insert_new_workspaces_op( + uid: i64, + user_workspaces: &[UserWorkspace], + conn: &mut SqliteConnection, +) -> Result<(), FlowyError> { + for user_workspace in user_workspaces { + let new_record = UserWorkspaceTable::try_from((uid, user_workspace))?; + diesel::insert_into(user_workspace_table::table) + .values(new_record) + .execute(conn)?; + } + Ok(()) +} + impl TryFrom<(i64, &UserWorkspace)> for UserWorkspaceTable { type Error = FlowyError; 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 83e1c62f7790c..aa75a6f91253f 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 @@ -16,7 +16,9 @@ use crate::entities::{RepeatedUserWorkspacePB, ResetWorkspacePB}; use crate::migrations::AnonUser; use crate::notification::{send_notification, UserNotification}; use crate::services::data_import::{upload_collab_objects_data, ImportContext}; -use crate::services::sqlite_sql::workspace_sql::UserWorkspaceTable; +use crate::services::sqlite_sql::workspace_sql::{ + get_all_user_workspace_op, get_user_workspace_op, insert_new_workspaces_op, UserWorkspaceTable, +}; use crate::user_manager::UserManager; use flowy_user_pub::session::Session; @@ -151,6 +153,29 @@ impl UserManager { Ok(()) } + pub async fn add_workspace(&self, workspace_name: &str) -> FlowyResult { + let new_workspace = self + .cloud_services + .get_user_service()? + .create_workspace(workspace_name) + .await?; + + // save the workspace to sqlite db + let uid = self.user_id()?; + let mut conn = self.db_connection(uid)?; + insert_new_workspaces_op(uid, &[new_workspace.clone()], &mut conn)?; + Ok(new_workspace) + } + + pub async fn delete_workspace(&self, workspace_id: &str) -> FlowyResult<()> { + self + .cloud_services + .get_user_service()? + .delete_workspace(workspace_id) + .await?; + Ok(()) + } + pub async fn add_workspace_member( &self, user_email: String, @@ -204,19 +229,13 @@ impl UserManager { } pub fn get_user_workspace(&self, uid: i64, workspace_id: &str) -> Option { - let mut conn = self.db_connection(uid).ok()?; - let row = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::id.eq(workspace_id)) - .first::(&mut *conn) - .ok()?; - Some(UserWorkspace::from(row)) + let conn = self.db_connection(uid).ok()?; + get_user_workspace_op(workspace_id, conn) } - pub fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult> { - let mut conn = self.db_connection(uid)?; - let rows = user_workspace_table::dsl::user_workspace_table - .filter(user_workspace_table::uid.eq(uid)) - .load::(&mut *conn)?; + pub async fn get_all_user_workspaces(&self, uid: i64) -> FlowyResult> { + let conn = self.db_connection(uid)?; + let workspaces = get_all_user_workspace_op(uid, conn)?; if let Ok(service) = self.cloud_services.get_user_service() { if let Ok(pool) = self.db_pool(uid) { @@ -233,7 +252,7 @@ impl UserManager { }); } } - Ok(rows.into_iter().map(UserWorkspace::from).collect()) + Ok(workspaces) } /// Reset the remote workspace using local workspace data. This is useful when a user wishes to diff --git a/frontend/scripts/tool/update_client_api_rev.sh b/frontend/scripts/tool/update_client_api_rev.sh index 472bb87d6fd50..877a0461950ba 100755 --- a/frontend/scripts/tool/update_client_api_rev.sh +++ b/frontend/scripts/tool/update_client_api_rev.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # Ensure a new revision ID is provided if [ "$#" -ne 1 ]; then From fda70ff5606a3f6a0cb17df65ba12adbbd8ec5e3 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 4 Feb 2024 05:50:23 +0800 Subject: [PATCH 09/50] feat: folder web (#4580) * chore: folder wasm * chore: folder wasm * chore: resolve deps * chore: fix trait * chore: try localset * chore: fix * chore: fix * chore: fix * chore: async init sdk * chore: fix test * chore: fix test --- frontend/Makefile.toml | 1 + .../lib/appflowy_backend.dart | 12 +- .../packages/appflowy_backend/lib/ffi.dart | 5 +- .../appflowy_backend/macos/Classes/binding.h | 2 +- frontend/appflowy_tauri/src-tauri/Cargo.lock | 1 + frontend/appflowy_tauri/src-tauri/src/init.rs | 7 +- .../services/backend/notifications/index.ts | 2 - .../backend/notifications/observer.ts | 34 --- .../services/backend/notifications/parser.ts | 44 ---- .../src/services/deps_resolve/index.ts | 1 - frontend/appflowy_web/.eslintrc.cjs | 81 +++++-- frontend/appflowy_web/package.json | 4 +- frontend/appflowy_web/pnpm-lock.yaml | 165 +++++++++++++++ frontend/appflowy_web/src/App.tsx | 4 +- .../src/services/backend/index.ts | 3 + frontend/appflowy_web/wasm-libs/Cargo.lock | 110 +++++++--- frontend/appflowy_web/wasm-libs/Cargo.toml | 8 +- .../wasm-libs/af-persistence/Cargo.toml | 2 - .../appflowy_web/wasm-libs/af-user/Cargo.toml | 2 - .../appflowy_web/wasm-libs/af-user/build.rs | 7 +- .../af-user/src/authenticate_user.rs | 20 ++ .../wasm-libs/af-user/src/event_handler.rs | 12 +- .../wasm-libs/af-user/src/event_map.rs | 4 +- .../appflowy_web/wasm-libs/af-user/src/lib.rs | 1 + .../wasm-libs/af-user/src/manager.rs | 14 +- .../appflowy_web/wasm-libs/af-wasm/Cargo.toml | 17 ++ .../wasm-libs/af-wasm/src/core.rs | 38 +++- .../af-wasm/src/deps_resolve/document_deps.rs | 19 ++ .../af-wasm/src/deps_resolve/folder_deps.rs | 20 ++ .../wasm-libs/af-wasm/src/deps_resolve/mod.rs | 2 + .../wasm-libs/af-wasm/src/integrate/server.rs | 29 ++- .../appflowy_web/wasm-libs/af-wasm/src/lib.rs | 9 + .../wasm-libs/af-wasm/tests/util/tester.rs | 43 ++++ frontend/rust-lib/Cargo.lock | 1 + .../flowy-codegen/src/protobuf_file/mod.rs | 4 +- .../flowy-codegen/src/ts_event/mod.rs | 9 +- frontend/rust-lib/dart-ffi/binding.h | 2 +- frontend/rust-lib/dart-ffi/src/lib.rs | 13 +- .../rust-lib/event-integration/src/lib.rs | 20 +- frontend/rust-lib/flowy-config/build.rs | 6 +- frontend/rust-lib/flowy-core/Cargo.toml | 4 +- .../flowy-core/src/integrate/trait_impls.rs | 7 +- .../rust-lib/flowy-core/src/integrate/user.rs | 2 +- frontend/rust-lib/flowy-core/src/lib.rs | 13 +- frontend/rust-lib/flowy-database2/build.rs | 2 +- frontend/rust-lib/flowy-date/build.rs | 6 +- frontend/rust-lib/flowy-document/build.rs | 9 +- frontend/rust-lib/flowy-error/build.rs | 7 +- frontend/rust-lib/flowy-folder/Cargo.toml | 6 +- frontend/rust-lib/flowy-folder/build.rs | 25 ++- frontend/rust-lib/flowy-folder/src/manager.rs | 13 +- .../rust-lib/flowy-folder/src/manager_init.rs | 12 +- frontend/rust-lib/flowy-folder/src/util.rs | 22 +- frontend/rust-lib/flowy-notification/build.rs | 7 +- frontend/rust-lib/flowy-server/src/lib.rs | 1 - frontend/rust-lib/flowy-server/src/request.rs | 198 ------------------ frontend/rust-lib/flowy-storage/src/lib.rs | 89 ++++---- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 23 +- frontend/rust-lib/flowy-user/build.rs | 6 +- frontend/rust-lib/lib-dispatch/Cargo.toml | 1 + .../rust-lib/lib-dispatch/src/dispatcher.rs | 85 ++++---- frontend/rust-lib/lib-dispatch/src/runtime.rs | 24 +-- .../lib-dispatch/src/service/boxed.rs | 8 +- frontend/rust-lib/lib-infra/src/util.rs | 15 ++ frontend/scripts/makefile/web.toml | 9 + 65 files changed, 816 insertions(+), 556 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/services/backend/notifications/index.ts delete mode 100644 frontend/appflowy_tauri/src/services/backend/notifications/observer.ts delete mode 100644 frontend/appflowy_tauri/src/services/backend/notifications/parser.ts delete mode 100644 frontend/appflowy_tauri/src/services/deps_resolve/index.ts create mode 100644 frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs create mode 100644 frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs create mode 100644 frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs create mode 100644 frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs delete mode 100644 frontend/rust-lib/flowy-server/src/request.rs diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 3038bd7e7866a..a5bac2f43b002 100644 --- a/frontend/Makefile.toml +++ b/frontend/Makefile.toml @@ -50,6 +50,7 @@ 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" # Test default config TEST_CRATE_TYPE = "cdylib" TEST_LIB_EXT = "dylib" diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart index c10178f666666..f9001c475fee1 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/appflowy_backend.dart @@ -29,8 +29,16 @@ class FlowySDK { Future init(String configuration) async { final port = RustStreamReceiver.shared.port; ffi.set_stream_port(port); - ffi.store_dart_post_cobject(NativeApi.postCObject); - ffi.init_sdk(configuration.toNativeUtf8()); + + // final completer = Completer(); + // // Create a SendPort that accepts only one message. + // final sendPort = singleCompletePort(completer); + + final code = ffi.init_sdk(0, configuration.toNativeUtf8()); + if (code != 0) { + throw Exception('Failed to initialize the SDK'); + } + // return completer.future; } } diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart index 4a3a8aa0d2f4e..51b6f9851f172 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart +++ b/frontend/appflowy_flutter/packages/appflowy_backend/lib/ffi.dart @@ -77,17 +77,20 @@ typedef _invoke_sync_Dart = Pointer Function( /// C function `init_sdk`. int init_sdk( + int port, Pointer data, ) { - return _init_sdk(data); + return _init_sdk(port, data); } final _init_sdk_Dart _init_sdk = _dart_ffi_lib.lookupFunction<_init_sdk_C, _init_sdk_Dart>('init_sdk'); typedef _init_sdk_C = Int64 Function( + Int64 port, Pointer path, ); typedef _init_sdk_Dart = int Function( + int port, Pointer path, ); diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h b/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h index 710af508fd51e..3fe1f39faaf71 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h +++ b/frontend/appflowy_flutter/packages/appflowy_backend/macos/Classes/binding.h @@ -3,7 +3,7 @@ #include #include -int64_t init_sdk(char *data); +int64_t init_sdk(int64_t port, char *data); void async_event(int64_t port, const uint8_t *input, uintptr_t len); diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 9c32f11e39295..734d7b474a1de 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -1930,6 +1930,7 @@ dependencies = [ name = "flowy-folder" version = "0.1.0" dependencies = [ + "async-trait", "bytes", "chrono", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/src/init.rs b/frontend/appflowy_tauri/src-tauri/src/init.rs index b226251b59939..7f7c2726d3e35 100644 --- a/frontend/appflowy_tauri/src-tauri/src/init.rs +++ b/frontend/appflowy_tauri/src-tauri/src/init.rs @@ -1,5 +1,7 @@ use flowy_core::config::AppFlowyCoreConfig; use flowy_core::{AppFlowyCore, DEFAULT_NAME}; +use lib_dispatch::runtime::AFPluginRuntime; +use std::sync::Arc; pub fn init_flowy_core() -> AppFlowyCore { let config_json = include_str!("../tauri.conf.json"); @@ -26,5 +28,8 @@ pub fn init_flowy_core() -> AppFlowyCore { DEFAULT_NAME.to_string(), ) .log_filter("trace", vec!["appflowy_tauri".to_string()]); - AppFlowyCore::new(config) + + 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_tauri/src/services/backend/notifications/index.ts b/frontend/appflowy_tauri/src/services/backend/notifications/index.ts deleted file mode 100644 index 132155f3c8558..0000000000000 --- a/frontend/appflowy_tauri/src/services/backend/notifications/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './observer'; -export * from './parser'; diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts b/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts deleted file mode 100644 index 8366be372996f..0000000000000 --- a/frontend/appflowy_tauri/src/services/backend/notifications/observer.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { listen, UnlistenFn } from "@tauri-apps/api/event"; -import { SubscribeObject } from "../models/flowy-notification"; -import { NotificationParser } from "./parser"; - -export abstract class AFNotificationObserver { - parser?: NotificationParser | null; - private _listener?: UnlistenFn; - - protected constructor(parser?: NotificationParser) { - this.parser = parser; - } - - async start() { - this._listener = await listen("af-notification", (notification) => { - const object: SubscribeObject = SubscribeObject.fromObject(notification.payload as {}); - if (this.parser?.id !== undefined) { - if (object.id === this.parser.id) { - this.parser?.parse(object); - } - } else { - this.parser?.parse(object); - } - }); - } - - async stop() { - if (this._listener !== undefined) { - // call the unlisten function before setting it to undefined - this._listener(); - this._listener = undefined; - } - this.parser = null; - } -} diff --git a/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts b/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts deleted file mode 100644 index dc58c4eec7ce0..0000000000000 --- a/frontend/appflowy_tauri/src/services/backend/notifications/parser.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { FlowyError } from "../models/flowy-error"; -import { SubscribeObject } from "../models/flowy-notification"; -import { Err, Ok, Result } from "ts-results"; - -export declare type OnNotificationPayload = (ty: T, payload: Result) => void; -export declare type OnNotificationError = (error: FlowyError) => void; -export declare type NotificationTyParser = (num: number) => T | null; -export declare type ErrParser = (data: Uint8Array) => E; - -export abstract class NotificationParser { - id?: string; - onPayload: OnNotificationPayload; - tyParser: NotificationTyParser; - - protected constructor( - onPayload: OnNotificationPayload, - tyParser: NotificationTyParser, - id?: string - ) { - this.id = id; - this.onPayload = onPayload; - this.tyParser = tyParser; - } - - parse(subject: SubscribeObject) { - if (typeof this.id !== "undefined" && this.id.length === 0) { - if (subject.id !== this.id) { - return; - } - } - - const ty = this.tyParser(subject.ty); - if (ty === null) { - return; - } - - if (subject.has_error) { - const error = FlowyError.deserializeBinary(subject.error); - this.onPayload(ty, Err(error)); - } else { - this.onPayload(ty, Ok(subject.payload)); - } - } -} diff --git a/frontend/appflowy_tauri/src/services/deps_resolve/index.ts b/frontend/appflowy_tauri/src/services/deps_resolve/index.ts deleted file mode 100644 index f47da57f03d17..0000000000000 --- a/frontend/appflowy_tauri/src/services/deps_resolve/index.ts +++ /dev/null @@ -1 +0,0 @@ -export {} \ No newline at end of file diff --git a/frontend/appflowy_web/.eslintrc.cjs b/frontend/appflowy_web/.eslintrc.cjs index d6c953795300e..a1160f0bd3f56 100644 --- a/frontend/appflowy_web/.eslintrc.cjs +++ b/frontend/appflowy_web/.eslintrc.cjs @@ -1,18 +1,73 @@ module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], + // 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', - plugins: ['react-refresh'], + parserOptions: { + project: 'tsconfig.json', + sourceType: 'module', + tsconfigRootDir: __dirname, + extraFileExtensions: ['.json'], + }, + plugins: ['@typescript-eslint', "react-hooks"], rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, + "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/package.json b/frontend/appflowy_web/package.json index 97789f0f82d7f..faafdba1540a4 100644 --- a/frontend/appflowy_web/package.json +++ b/frontend/appflowy_web/package.json @@ -10,6 +10,7 @@ "build": "tsc && vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "clean": "cargo make --cwd .. web_clean", + "test": "cargo test && wasm-pack test --headless", "preview": "vite preview" }, "dependencies": { @@ -34,6 +35,7 @@ "eslint-plugin-react-refresh": "^0.4.5", "typescript": "^5.2.2", "vite": "^5.0.8", - "vite-plugin-wasm": "^3.3.0" + "vite-plugin-wasm": "^3.3.0", + "rimraf": "^5.0.5" } } diff --git a/frontend/appflowy_web/pnpm-lock.yaml b/frontend/appflowy_web/pnpm-lock.yaml index 19299b3caf069..3c23a1ff65c1f 100644 --- a/frontend/appflowy_web/pnpm-lock.yaml +++ b/frontend/appflowy_web/pnpm-lock.yaml @@ -54,6 +54,9 @@ devDependencies: eslint-plugin-react-refresh: specifier: ^0.4.5 version: 0.4.5(eslint@8.55.0) + rimraf: + specifier: ^5.0.5 + version: 5.0.5 typescript: specifier: ^5.2.2 version: 5.2.2 @@ -559,6 +562,18 @@ packages: resolution: {integrity: sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==} dev: true + /@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 + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -610,6 +625,13 @@ packages: fastq: 1.16.0 dev: true + /@pkgjs/parseargs@0.11.0: + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + requiresBuild: true + dev: true + optional: true + /@rollup/rollup-android-arm-eabi@4.9.2: resolution: {integrity: sha512-RKzxFxBHq9ysZ83fn8Iduv3A283K7zPPYuhL/z9CQuyFrjwpErJx0h4aeb/bnJ+q29GRLgJpY66ceQ/Wcsn3wA==} cpu: [arm] @@ -962,6 +984,11 @@ packages: engines: {node: '>=8'} dev: true + /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'} @@ -976,6 +1003,11 @@ packages: color-convert: 2.0.1 dev: true + /ansi-styles@6.2.1: + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} + engines: {node: '>=12'} + dev: true + /argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -996,6 +1028,12 @@ packages: concat-map: 0.0.1 dev: true + /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'} @@ -1112,10 +1150,22 @@ packages: esutils: 2.0.3 dev: true + /eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + dev: true + /electron-to-chromium@1.4.620: resolution: {integrity: sha512-a2fcSHOHrqBJsPNXtf6ZCEZpXrFCcbK1FBxfX3txoqWzNgtEDG1f3M59M98iwxhRW4iMKESnSjbJ310/rkrp0g==} dev: true + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + dev: true + /esbuild@0.19.11: resolution: {integrity: sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==} engines: {node: '>=12'} @@ -1341,6 +1391,14 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} 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 + /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} dev: true @@ -1372,6 +1430,18 @@ packages: is-glob: 4.0.3 dev: true + /glob@10.3.10: + resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + dependencies: + foreground-child: 3.1.1 + jackspeak: 2.3.6 + minimatch: 9.0.3 + minipass: 7.0.4 + path-scurry: 1.10.1 + dev: true + /glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} dependencies: @@ -1459,6 +1529,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1480,6 +1555,15 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true + /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 + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1546,6 +1630,11 @@ packages: js-tokens: 4.0.0 dev: false + /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: @@ -1578,6 +1667,18 @@ packages: brace-expansion: 1.1.11 dev: true + /minimatch@9.0.3: + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /minipass@7.0.4: + resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + engines: {node: '>=16 || 14 >=14.17'} + dev: true + /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} dev: true @@ -1650,6 +1751,14 @@ packages: engines: {node: '>=8'} dev: true + /path-scurry@1.10.1: + resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} + 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'} @@ -1731,6 +1840,14 @@ packages: glob: 7.2.3 dev: true + /rimraf@5.0.5: + resolution: {integrity: sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==} + engines: {node: '>=14'} + hasBin: true + dependencies: + glob: 10.3.10 + dev: true + /rollup@4.9.2: resolution: {integrity: sha512-66RB8OtFKUTozmVEh3qyNfH+b+z2RXBVloqO2KCC/pjFaGaHtxP9fVfOQKPSGXg2mElmjmxjW/fZ7iKrEpMH5Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1789,6 +1906,11 @@ packages: engines: {node: '>=8'} dev: true + /signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + dev: true + /slash@3.0.0: resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} engines: {node: '>=8'} @@ -1799,6 +1921,24 @@ packages: engines: {node: '>=0.10.0'} dev: true + /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 + dev: true + + /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 + /strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -1806,6 +1946,13 @@ packages: ansi-regex: 5.0.1 dev: true + /strip-ansi@7.1.0: + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} + engines: {node: '>=12'} + dependencies: + ansi-regex: 6.0.1 + dev: true + /strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1950,6 +2097,24 @@ packages: isexe: 2.0.0 dev: true + /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 + dev: true + + /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==} dev: true diff --git a/frontend/appflowy_web/src/App.tsx b/frontend/appflowy_web/src/App.tsx index 72ffe19fdb6af..6acdc9a892f19 100644 --- a/frontend/appflowy_web/src/App.tsx +++ b/frontend/appflowy_web/src/App.tsx @@ -5,9 +5,9 @@ import { useEffect } from "react"; import { initApp } from "@/application/app.ts"; import { subscribeNotification } from "@/application/notification.ts"; import { NotifyArgs } from "./@types/global"; -import {init_tracing_log, init_wasm_core} from "../wasm-libs/af-wasm/pkg"; +import { init_tracing_log, init_wasm_core } from "../wasm-libs/af-wasm/pkg"; import { v4 as uuidv4 } from 'uuid'; -import {AddUserPB, UserWasmEventAddUser} from "@/services/backend/events/af-user"; +import {AddUserPB, UserWasmEventAddUser} from "@/services/backend/events/user"; init_tracing_log(); // FIXME: handle the promise that init_wasm_core returns diff --git a/frontend/appflowy_web/src/services/backend/index.ts b/frontend/appflowy_web/src/services/backend/index.ts index 1b3fa9424a2ba..d01244a3b15fa 100644 --- a/frontend/appflowy_web/src/services/backend/index.ts +++ b/frontend/appflowy_web/src/services/backend/index.ts @@ -1,2 +1,5 @@ export * from "./models/af-user"; export * from "./models/flowy-error"; +export * from "./models/flowy-folder"; +export * from "./models/flowy-document"; +export * from "./models/flowy-notification"; \ No newline at end of file diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 8eb2d45239eba..8b15b1a27fab3 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -45,7 +45,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cipher", "cpufeatures", ] @@ -110,14 +110,18 @@ dependencies = [ name = "af-wasm" version = "0.1.0" dependencies = [ + "af-persistence", "af-user", "collab", "collab-integrate", + "console_error_panic_hook", "flowy-document", "flowy-error", + "flowy-folder", "flowy-notification", "flowy-server", "flowy-server-pub", + "flowy-storage", "flowy-user-pub", "js-sys", "lazy_static", @@ -136,6 +140,7 @@ dependencies = [ "wasm-bindgen-futures", "wasm-bindgen-test", "web-sys", + "wee_alloc", ] [[package]] @@ -155,7 +160,7 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "getrandom 0.2.12", "once_cell", "version_check", @@ -284,7 +289,7 @@ checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", - "cfg-if", + "cfg-if 1.0.0", "libc", "miniz_oxide", "object", @@ -467,6 +472,12 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + [[package]] name = "cfg-if" version = "1.0.0" @@ -771,7 +782,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen", ] @@ -840,7 +851,7 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -917,7 +928,7 @@ version = "5.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "hashbrown", "lock_api", "once_cell", @@ -1054,7 +1065,7 @@ version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -1295,6 +1306,39 @@ dependencies = [ "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" @@ -1577,7 +1621,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.9.0+wasi-snapshot-preview1", @@ -1590,7 +1634,7 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", @@ -1955,7 +1999,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6cc2083760572ee02385ab8b7c02c20925d2dd1f97a1a25a8737a238608f1152" dependencies = [ "accessory", - "cfg-if", + "cfg-if 1.0.0", "delegate-display", "fancy_constructor", "js-sys", @@ -2002,7 +2046,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", ] [[package]] @@ -2141,7 +2185,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "windows-sys 0.48.0", ] @@ -2231,7 +2275,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd94d5da95b30ae6e10621ad02340909346ad91661f3f8c0f2b62345e46a2f67" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "proc-macro2", "quote", "syn 2.0.48", @@ -2293,6 +2337,12 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + [[package]] name = "mime" version = "0.3.17" @@ -2452,7 +2502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15c9d69dd87a29568d4d017cfe8ec518706046a05184e5aea92d0af890b803c8" dependencies = [ "bitflags 2.4.2", - "cfg-if", + "cfg-if 1.0.0", "foreign-types", "libc", "once_cell", @@ -2536,7 +2586,7 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "instant", "libc", "redox_syscall 0.2.16", @@ -2550,7 +2600,7 @@ version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "libc", "redox_syscall 0.4.1", "smallvec", @@ -2853,7 +2903,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "opaque-debug", "universal-hash", @@ -3621,7 +3671,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", ] @@ -3638,7 +3688,7 @@ version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "cpufeatures", "digest", ] @@ -3875,7 +3925,7 @@ version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "01ce4141aa927a6d1bd34a041795abd0db1cccba5d5f24b009f694bdf3a1f3fa" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "fastrand", "redox_syscall 0.4.1", "rustix", @@ -3971,7 +4021,7 @@ version = "1.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "once_cell", ] @@ -4465,7 +4515,7 @@ version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "wasm-bindgen-macro", ] @@ -4490,7 +4540,7 @@ version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "js-sys", "wasm-bindgen", "web-sys", @@ -4611,6 +4661,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + [[package]] name = "which" version = "4.4.2" @@ -4801,7 +4863,7 @@ version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "cfg-if", + "cfg-if 1.0.0", "windows-sys 0.48.0", ] diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 766213c6a9340..9c333f613345f 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -24,6 +24,8 @@ flowy-error = { path = "../../rust-lib/flowy-error" } flowy-derive = { path = "../../rust-lib/build-tool/flowy-derive" } flowy-codegen = { path = "../../rust-lib/build-tool/flowy-codegen" } flowy-document = { path = "../../rust-lib/flowy-document" } +flowy-folder = { path = "../../rust-lib/flowy-folder" } +flowy-storage = { path = "../../rust-lib/flowy-storage" } lib-infra = { path = "../../rust-lib/lib-infra" } collab = { version = "0.1.0" } collab-entity = { version = "0.1.0" } @@ -40,6 +42,7 @@ wasm-bindgen-futures = "0.4.40" serde-wasm-bindgen = "0.4" + [profile.dev] opt-level = 0 lto = false @@ -50,11 +53,6 @@ lto = true opt-level = 3 codegen-units = 1 -[profile.profiling] -inherits = "release" -debug = true -codegen-units = 16 -lto = false [patch.crates-io] # Please using the following command to update the revision id diff --git a/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml b/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml index 8ce5683877100..14825ef54a4b5 100644 --- a/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/af-persistence/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -crate-type = ["cdylib", "rlib"] [dependencies] indexed_db_futures = { version = "0.4" } diff --git a/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml b/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml index 2d7cb319807f0..e92c6021a3ac3 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/af-user/Cargo.toml @@ -4,8 +4,6 @@ version = "0.1.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[lib] -crate-type = ["cdylib", "rlib"] [dependencies] af-persistence.workspace = true diff --git a/frontend/appflowy_web/wasm-libs/af-user/build.rs b/frontend/appflowy_web/wasm-libs/af-user/build.rs index 2c47f8f2d26fa..11b55130978b8 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/build.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/build.rs @@ -1,16 +1,15 @@ use flowy_codegen::Project; fn main() { - let crate_name = env!("CARGO_PKG_NAME"); - flowy_codegen::protobuf_file::ts_gen( - crate_name, + env!("CARGO_PKG_NAME"), + "user", Project::Web { relative_path: "../../../".to_string(), }, ); flowy_codegen::ts_event::gen( - crate_name, + "user", Project::Web { relative_path: "../../../".to_string(), }, diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs b/frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs new file mode 100644 index 0000000000000..57ba448081b05 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-user/src/authenticate_user.rs @@ -0,0 +1,20 @@ +use af_persistence::store::AppFlowyWASMStore; +use flowy_error::FlowyResult; +use flowy_user_pub::session::Session; +use std::rc::Rc; +use std::sync::Arc; +use tokio::sync::RwLock; + +pub struct AuthenticateUser { + session: Arc>>, + store: Rc, +} + +impl AuthenticateUser { + pub async fn new(store: Rc) -> FlowyResult { + Ok(Self { + session: Arc::new(RwLock::new(None)), + store, + }) + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs b/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs index 54291cc48600a..401f1e98ab80f 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/event_handler.rs @@ -1,5 +1,5 @@ use crate::entities::*; -use crate::manager::UserManagerWASM; +use crate::manager::UserManager; use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use lib_infra::box_any::BoxAny; @@ -8,7 +8,7 @@ use std::rc::{Rc, Weak}; #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn oauth_sign_in_handler( data: AFPluginData, - manager: AFPluginState>, + manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); @@ -19,7 +19,7 @@ pub async fn oauth_sign_in_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn add_user_handler( data: AFPluginData, - manager: AFPluginState>, + manager: AFPluginState>, ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.into_inner(); @@ -30,7 +30,7 @@ pub async fn add_user_handler( #[tracing::instrument(level = "debug", skip(data, manager), err)] pub async fn sign_in_with_password_handler( data: AFPluginData, - manager: AFPluginState>, + manager: AFPluginState>, ) -> DataResult { let manager = upgrade_manager(manager)?; let params = data.into_inner(); @@ -40,9 +40,7 @@ pub async fn sign_in_with_password_handler( data_result_ok(UserProfilePB::from(user_profile)) } -fn upgrade_manager( - manager: AFPluginState>, -) -> FlowyResult> { +fn upgrade_manager(manager: AFPluginState>) -> FlowyResult> { let manager = manager .upgrade() .ok_or(FlowyError::internal().with_context("The user session is already drop"))?; diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs b/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs index 302a26aaf7d68..1047760352042 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/event_map.rs @@ -1,12 +1,12 @@ use crate::event_handler::*; -use crate::manager::UserManagerWASM; +use crate::manager::UserManager; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; use std::rc::Weak; use strum_macros::Display; #[rustfmt::skip] -pub fn init(user_manager: Weak) -> AFPlugin { +pub fn init(user_manager: Weak) -> AFPlugin { AFPlugin::new() .name("Flowy-User") .state(user_manager) diff --git a/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs b/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs index 9d9d15ce382f1..d3519c75520eb 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/lib.rs @@ -1,3 +1,4 @@ +pub mod authenticate_user; mod define; pub mod entities; mod event_handler; 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 2d90af078a2f3..4d64464d42051 100644 --- a/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs +++ b/frontend/appflowy_web/wasm-libs/af-user/src/manager.rs @@ -1,3 +1,4 @@ +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; @@ -37,22 +38,26 @@ pub trait UserCallback { ) -> Fut>; } -pub struct UserManagerWASM { +pub struct UserManager { device_id: String, + pub(crate) store: Rc, pub(crate) cloud_services: Rc, pub(crate) collab_builder: Weak, - pub(crate) store: Rc, - user_callbacks: Vec>, + pub(crate) authenticate_user: Rc, #[allow(dead_code)] pub(crate) user_awareness: Rc>>, pub(crate) collab_db: Arc, + + user_callbacks: Vec>, } -impl UserManagerWASM { +impl UserManager { pub async fn new( device_id: &str, + store: Rc, cloud_services: Rc, + authenticate_user: Rc, collab_builder: Weak, ) -> Result { let device_id = device_id.to_string(); @@ -63,6 +68,7 @@ impl UserManagerWASM { cloud_services, collab_builder, store, + authenticate_user, user_callbacks: vec![], user_awareness: Rc::new(Default::default()), collab_db, diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml b/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml index 4f1781dd0572d..7d8167aadb2a9 100644 --- a/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/af-wasm/Cargo.toml @@ -20,12 +20,15 @@ collab-integrate = { workspace = true } tokio-stream.workspace = true af-user.workspace = true +af-persistence.workspace = true +flowy-storage = { workspace = true } flowy-notification = { workspace = true, features = ["web_ts"] } flowy-user-pub = { workspace = true } flowy-server = { workspace = true } flowy-server-pub = { workspace = true } flowy-error = { workspace = true, features = ["impl_from_dispatch_error", "web_ts"] } flowy-document = { workspace = true, features = ["web_ts"] } +flowy-folder = { workspace = true, features = ["web_ts"] } lib-infra = { workspace = true } collab = { workspace = true, features = ["async-plugin"] } web-sys = "0.3" @@ -34,9 +37,23 @@ uuid.workspace = true serde-wasm-bindgen.workspace = true js-sys = "0.3.67" + +# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size +# compared to the default allocator's ~10K. However, it is slower than the default +# allocator, so it's not enabled by default. +wee_alloc = { version = "0.4.2", optional = true } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so it's only enabled +# in debug mode. +[target."cfg(debug_assertions)".dependencies] +console_error_panic_hook = "0.1.5" + [dev-dependencies] wasm-bindgen-test = "0.3.40" tokio = { version = "1.0", features = ["sync"] } [features] +#default = ["wee_alloc"] localhost_dev = [] \ No newline at end of file diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs index 088d8c889ede6..01a371d5e9d99 100644 --- a/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/core.rs @@ -1,8 +1,15 @@ +use crate::deps_resolve::document_deps::DocumentDepsResolver; +use crate::deps_resolve::folder_deps::FolderDepsResolver; use crate::integrate::server::ServerProviderWASM; -use af_user::manager::UserManagerWASM; +use af_persistence::store::AppFlowyWASMStore; +use af_user::authenticate_user::AuthenticateUser; +use af_user::manager::UserManager; use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_document::manager::DocumentManager; use flowy_error::FlowyResult; +use flowy_folder::manager::FolderManager; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; +use flowy_storage::ObjectStorageService; use lib_dispatch::prelude::AFPluginDispatcher; use lib_dispatch::runtime::AFPluginRuntime; use std::rc::Rc; @@ -11,7 +18,9 @@ use std::sync::Arc; pub struct AppFlowyWASMCore { pub collab_builder: Arc, pub event_dispatcher: Rc, - pub user_manager: Rc, + pub user_manager: Rc, + pub folder_manager: Rc, + pub document_manager: Rc, } impl AppFlowyWASMCore { @@ -23,10 +32,31 @@ impl AppFlowyWASMCore { device_id.to_string(), )); + let store = Rc::new(AppFlowyWASMStore::new().await?); + let auth_user = Rc::new(AuthenticateUser::new(store.clone()).await?); + + let document_manager = DocumentDepsResolver::resolve( + Rc::downgrade(&auth_user), + collab_builder.clone(), + server_provider.clone(), + Rc::downgrade(&(server_provider.clone() as Rc)), + ) + .await; + + let folder_manager = FolderDepsResolver::resolve( + Rc::downgrade(&auth_user), + document_manager.clone(), + collab_builder.clone(), + server_provider.clone(), + ) + .await; + let user_manager = Rc::new( - UserManagerWASM::new( + UserManager::new( device_id, + store, server_provider.clone(), + auth_user, Arc::downgrade(&collab_builder), ) .await?, @@ -40,6 +70,8 @@ impl AppFlowyWASMCore { collab_builder, event_dispatcher, user_manager, + folder_manager, + document_manager, }) } } diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs new file mode 100644 index 0000000000000..3580bb762f268 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/document_deps.rs @@ -0,0 +1,19 @@ +use crate::integrate::server::ServerProviderWASM; +use af_user::authenticate_user::AuthenticateUser; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_document::manager::DocumentManager; +use flowy_storage::ObjectStorageService; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +pub struct DocumentDepsResolver; +impl DocumentDepsResolver { + pub async fn resolve( + authenticate_user: Weak, + collab_builder: Arc, + server_provider: Rc, + storage_service: Weak, + ) -> Rc { + todo!() + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs new file mode 100644 index 0000000000000..e291b5551a02d --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/folder_deps.rs @@ -0,0 +1,20 @@ +use crate::integrate::server::ServerProviderWASM; +use af_user::authenticate_user::AuthenticateUser; +use collab_integrate::collab_builder::AppFlowyCollabBuilder; +use flowy_document::manager::DocumentManager; +use flowy_folder::manager::FolderManager; +use std::rc::{Rc, Weak}; +use std::sync::Arc; + +pub struct FolderDepsResolver; + +impl FolderDepsResolver { + pub async fn resolve( + authenticate_user: Weak, + document_manager: Rc, + collab_builder: Arc, + server_provider: Rc, + ) -> Rc { + todo!() + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs new file mode 100644 index 0000000000000..b2105223609d7 --- /dev/null +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/deps_resolve/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod document_deps; +pub(crate) mod folder_deps; 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 ea62fc49fb775..d5263fab06535 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 @@ -6,11 +6,10 @@ use flowy_error::FlowyError; use flowy_server::af_cloud::AppFlowyCloudServer; use flowy_server::AppFlowyServer; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; -use flowy_user_pub::cloud::{ - UserCloudService, UserCloudServiceProvider, UserCloudServiceProviderBase, -}; +use flowy_storage::{ObjectIdentity, ObjectStorageService, ObjectValue}; +use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserTokenState}; -use lib_infra::future::{to_fut, Fut}; +use lib_infra::future::{to_fut, Fut, FutureResult}; use parking_lot::RwLock; use std::rc::Rc; use std::sync::Arc; @@ -64,9 +63,7 @@ impl CollabCloudPluginProvider for ServerProviderWASM { } } -impl UserCloudServiceProvider for ServerProviderWASM {} - -impl UserCloudServiceProviderBase for ServerProviderWASM { +impl UserCloudServiceProvider for ServerProviderWASM { fn set_token(&self, token: &str) -> Result<(), FlowyError> { self.get_server().set_token(token)?; Ok(()) @@ -104,3 +101,21 @@ impl UserCloudServiceProviderBase for ServerProviderWASM { self.config.base_url.clone() } } + +impl ObjectStorageService for ServerProviderWASM { + fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult { + todo!() + } + + fn put_object(&self, url: String, object_value: ObjectValue) -> FutureResult<(), FlowyError> { + todo!() + } + + fn delete_object(&self, url: String) -> FutureResult<(), FlowyError> { + todo!() + } + + fn get_object(&self, url: String) -> FutureResult { + todo!() + } +} diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs b/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs index e0a88cc8cfeb8..ab017366bde9d 100644 --- a/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs +++ b/frontend/appflowy_web/wasm-libs/af-wasm/src/lib.rs @@ -4,6 +4,7 @@ use std::cell::RefCell; use std::rc::Rc; pub mod core; +mod deps_resolve; mod integrate; pub mod notification; @@ -23,6 +24,10 @@ lazy_static! { static ref APPFLOWY_CORE: RefCellAppFlowyCore = RefCellAppFlowyCore::new(); } +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] @@ -37,6 +42,10 @@ pub fn init_tracing_log() { #[wasm_bindgen] pub fn init_wasm_core() -> js_sys::Promise { + // It's disabled in release mode so it doesn't bloat up the file size. + #[cfg(debug_assertions)] + console_error_panic_hook::set_once(); + #[cfg(feature = "localhost_dev")] let config = AFCloudConfiguration { base_url: "http://localhost".to_string(), diff --git a/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs index 3a5edde679763..b8744bdeba6d8 100644 --- a/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs +++ b/frontend/appflowy_web/wasm-libs/af-wasm/tests/util/tester.rs @@ -7,6 +7,11 @@ use flowy_error::FlowyResult; use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use parking_lot::Once; +use flowy_document::deps::DocumentData; +use flowy_document::entities::{CreateDocumentPayloadPB, DocumentDataPB, OpenDocumentPayloadPB}; +use flowy_document::event_map::DocumentEvent; +use flowy_folder::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; +use flowy_folder::event_map::FolderEvent; use std::sync::Arc; use uuid::Uuid; @@ -48,6 +53,44 @@ impl WASMEventTester { .parse::(); Ok(user_profile) } + + pub async fn create_and_open_document(&self, parent_id: &str) -> ViewPB { + let payload = CreateViewPayloadPB { + parent_view_id: parent_id.to_string(), + name, + desc: "".to_string(), + thumbnail: None, + layout: ViewLayoutPB::Document, + initial_data, + meta: Default::default(), + set_as_current: true, + index: None, + }; + let view = self + .event_builder() + .event(FolderEvent::CreateView) + .payload(payload) + .async_send() + .await + .parse::(); + + let payload = OpenDocumentPayloadPB { + document_id: view.id.clone(), + }; + + let _ = self + .event_builder() + .event(DocumentEvent::OpenDocument) + .payload(payload) + .async_send() + .await + .parse::(); + view + } + + fn event_builder(&self) -> EventBuilder { + EventBuilder::new(self.core.clone()) + } } pub fn unique_email() -> String { diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index cb83e6e90cdfb..1030d588b2645 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -1920,6 +1920,7 @@ dependencies = [ name = "flowy-folder" version = "0.1.0" dependencies = [ + "async-trait", "bytes", "chrono", "collab", diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs index ac0cdf071246c..462f5940f785f 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/protobuf_file/mod.rs @@ -76,7 +76,7 @@ pub fn dart_gen(crate_name: &str) { } #[allow(unused_variables)] -pub fn ts_gen(crate_name: &str, project: Project) { +pub fn ts_gen(crate_name: &str, dest_folder_name: &str, project: Project) { // 1. generate the proto files to proto_file_dir #[cfg(feature = "proto_gen")] let proto_crates = gen_proto_files(crate_name); @@ -116,7 +116,7 @@ pub fn ts_gen(crate_name: &str, project: Project) { // 2. generate the protobuf files(Dart) #[cfg(feature = "ts")] generate_ts_protobuf_files( - crate_name, + dest_folder_name, &proto_file_output_path, &proto_file_paths, &file_names, diff --git a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs index 43087c40528fb..9f275115d5a2b 100644 --- a/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs +++ b/frontend/rust-lib/build-tool/flowy-codegen/src/ts_event/mod.rs @@ -13,7 +13,7 @@ use std::path::PathBuf; use syn::Item; use walkdir::WalkDir; -pub fn gen(crate_name: &str, project: Project) { +pub fn gen(dest_folder_name: &str, project: Project) { let root = project.event_root(); let backend_service_path = project.dst(); @@ -40,7 +40,7 @@ pub fn gen(crate_name: &str, project: Project) { } render_result.push_str(TS_FOOTER); - let ts_event_folder: PathBuf = [&root, &backend_service_path, "events", crate_name] + let ts_event_folder: PathBuf = [&root, &backend_service_path, "events", dest_folder_name] .iter() .collect(); if !ts_event_folder.as_path().exists() { @@ -82,7 +82,10 @@ pub fn gen(crate_name: &str, project: Project) { Ok(ref mut file) => { let mut export = String::new(); export.push_str("// Auto-generated, do not edit \n"); - export.push_str(&format!("export * from '../../models/{}';\n", crate_name)); + export.push_str(&format!( + "export * from '../../models/{}';\n", + dest_folder_name + )); export.push_str(&format!("export * from './{}';\n", event_file)); file.write_all(export.as_bytes()).unwrap(); File::flush(file).unwrap(); diff --git a/frontend/rust-lib/dart-ffi/binding.h b/frontend/rust-lib/dart-ffi/binding.h index 710af508fd51e..3fe1f39faaf71 100644 --- a/frontend/rust-lib/dart-ffi/binding.h +++ b/frontend/rust-lib/dart-ffi/binding.h @@ -3,7 +3,7 @@ #include #include -int64_t init_sdk(char *data); +int64_t init_sdk(int64_t port, char *data); void async_event(int64_t port, const uint8_t *input, uintptr_t len); diff --git a/frontend/rust-lib/dart-ffi/src/lib.rs b/frontend/rust-lib/dart-ffi/src/lib.rs index 6f3c06064fed8..e7c536822778e 100644 --- a/frontend/rust-lib/dart-ffi/src/lib.rs +++ b/frontend/rust-lib/dart-ffi/src/lib.rs @@ -13,6 +13,7 @@ use flowy_notification::{register_notification_sender, unregister_all_notificati use flowy_server_pub::AuthenticatorType; use lib_dispatch::prelude::ToBytes; use lib_dispatch::prelude::*; +use lib_dispatch::runtime::AFPluginRuntime; use crate::appflowy_yaml::save_appflowy_cloud_config; use crate::env_serde::AppFlowyDartConfiguration; @@ -52,7 +53,9 @@ unsafe impl Sync for MutexAppFlowyCore {} unsafe impl Send for MutexAppFlowyCore {} #[no_mangle] -pub extern "C" fn init_sdk(data: *mut c_char) -> i64 { +pub extern "C" fn init_sdk(_port: i64, data: *mut c_char) -> i64 { + // and sent it the `Rust's` result + // no need to convert anything :) let c_str = unsafe { CStr::from_ptr(data) }; let serde_str = c_str.to_str().unwrap(); let configuration = AppFlowyDartConfiguration::from_str(serde_str); @@ -78,7 +81,13 @@ pub extern "C" fn init_sdk(data: *mut c_char) -> i64 { core.close_db(); } - *APPFLOWY_CORE.0.lock() = Some(AppFlowyCore::new(config)); + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + // let isolate = allo_isolate::Isolate::new(port); + *APPFLOWY_CORE.0.lock() = runtime.block_on(async move { + Some(AppFlowyCore::new(config, cloned_runtime).await) + // isolate.post("".to_string()); + }); 0 } diff --git a/frontend/rust-lib/event-integration/src/lib.rs b/frontend/rust-lib/event-integration/src/lib.rs index 06057937acef6..a91125ca54a17 100644 --- a/frontend/rust-lib/event-integration/src/lib.rs +++ b/frontend/rust-lib/event-integration/src/lib.rs @@ -19,6 +19,7 @@ use flowy_notification::register_notification_sender; use flowy_server::AppFlowyServer; use flowy_user::entities::AuthenticatorPB; use flowy_user::errors::FlowyError; +use lib_dispatch::runtime::AFPluginRuntime; use crate::user_event::TestNotificationSender; @@ -134,19 +135,14 @@ pub fn document_from_document_doc_state(doc_id: &str, doc_state: CollabDocState) Document::from_doc_state(CollabOrigin::Empty, doc_state, doc_id, vec![]).unwrap() } -#[cfg(target_arch = "wasm32")] async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { - // let runtime = tokio::runtime::Runtime::new().unwrap(); - // let local_set = tokio::task::LocalSet::new(); - // runtime.block_on(AppFlowyCore::new(config)) - AppFlowyCore::new(config).await -} - -#[cfg(not(target_arch = "wasm32"))] -async fn init_core(config: AppFlowyCoreConfig) -> AppFlowyCore { - std::thread::spawn(|| AppFlowyCore::new(config)) - .join() - .unwrap() + std::thread::spawn(|| { + let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + let cloned_runtime = runtime.clone(); + runtime.block_on(async move { AppFlowyCore::new(config, cloned_runtime).await }) + }) + .join() + .unwrap() } impl std::ops::Deref for EventIntegrationTest { diff --git a/frontend/rust-lib/flowy-config/build.rs b/frontend/rust-lib/flowy-config/build.rs index f1036a135d35a..84b506ee31c8b 100644 --- a/frontend/rust-lib/flowy-config/build.rs +++ b/frontend/rust-lib/flowy-config/build.rs @@ -8,6 +8,10 @@ fn main() { #[cfg(feature = "tauri_ts")] { flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_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::Tauri, + ); } } diff --git a/frontend/rust-lib/flowy-core/Cargo.toml b/frontend/rust-lib/flowy-core/Cargo.toml index be19eb845bd22..59ddd300d15ad 100644 --- a/frontend/rust-lib/flowy-core/Cargo.toml +++ b/frontend/rust-lib/flowy-core/Cargo.toml @@ -60,9 +60,9 @@ dart = [ ] ts = [ "flowy-user/tauri_ts", - "flowy-folder/ts", + "flowy-folder/tauri_ts", "flowy-database2/ts", "flowy-config/tauri_ts", ] rev-sqlite = ["flowy-user/rev-sqlite"] -openssl_vendored = ["flowy-sqlite/openssl_vendored"] +openssl_vendored = ["flowy-sqlite/openssl_vendored"] \ No newline at end of file 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 7970f1d924361..0560c3c480931 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/trait_impls.rs @@ -24,9 +24,7 @@ use flowy_folder_pub::cloud::{ use flowy_server_pub::af_cloud_config::AFCloudConfiguration; use flowy_server_pub::supabase_config::SupabaseConfiguration; use flowy_storage::ObjectValue; -use flowy_user_pub::cloud::{ - UserCloudService, UserCloudServiceProvider, UserCloudServiceProviderBase, -}; +use flowy_user_pub::cloud::{UserCloudService, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserTokenState}; use lib_infra::future::{to_fut, Fut, FutureResult}; @@ -65,9 +63,8 @@ impl ObjectStorageService for ServerProvider { }) } } -impl UserCloudServiceProvider for ServerProvider {} -impl UserCloudServiceProviderBase for ServerProvider { +impl UserCloudServiceProvider for ServerProvider { fn set_token(&self, token: &str) -> Result<(), FlowyError> { let server = self.get_server()?; server.set_token(token)?; diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index 5caf2374b270f..1959c2dd6e425 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -10,7 +10,7 @@ use flowy_document::manager::DocumentManager; use flowy_error::FlowyResult; use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_user::event_map::UserStatusCallback; -use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProviderBase}; +use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; use flowy_user_pub::entities::{Authenticator, UserProfile, UserWorkspace}; use lib_infra::future::{to_fut, Fut}; diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 8df443e1156db..1d9a34e28b2d6 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -2,9 +2,7 @@ use flowy_storage::ObjectStorageService; use std::sync::Arc; - use std::time::Duration; - use tokio::sync::RwLock; use tracing::{debug, error, event, info, instrument}; @@ -53,19 +51,10 @@ pub struct AppFlowyCore { } impl AppFlowyCore { - #[cfg(target_arch = "wasm32")] - pub async fn new(config: AppFlowyCoreConfig) -> Self { - let runtime = Arc::new(AFPluginRuntime::new().unwrap()); + pub async fn new(config: AppFlowyCoreConfig, runtime: Arc) -> Self { Self::init(config, runtime).await } - #[cfg(not(target_arch = "wasm32"))] - pub fn new(config: AppFlowyCoreConfig) -> Self { - let runtime = Arc::new(AFPluginRuntime::new().unwrap()); - let cloned_runtime = runtime.clone(); - runtime.block_on(Self::init(config, cloned_runtime)) - } - pub fn close_db(&self) { self.user_manager.close_db(); } diff --git a/frontend/rust-lib/flowy-database2/build.rs b/frontend/rust-lib/flowy-database2/build.rs index 4a3b6113aee84..7d0351bcf6392 100644 --- a/frontend/rust-lib/flowy-database2/build.rs +++ b/frontend/rust-lib/flowy-database2/build.rs @@ -9,6 +9,6 @@ fn main() { #[cfg(feature = "ts")] { flowy_codegen::ts_event::gen(crate_name, flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_gen(crate_name, flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen(crate_name, crate_name, flowy_codegen::Project::Tauri); } } diff --git a/frontend/rust-lib/flowy-date/build.rs b/frontend/rust-lib/flowy-date/build.rs index f1036a135d35a..84b506ee31c8b 100644 --- a/frontend/rust-lib/flowy-date/build.rs +++ b/frontend/rust-lib/flowy-date/build.rs @@ -8,6 +8,10 @@ fn main() { #[cfg(feature = "tauri_ts")] { flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_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::Tauri, + ); } } diff --git a/frontend/rust-lib/flowy-document/build.rs b/frontend/rust-lib/flowy-document/build.rs index 4585268158d30..c3d11111cb6b3 100644 --- a/frontend/rust-lib/flowy-document/build.rs +++ b/frontend/rust-lib/flowy-document/build.rs @@ -8,19 +8,24 @@ fn main() { #[cfg(feature = "tauri_ts")] { flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_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::Tauri, + ); } #[cfg(feature = "web_ts")] { flowy_codegen::ts_event::gen( - env!("CARGO_PKG_NAME"), + "document", flowy_codegen::Project::Web { relative_path: "../../".to_string(), }, ); flowy_codegen::protobuf_file::ts_gen( env!("CARGO_PKG_NAME"), + "document", flowy_codegen::Project::Web { relative_path: "../../".to_string(), }, diff --git a/frontend/rust-lib/flowy-error/build.rs b/frontend/rust-lib/flowy-error/build.rs index 9e893a2f93456..47839f938f169 100644 --- a/frontend/rust-lib/flowy-error/build.rs +++ b/frontend/rust-lib/flowy-error/build.rs @@ -3,11 +3,16 @@ 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"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); #[cfg(feature = "web_ts")] flowy_codegen::protobuf_file::ts_gen( env!("CARGO_PKG_NAME"), + "error", flowy_codegen::Project::Web { relative_path: "../../".to_string(), }, diff --git a/frontend/rust-lib/flowy-folder/Cargo.toml b/frontend/rust-lib/flowy-folder/Cargo.toml index 98b5fa1b082c3..757e9bfff7221 100644 --- a/frontend/rust-lib/flowy-folder/Cargo.toml +++ b/frontend/rust-lib/flowy-folder/Cargo.toml @@ -22,7 +22,7 @@ flowy-error = { path = "../flowy-error", features = ["impl_from_dispatch_error"] lib-dispatch = { workspace = true } bytes.workspace = true lib-infra = { workspace = true } -tokio = { workspace = true, features = ["full"] } +tokio = { workspace = true, features = ["sync"] } nanoid = "0.4.0" lazy_static = "1.4.0" chrono = { workspace = true, default-features = false, features = ["clock"] } @@ -32,11 +32,13 @@ uuid.workspace = true tokio-stream = { workspace = true, features = ["sync"] } serde_json.workspace = true validator = "0.16.0" +async-trait.workspace = true [build-dependencies] flowy-codegen.workspace = true [features] dart = ["flowy-codegen/dart", "flowy-notification/dart"] -ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] +tauri_ts = ["flowy-codegen/ts", "flowy-notification/tauri_ts"] +web_ts = ["flowy-codegen/ts", "flowy-notification/web_ts"] test_helper = [] diff --git a/frontend/rust-lib/flowy-folder/build.rs b/frontend/rust-lib/flowy-folder/build.rs index 343a1a21e3129..0ea0f628f7bbc 100644 --- a/frontend/rust-lib/flowy-folder/build.rs +++ b/frontend/rust-lib/flowy-folder/build.rs @@ -5,9 +5,30 @@ fn main() { flowy_codegen::dart_event::gen(env!("CARGO_PKG_NAME")); } - #[cfg(feature = "ts")] + #[cfg(feature = "tauri_ts")] { flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_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::Tauri, + ); + } + + #[cfg(feature = "web_ts")] + { + flowy_codegen::ts_event::gen( + "folder", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + "folder", + flowy_codegen::Project::Web { + relative_path: "../../".to_string(), + }, + ); } } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 701e36a8da83d..72fbd27154f3d 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -15,7 +15,7 @@ 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::async_trait::async_trait; +use lib_infra::conditional_send_sync_trait; use crate::entities::icon::UpdateViewIconParams; use crate::entities::{ @@ -35,11 +35,12 @@ use crate::util::{ }; use crate::view_operation::{create_view, FolderOperationHandler, FolderOperationHandlers}; -/// [FolderUser] represents the user for folder. -#[async_trait] -pub trait FolderUser: Send + Sync { - fn user_id(&self) -> Result; - fn collab_db(&self, uid: i64) -> Result, FlowyError>; +conditional_send_sync_trait! { + "[crate::manager::FolderUser] represents the user for folder."; + FolderUser { + fn user_id(&self) -> Result; + fn collab_db(&self, uid: i64) -> Result, FlowyError>; + } } pub struct FolderManager { diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index c40394e220737..b83ef2fc5a9a7 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -12,7 +12,6 @@ use crate::manager_observer::{ subscribe_folder_trash_changed, subscribe_folder_view_changed, }; use crate::user_default::DefaultFolderBuilder; -use crate::util::is_exist_in_local_disk; impl FolderManager { /// Called immediately after the application launched if the user already sign in/sign up. @@ -47,7 +46,7 @@ impl FolderManager { FolderInitDataSource::LocalDisk { create_if_not_exist, } => { - let is_exist = is_exist_in_local_disk(&self.user, &workspace_id).unwrap_or(false); + let is_exist = self.is_workspace_exist_in_local(uid, &workspace_id).await; if is_exist { self .open_local_folder(uid, &workspace_id, collab_db, folder_notifier) @@ -104,6 +103,15 @@ impl FolderManager { Ok(()) } + async fn is_workspace_exist_in_local(&self, uid: i64, workspace_id: &str) -> bool { + if let Ok(weak_collab) = self.user.collab_db(uid) { + if let Some(collab_db) = weak_collab.upgrade() { + return collab_db.is_exist(uid, workspace_id).await.unwrap_or(false); + } + } + false + } + async fn create_default_folder( &self, uid: i64, diff --git a/frontend/rust-lib/flowy-folder/src/util.rs b/frontend/rust-lib/flowy-folder/src/util.rs index f056cfd90e88e..a56db3351179a 100644 --- a/frontend/rust-lib/flowy-folder/src/util.rs +++ b/frontend/rust-lib/flowy-folder/src/util.rs @@ -1,31 +1,13 @@ +use crate::entities::UserFolderPB; use collab_folder::Folder; -use collab_integrate::CollabKVAction; -use collab_plugins::local_storage::kv::KVTransactionDB; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{ErrorCode, FlowyError}; use flowy_folder_pub::folder_builder::ParentChildViews; -use std::sync::Arc; use tracing::{event, instrument}; -use crate::entities::UserFolderPB; -use crate::manager::FolderUser; - pub(crate) fn folder_not_init_error() -> FlowyError { FlowyError::internal().with_context("Folder not initialized") } -pub(crate) fn is_exist_in_local_disk( - user: &Arc, - doc_id: &str, -) -> FlowyResult { - let uid = user.user_id()?; - if let Some(collab_db) = user.collab_db(uid)?.upgrade() { - let read_txn = collab_db.read_txn(); - Ok(read_txn.is_exist(uid, doc_id)) - } else { - Ok(false) - } -} - pub(crate) fn workspace_data_not_sync_error(uid: i64, workspace_id: &str) -> FlowyError { FlowyError::from(ErrorCode::WorkspaceDataNotSync).with_payload(UserFolderPB { uid, diff --git a/frontend/rust-lib/flowy-notification/build.rs b/frontend/rust-lib/flowy-notification/build.rs index 9e893a2f93456..acacab7e88e66 100644 --- a/frontend/rust-lib/flowy-notification/build.rs +++ b/frontend/rust-lib/flowy-notification/build.rs @@ -3,11 +3,16 @@ 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"), flowy_codegen::Project::Tauri); + flowy_codegen::protobuf_file::ts_gen( + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_NAME"), + flowy_codegen::Project::Tauri, + ); #[cfg(feature = "web_ts")] flowy_codegen::protobuf_file::ts_gen( env!("CARGO_PKG_NAME"), + "notification", flowy_codegen::Project::Web { relative_path: "../../".to_string(), }, diff --git a/frontend/rust-lib/flowy-server/src/lib.rs b/frontend/rust-lib/flowy-server/src/lib.rs index c06a43d334a84..4e647f4210e29 100644 --- a/frontend/rust-lib/flowy-server/src/lib.rs +++ b/frontend/rust-lib/flowy-server/src/lib.rs @@ -2,7 +2,6 @@ pub use server::*; pub mod af_cloud; pub mod local_server; -// mod request; mod response; mod server; diff --git a/frontend/rust-lib/flowy-server/src/request.rs b/frontend/rust-lib/flowy-server/src/request.rs deleted file mode 100644 index eca98bbcfdf5c..0000000000000 --- a/frontend/rust-lib/flowy-server/src/request.rs +++ /dev/null @@ -1,198 +0,0 @@ -use std::{sync::Arc, time::Duration}; - -use bytes::Bytes; -use hyper::http; -use reqwest::{header::HeaderMap, Client, Method, Response}; -use tokio::sync::oneshot; - -use flowy_error::{internal_error, FlowyError}; - -use crate::af_cloud::configuration::HEADER_TOKEN; -use crate::response::HttpResponse; - -pub trait ResponseMiddleware { - fn receive_response(&self, token: &Option, response: &HttpResponse); -} - -pub struct HttpRequestBuilder { - url: String, - body: Option, - response: Option, - headers: HeaderMap, - method: Method, - middleware: Vec>, -} - -impl std::default::Default for HttpRequestBuilder { - fn default() -> Self { - Self { - url: "".to_owned(), - body: None, - response: None, - headers: HeaderMap::new(), - method: Method::GET, - middleware: Vec::new(), - } - } -} - -impl HttpRequestBuilder { - pub fn new() -> Self { - HttpRequestBuilder::default() - } - - #[allow(dead_code)] - pub fn middleware(mut self, middleware: Arc) -> Self - where - T: 'static + ResponseMiddleware + Send + Sync, - { - self.middleware.push(middleware); - self - } - - pub fn get(mut self, url: &str) -> Self { - self.url = url.to_owned(); - self.method = Method::GET; - self - } - - pub fn post(mut self, url: &str) -> Self { - self.url = url.to_owned(); - self.method = Method::POST; - self - } - - pub fn patch(mut self, url: &str) -> Self { - self.url = url.to_owned(); - self.method = Method::PATCH; - self - } - - pub fn delete(mut self, url: &str) -> Self { - self.url = url.to_owned(); - self.method = Method::DELETE; - self - } - - pub fn header(mut self, key: &'static str, value: &str) -> Self { - self.headers.insert(key, value.parse().unwrap()); - self - } - - pub fn json(self, body: T) -> Result - where - T: serde::Serialize, - { - let bytes = Bytes::from(serde_json::to_vec(&body)?); - self.bytes(bytes) - } - - pub fn bytes(mut self, body: Bytes) -> Result { - self.body = Some(body); - Ok(self) - } - - pub async fn send(self) -> Result<(), FlowyError> { - let _ = self.inner_send().await?; - Ok(()) - } - - pub async fn response(self) -> Result - where - T: serde::de::DeserializeOwned, - { - let builder = self.inner_send().await?; - match builder.response { - None => Err(unexpected_empty_payload(&builder.url)), - Some(data) => { - let value = serde_json::from_slice(&data)?; - Ok(value) - }, - } - } - - fn token(&self) -> Option { - match self.headers.get(HEADER_TOKEN) { - None => None, - Some(header) => match header.to_str() { - Ok(val) => Some(val.to_owned()), - Err(_) => None, - }, - } - } - - async fn inner_send(mut self) -> Result { - let (tx, rx) = oneshot::channel::>(); - let url = self.url.clone(); - let body = self.body.take(); - let method = self.method.clone(); - let headers = self.headers.clone(); - - // reqwest client is not 'Sync' but channel is. - tokio::spawn(async move { - let client = default_client(); - let mut builder = client.request(method.clone(), url).headers(headers); - if let Some(body) = body { - builder = builder.body(body); - } - - let response = builder.send().await; - let _ = tx.send(response); - }); - - let response = rx.await.map_err(internal_error)?; - tracing::trace!("Http Response: {:?}", response); - let flowy_response = flowy_response_from(response?).await?; - let token = self.token(); - self.middleware.iter().for_each(|middleware| { - middleware.receive_response(&token, &flowy_response); - }); - match flowy_response.error { - None => { - self.response = Some(flowy_response.data); - Ok(self) - }, - Some(error) => Err(FlowyError::new(error.code, &error.msg)), - } - } -} - -fn unexpected_empty_payload(url: &str) -> FlowyError { - let msg = format!("Request: {} receives unexpected empty payload", url); - FlowyError::payload_none().with_context(msg) -} - -async fn flowy_response_from(original: Response) -> Result { - let bytes = original.bytes().await?; - let response: HttpResponse = serde_json::from_slice(&bytes)?; - Ok(response) -} - -#[allow(dead_code)] -async fn get_response_data(original: Response) -> Result { - if original.status() == http::StatusCode::OK { - let bytes = original.bytes().await?; - let response: HttpResponse = serde_json::from_slice(&bytes)?; - match response.error { - None => Ok(response.data), - Some(error) => Err(FlowyError::new(error.code, &error.msg)), - } - } else { - Err(FlowyError::http().with_context(original)) - } -} - -fn default_client() -> Client { - let result = reqwest::Client::builder() - .connect_timeout(Duration::from_millis(500)) - .timeout(Duration::from_secs(5)) - .build(); - - match result { - Ok(client) => client, - Err(e) => { - tracing::error!("Create reqwest client failed: {}", e); - reqwest::Client::new() - }, - } -} diff --git a/frontend/rust-lib/flowy-storage/src/lib.rs b/frontend/rust-lib/flowy-storage/src/lib.rs index 8f0d02dfada31..b318b55cb5e08 100644 --- a/frontend/rust-lib/flowy-storage/src/lib.rs +++ b/frontend/rust-lib/flowy-storage/src/lib.rs @@ -12,7 +12,7 @@ use bytes::Bytes; use flowy_error::FlowyError; use lib_infra::future::FutureResult; -use lib_infra::{if_native, if_wasm}; +use lib_infra::{conditional_send_sync_trait, if_native, if_wasm}; use mime::Mime; pub struct ObjectIdentity { @@ -26,50 +26,49 @@ pub struct ObjectValue { pub raw: Bytes, pub mime: Mime, } - -/// Provides a service for object storage. -/// -/// The trait includes methods for CRUD operations on storage objects. -pub trait ObjectStorageService: Send + Sync + 'static { - /// Creates a new storage object. - /// - /// # Parameters - /// - `url`: url of the object to be created. - /// - /// # Returns - /// - `Ok()` - /// - `Err(Error)`: An error occurred during the operation. - fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult; - - /// Creates a new storage object. - /// - /// # Parameters - /// - `url`: url of the object to be created. - /// - /// # Returns - /// - `Ok()` - /// - `Err(Error)`: An error occurred during the operation. - fn put_object(&self, url: String, object_value: ObjectValue) -> FutureResult<(), FlowyError>; - - /// Deletes a storage object by its URL. - /// - /// # Parameters - /// - `url`: url of the object to be deleted. - /// - /// # Returns - /// - `Ok()` - /// - `Err(Error)`: An error occurred during the operation. - fn delete_object(&self, url: String) -> FutureResult<(), FlowyError>; - - /// Fetches a storage object by its URL. - /// - /// # Parameters - /// - `url`: url of the object - /// - /// # Returns - /// - `Ok(File)`: The returned file object. - /// - `Err(Error)`: An error occurred during the operation. - fn get_object(&self, url: String) -> FutureResult; +conditional_send_sync_trait! { + "Provides a service for object storage. The trait includes methods for CRUD operations on storage objects."; + ObjectStorageService { + /// Creates a new storage object. + /// + /// # Parameters + /// - `url`: url of the object to be created. + /// + /// # Returns + /// - `Ok()` + /// - `Err(Error)`: An error occurred during the operation. + fn get_object_url(&self, object_id: ObjectIdentity) -> FutureResult; + + /// Creates a new storage object. + /// + /// # Parameters + /// - `url`: url of the object to be created. + /// + /// # Returns + /// - `Ok()` + /// - `Err(Error)`: An error occurred during the operation. + fn put_object(&self, url: String, object_value: ObjectValue) -> FutureResult<(), FlowyError>; + + /// Deletes a storage object by its URL. + /// + /// # Parameters + /// - `url`: url of the object to be deleted. + /// + /// # Returns + /// - `Ok()` + /// - `Err(Error)`: An error occurred during the operation. + fn delete_object(&self, url: String) -> FutureResult<(), FlowyError>; + + /// Fetches a storage object by its URL. + /// + /// # Parameters + /// - `url`: url of the object + /// + /// # Returns + /// - `Ok(File)`: The returned file object. + /// - `Err(Error)`: An error occurred during the operation. + fn get_object(&self, url: String) -> FutureResult; + } } pub trait FileStoragePlan: Send + Sync + 'static { diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index e5e37ed5e55b1..41d71f1efb98f 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -14,8 +14,8 @@ use uuid::Uuid; use flowy_error::{ErrorCode, FlowyError}; use lib_infra::box_any::BoxAny; +use lib_infra::conditional_send_sync_trait; use lib_infra::future::FutureResult; -use lib_infra::{if_native, if_wasm}; use crate::entities::{ AuthResponse, Authenticator, Role, UpdateUserProfileParams, UserCredentials, UserProfile, @@ -57,21 +57,12 @@ impl Display for UserCloudConfig { } } -if_native! { -pub trait UserCloudServiceProvider: UserCloudServiceProviderBase + Send + Sync + 'static {} -} - -if_wasm! { -pub trait UserCloudServiceProvider: UserCloudServiceProviderBase + 'static {} -} +conditional_send_sync_trait! { + "This trait is intended for implementation by providers that offer cloud-based services for users. + It includes methods for handling authentication tokens, enabling/disabling synchronization, + setting network reachability, managing encryption secrets, and accessing user-specific cloud services."; -/// `UserCloudServiceProvider` defines a set of methods for managing user cloud services, -/// including token management, synchronization settings, network reachability, and authentication. -/// -/// This trait is intended for implementation by providers that offer cloud-based services for users. -/// It includes methods for handling authentication tokens, enabling/disabling synchronization, -/// setting network reachability, managing encryption secrets, and accessing user-specific cloud services. -pub trait UserCloudServiceProviderBase { + UserCloudServiceProvider { /// Sets the authentication token for the cloud service. /// /// # Arguments @@ -126,8 +117,8 @@ pub trait UserCloudServiceProviderBase { /// # Returns /// A `String` representing the service URL. fn service_url(&self) -> String; + } } - /// Provide the generic interface for the user cloud service /// The user cloud service is responsible for the user authentication and user profile management #[allow(unused_variables)] diff --git a/frontend/rust-lib/flowy-user/build.rs b/frontend/rust-lib/flowy-user/build.rs index f1036a135d35a..84b506ee31c8b 100644 --- a/frontend/rust-lib/flowy-user/build.rs +++ b/frontend/rust-lib/flowy-user/build.rs @@ -8,6 +8,10 @@ fn main() { #[cfg(feature = "tauri_ts")] { flowy_codegen::ts_event::gen(env!("CARGO_PKG_NAME"), flowy_codegen::Project::Tauri); - flowy_codegen::protobuf_file::ts_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::Tauri, + ); } } diff --git a/frontend/rust-lib/lib-dispatch/Cargo.toml b/frontend/rust-lib/lib-dispatch/Cargo.toml index f0b620277525c..48a7aba8163e1 100644 --- a/frontend/rust-lib/lib-dispatch/Cargo.toml +++ b/frontend/rust-lib/lib-dispatch/Cargo.toml @@ -44,5 +44,6 @@ futures-util = "0.3.26" default = ["use_protobuf"] use_serde = ["bincode", "serde_json", "serde", "serde_repr"] use_protobuf= ["protobuf"] +local_set = [] diff --git a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs index 7ef38a8d463f8..32f6968c4c8a2 100644 --- a/frontend/rust-lib/lib-dispatch/src/dispatcher.rs +++ b/frontend/rust-lib/lib-dispatch/src/dispatcher.rs @@ -16,51 +16,51 @@ use crate::{ service::{AFPluginServiceFactory, Service}, }; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub trait AFConcurrent {} -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] impl AFConcurrent for T where T: ?Sized {} -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub trait AFConcurrent: Send + Sync {} -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] impl AFConcurrent for T where T: Send + Sync {} -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub type AFBoxFuture<'a, T> = futures_core::future::LocalBoxFuture<'a, T>; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub type AFBoxFuture<'a, T> = futures_core::future::BoxFuture<'a, T>; pub type AFStateMap = std::sync::Arc; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub(crate) fn downcast_owned(boxed: AFBox) -> Option { boxed.downcast().ok().map(|boxed| *boxed) } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub(crate) fn downcast_owned(boxed: AFBox) -> Option { boxed.downcast().ok().map(|boxed| *boxed) } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub(crate) type AFBox = Box; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub(crate) type AFBox = Box; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub type BoxFutureCallback = Box AFBoxFuture<'static, ()> + 'static>; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub type BoxFutureCallback = Box AFBoxFuture<'static, ()> + Send + Sync + 'static>; -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub fn af_spawn(future: T) -> tokio::task::JoinHandle where T: Future + 'static, @@ -69,7 +69,7 @@ where tokio::task::spawn_local(future) } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub fn af_spawn(future: T) -> tokio::task::JoinHandle where T: Future + Send + 'static, @@ -170,15 +170,6 @@ impl AFPluginDispatcher { callback: Some(Box::new(callback)), }; - // Spawns a future onto the runtime. - // - // This spawns the given future onto the runtime's executor, usually a - // thread pool. The thread pool is then responsible for polling the future - // until it completes. - // - // The provided future will start running in the background immediately - // when `spawn` is called, even if you don't await the returned - // `JoinHandle`. let handle = dispatch.runtime.spawn(async move { service.call(service_ctx).await.unwrap_or_else(|e| { tracing::error!("Dispatch runtime error: {:?}", e); @@ -186,17 +177,35 @@ impl AFPluginDispatcher { }) }); - let runtime = dispatch.runtime.clone(); - DispatchFuture { - fut: Box::pin(async move { - let result = runtime.run_until(handle).await; - result.unwrap_or_else(|e| { - let msg = format!("EVENT_DISPATCH join error: {:?}", e); - tracing::error!("{}", msg); - let error = InternalError::JoinError(msg); - error.as_response() - }) - }), + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] + { + let result = dispatch.runtime.block_on(handle); + DispatchFuture { + fut: Box::pin(async move { + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) + }), + } + } + + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] + { + let runtime = dispatch.runtime.clone(); + DispatchFuture { + fut: Box::pin(async move { + let result = runtime.run_until(handle).await; + result.unwrap_or_else(|e| { + let msg = format!("EVENT_DISPATCH join error: {:?}", e); + tracing::error!("{}", msg); + let error = InternalError::JoinError(msg); + error.as_response() + }) + }), + } } } @@ -212,7 +221,7 @@ impl AFPluginDispatcher { )) } - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] #[track_caller] pub fn spawn(&self, future: F) -> tokio::task::JoinHandle where @@ -221,7 +230,7 @@ impl AFPluginDispatcher { self.runtime.spawn(future) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] #[track_caller] pub fn spawn(&self, future: F) -> tokio::task::JoinHandle where @@ -231,7 +240,7 @@ impl AFPluginDispatcher { self.runtime.spawn(future) } - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub async fn run_until(&self, future: F) -> F::Output where F: Future + 'static, @@ -240,7 +249,7 @@ impl AFPluginDispatcher { self.runtime.run_until(handle).await.unwrap() } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub async fn run_until<'a, F>(&self, future: F) -> F::Output where F: Future + Send + 'a, diff --git a/frontend/rust-lib/lib-dispatch/src/runtime.rs b/frontend/rust-lib/lib-dispatch/src/runtime.rs index 98cbab4a4e5ca..fd3658517cfa0 100644 --- a/frontend/rust-lib/lib-dispatch/src/runtime.rs +++ b/frontend/rust-lib/lib-dispatch/src/runtime.rs @@ -8,14 +8,14 @@ use tokio::task::JoinHandle; pub struct AFPluginRuntime { inner: Runtime, - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] local: tokio::task::LocalSet, } impl Display for AFPluginRuntime { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if cfg!(target_arch = "wasm32") { - write!(f, "Runtime(single_thread)") + if cfg!(any(target_arch = "wasm32", feature = "local_set")) { + write!(f, "Runtime(current_thread)") } else { write!(f, "Runtime(multi_thread)") } @@ -27,12 +27,12 @@ impl AFPluginRuntime { let inner = default_tokio_runtime()?; Ok(Self { inner, - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] local: tokio::task::LocalSet::new(), }) } - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] #[track_caller] pub fn spawn(&self, future: F) -> JoinHandle where @@ -41,7 +41,7 @@ impl AFPluginRuntime { self.local.spawn_local(future) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] #[track_caller] pub fn spawn(&self, future: F) -> JoinHandle where @@ -51,7 +51,7 @@ impl AFPluginRuntime { self.inner.spawn(future) } - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub async fn run_until(&self, future: F) -> F::Output where F: Future, @@ -59,7 +59,7 @@ impl AFPluginRuntime { self.local.run_until(future).await } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub async fn run_until(&self, future: F) -> F::Output where F: Future, @@ -67,7 +67,7 @@ impl AFPluginRuntime { future.await } - #[cfg(target_arch = "wasm32")] + #[cfg(any(target_arch = "wasm32", feature = "local_set"))] #[track_caller] pub fn block_on(&self, f: F) -> F::Output where @@ -76,7 +76,7 @@ impl AFPluginRuntime { self.local.block_on(&self.inner, f) } - #[cfg(not(target_arch = "wasm32"))] + #[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] #[track_caller] pub fn block_on(&self, f: F) -> F::Output where @@ -86,14 +86,14 @@ impl AFPluginRuntime { } } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub fn default_tokio_runtime() -> io::Result { runtime::Builder::new_current_thread() .thread_name("dispatch-rt-st") .build() } -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub fn default_tokio_runtime() -> io::Result { runtime::Builder::new_multi_thread() .thread_name("dispatch-rt-mt") diff --git a/frontend/rust-lib/lib-dispatch/src/service/boxed.rs b/frontend/rust-lib/lib-dispatch/src/service/boxed.rs index 5c6e2f769c7c0..7ff7a7c116138 100644 --- a/frontend/rust-lib/lib-dispatch/src/service/boxed.rs +++ b/frontend/rust-lib/lib-dispatch/src/service/boxed.rs @@ -16,7 +16,7 @@ where BoxServiceFactory(Box::new(FactoryWrapper(factory))) } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] type Inner = Box< dyn AFPluginServiceFactory< Req, @@ -27,7 +27,7 @@ type Inner = Box< Future = AFBoxFuture<'static, Result, Err>>, >, >; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] type Inner = Box< dyn AFPluginServiceFactory< Req, @@ -58,12 +58,12 @@ where } } -#[cfg(target_arch = "wasm32")] +#[cfg(any(target_arch = "wasm32", feature = "local_set"))] pub type BoxService = Box< dyn Service>>, >; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(all(not(target_arch = "wasm32"), not(feature = "local_set")))] pub type BoxService = Box< dyn Service>> + Sync diff --git a/frontend/rust-lib/lib-infra/src/util.rs b/frontend/rust-lib/lib-infra/src/util.rs index 80cdc89fe5dc8..4ac1ecd2de311 100644 --- a/frontend/rust-lib/lib-infra/src/util.rs +++ b/frontend/rust-lib/lib-infra/src/util.rs @@ -13,6 +13,21 @@ macro_rules! if_wasm { $item )*} } +// Define a generic macro to conditionally apply Send and Sync traits with documentation +#[macro_export] +macro_rules! conditional_send_sync_trait { + ($doc:expr; $trait_name:ident { $( $item:tt )* }) => { + // For wasm32 targets, define the trait without Send + Sync + #[doc = $doc] + #[cfg(target_arch = "wasm32")] + pub trait $trait_name { $( $item )* } + + // For non-wasm32 targets, define the trait with Send + Sync + #[doc = $doc] + #[cfg(not(target_arch = "wasm32"))] + pub trait $trait_name: Send + Sync { $( $item )* } + }; +} pub fn move_vec_element( vec: &mut Vec, diff --git a/frontend/scripts/makefile/web.toml b/frontend/scripts/makefile/web.toml index f0d50a4612e2b..40162c9e6c9f3 100644 --- a/frontend/scripts/makefile/web.toml +++ b/frontend/scripts/makefile/web.toml @@ -26,9 +26,18 @@ run_task = { name = [ "rm_rust_generated_files", "rm_web_generated_protobuf_files", "rm_web_generated_event_files", + "rm_pkg", ] } +[tasks.rm_pkg] +private = true +script = [""" +cd ${WEB_LIB_PATH} +rimraf dist pkg +"""] +script_runner = "@duckscript" + [tasks.rm_web_generated_protobuf_files] private = true script = [""" From a515715543e1ee68e90c0a616394ef1929a96279 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:52:59 +0800 Subject: [PATCH 10/50] feat: reorder sort precedence (#4592) * feat: reorder sorts * chore: add tests, fix tests, fix tauri build and fix clippy * fix: add missing import --- .../database/database_sort_test.dart | 112 +++++++++++++++++- .../util/database_test_op.dart | 28 +++++ .../application/field/field_controller.dart | 11 +- .../application/sort/sort_service.dart | 14 +++ .../application/sort/sort_editor_bloc.dart | 19 +++ .../widgets/sort/sort_editor.dart | 90 ++++++++++---- .../presentation/widgets/sort/sort_menu.dart | 36 +++--- .../setting/setting_property_list.dart | 1 + frontend/appflowy_tauri/src-tauri/Cargo.lock | 14 +-- frontend/appflowy_tauri/src-tauri/Cargo.toml | 46 +++++-- .../database/sort/sort_listeners.ts | 2 +- frontend/appflowy_web/wasm-libs/Cargo.toml | 23 ++-- frontend/rust-lib/Cargo.lock | 14 +-- frontend/rust-lib/Cargo.toml | 14 +-- .../src/entities/filter_entities/util.rs | 4 +- .../flowy-database2/src/entities/mod.rs | 22 ++++ .../src/entities/setting_entities.rs | 9 +- .../src/entities/sort_entities.rs | 29 ++++- .../flowy-database2/src/event_handler.rs | 16 ++- .../src/services/database/database_editor.rs | 15 ++- .../src/services/database_view/view_editor.rs | 33 +++++- .../services/database_view/view_operation.rs | 2 + .../src/services/sort/controller.rs | 24 +++- .../src/services/sort/entities.rs | 13 ++ .../database/sort_test/multi_sort_test.rs | 52 ++++++++ .../tests/database/sort_test/script.rs | 34 ++++-- .../database/sort_test/single_sort_test.rs | 21 ++-- .../rust-lib/lib-infra/src/validator_fn.rs | 25 ++++ 28 files changed, 589 insertions(+), 134 deletions(-) diff --git a/frontend/appflowy_flutter/integration_test/database/database_sort_test.dart b/frontend/appflowy_flutter/integration_test/database/database_sort_test.dart index 633f77a3ef1f0..6b4d53d5bc783 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_sort_test.dart +++ b/frontend/appflowy_flutter/integration_test/database/database_sort_test.dart @@ -86,7 +86,7 @@ void main() { testWidgets('add checkbox sort', (tester) async { await tester.openV020database(); - // create a filter + // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); @@ -136,7 +136,7 @@ void main() { testWidgets('add number sort', (tester) async { await tester.openV020database(); - // create a filter + // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Number, 'number'); @@ -188,7 +188,7 @@ void main() { testWidgets('add checkbox and number sort', (tester) async { await tester.openV020database(); - // create a filter + // create a sort await tester.tapDatabaseSortButton(); await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); @@ -264,5 +264,111 @@ void main() { await tester.pumpAndSettle(); }); + + testWidgets('reorder sort', (tester) async { + await tester.openV020database(); + // create a sort + await tester.tapDatabaseSortButton(); + await tester.tapCreateSortByFieldType(FieldType.Checkbox, 'Done'); + + // open the sort menu and sort checkbox by descending + await tester.tapSortMenuInSettingBar(); + await tester.tapSortButtonByName('Done'); + await tester.tapSortByDescending(); + + // add another sort, this time by number descending + await tester.tapSortMenuInSettingBar(); + await tester.tapCreateSortByFieldTypeInSortMenu( + FieldType.Number, + 'number', + ); + await tester.tapSortButtonByName('number'); + await tester.tapSortByDescending(); + + // check checkbox cell order + for (final (index, content) in [ + true, + true, + true, + true, + true, + false, + false, + false, + false, + false, + ].indexed) { + await tester.assertCheckboxCell( + rowIndex: index, + isSelected: content, + ); + } + + // check number cell order + for (final (index, content) in [ + '1', + '0.2', + '0.1', + '-1', + '-2', + '12', + '11', + '10', + '2', + '', + ].indexed) { + tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + + // reorder sort + await tester.tapSortMenuInSettingBar(); + await tester.reorderSort( + (FieldType.Number, 'number'), + (FieldType.Checkbox, 'Done'), + ); + + // check checkbox cell order + for (final (index, content) in [ + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + ].indexed) { + await tester.assertCheckboxCell( + rowIndex: index, + isSelected: content, + ); + } + + // check the number cell order + for (final (index, content) in [ + '12', + '11', + '10', + '2', + '1', + '0.2', + '0.1', + '-1', + '-2', + '', + ].indexed) { + tester.assertCellContent( + rowIndex: index, + fieldType: FieldType.Number, + content: content, + ); + } + }); }); } diff --git a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart index cb997738c6da2..12722ebe1b58a 100644 --- a/frontend/appflowy_flutter/integration_test/util/database_test_op.dart +++ b/frontend/appflowy_flutter/integration_test/util/database_test_op.dart @@ -1070,6 +1070,34 @@ extension AppFlowyDatabaseTest on WidgetTester { await tapButton(findSortItem); } + /// Must call [tapSortMenuInSettingBar] first. + Future reorderSort( + (FieldType, String) from, + (FieldType, String) to, + ) async { + final fromSortItem = find.byWidgetPredicate( + (widget) => + widget is DatabaseSortItem && + widget.sortInfo.fieldInfo.fieldType == from.$1 && + widget.sortInfo.fieldInfo.name == from.$2, + ); + final toSortItem = find.byWidgetPredicate( + (widget) => + widget is DatabaseSortItem && + widget.sortInfo.fieldInfo.fieldType == to.$1 && + widget.sortInfo.fieldInfo.name == to.$2, + ); + final dragElement = find.descendant( + of: fromSortItem, + matching: find.byType(ReorderableDragStartListener), + ); + await drag( + dragElement, + getCenter(toSortItem) - getCenter(fromSortItem), + ); + await pumpAndSettle(const Duration(milliseconds: 200)); + } + /// Must call [tapSortButtonByName] first. Future tapSortByDescending() async { await tapButton( 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 3be14cec47d8d..60d1037b100ba 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 @@ -282,16 +282,19 @@ class FieldController { ) { for (final newSortPB in changeset.insertSorts) { final sortIndex = newSortInfos - .indexWhere((element) => element.sortId == newSortPB.id); + .indexWhere((element) => element.sortId == newSortPB.sort.id); if (sortIndex == -1) { final fieldInfo = _findFieldInfo( fieldInfos: fieldInfos, - fieldId: newSortPB.fieldId, - fieldType: newSortPB.fieldType, + fieldId: newSortPB.sort.fieldId, + fieldType: newSortPB.sort.fieldType, ); if (fieldInfo != null) { - newSortInfos.add(SortInfo(sortPB: newSortPB, fieldInfo: fieldInfo)); + newSortInfos.insert( + newSortPB.index, + SortInfo(sortPB: newSortPB.sort, fieldInfo: fieldInfo), + ); } } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart index e0b9e591e5439..21260c4f3d1cb 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/sort/sort_service.dart @@ -75,6 +75,20 @@ class SortBackendService { }); } + Future> reorderSort({ + required String fromSortId, + required String toSortId, + }) { + final payload = DatabaseSettingChangesetPB() + ..viewId = viewId + ..reorderSort = (ReorderSortPayloadPB() + ..viewId = viewId + ..fromSortId = fromSortId + ..toSortId = toSortId); + + return DatabaseEventUpdateDatabaseSetting(payload).send(); + } + Future> deleteSort({ required String fieldId, required String sortId, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart index 003052b14b049..0eb2b9c6c895c 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/sort/sort_editor_bloc.dart @@ -71,6 +71,23 @@ class SortEditorBloc extends Bloc { ); result.fold((l) => null, (err) => Log.error(err)); }, + reorderSort: (fromIndex, toIndex) async { + if (fromIndex < toIndex) { + toIndex--; + } + + final fromId = state.sortInfos[fromIndex].sortId; + final toId = state.sortInfos[toIndex].sortId; + + final newSorts = [...state.sortInfos]; + newSorts.insert(toIndex, newSorts.removeAt(fromIndex)); + emit(state.copyWith(sortInfos: newSorts)); + final result = await _sortBackendSvc.reorderSort( + fromSortId: fromId, + toSortId: toId, + ); + result.fold((l) => null, (err) => Log.error(err)); + }, ); }, ); @@ -113,6 +130,8 @@ class SortEditorEvent with _$SortEditorEvent { ) = _SetCondition; const factory SortEditorEvent.deleteSort(SortInfo sortInfo) = _DeleteSort; const factory SortEditorEvent.deleteAllSorts() = _DeleteAllSorts; + const factory SortEditorEvent.reorderSort(int oldIndex, int newIndex) = + _ReorderSort; } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart index b3efb32b960b8..9dfaa03d3f5d4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_editor.dart @@ -1,3 +1,5 @@ +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/field_controller.dart'; @@ -48,33 +50,58 @@ class _SortEditorState extends State { )..add(const SortEditorEvent.initial()), child: BlocBuilder( builder: (context, state) { - return Column( - children: [ - ...state.sortInfos.map( - (info) => Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: DatabaseSortItem( - sortInfo: info, - popoverMutex: popoverMutex, - ), - ), + final sortInfos = state.sortInfos; + + return ReorderableListView.builder( + onReorder: (oldIndex, newIndex) => context + .read() + .add(SortEditorEvent.reorderSort(oldIndex, newIndex)), + itemCount: state.sortInfos.length, + itemBuilder: (context, index) => Padding( + key: ValueKey(sortInfos[index].sortId), + padding: const EdgeInsets.symmetric(vertical: 6), + child: DatabaseSortItem( + index: index, + sortInfo: sortInfos[index], + popoverMutex: popoverMutex, ), - Row( + ), + proxyDecorator: (child, index, animation) => Material( + color: Colors.transparent, + child: Stack( children: [ - Flexible( - child: DatabaseAddSortButton( - viewId: widget.viewId, - fieldController: widget.fieldController, - popoverMutex: popoverMutex, - ), + BlocProvider.value( + value: context.read(), + child: child, ), - const HSpace(6), - Flexible( - child: DatabaseDeleteSortButton(popoverMutex: popoverMutex), + MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grabbing, + child: const SizedBox.expand(), ), ], ), - ], + ), + shrinkWrap: true, + buildDefaultDragHandles: false, + footer: Row( + children: [ + Flexible( + child: DatabaseAddSortButton( + viewId: widget.viewId, + fieldController: widget.fieldController, + popoverMutex: popoverMutex, + ), + ), + const HSpace(6), + Flexible( + child: DatabaseDeleteSortButton( + popoverMutex: popoverMutex, + ), + ), + ], + ), ); }, ), @@ -85,10 +112,12 @@ class _SortEditorState extends State { class DatabaseSortItem extends StatelessWidget { const DatabaseSortItem({ super.key, + required this.index, required this.popoverMutex, required this.sortInfo, }); + final int index; final PopoverMutex popoverMutex; final SortInfo sortInfo; @@ -107,6 +136,23 @@ class DatabaseSortItem extends StatelessWidget { return Row( children: [ + ReorderableDragStartListener( + index: index, + child: MouseRegion( + cursor: Platform.isWindows + ? SystemMouseCursors.click + : SystemMouseCursors.grab, + child: SizedBox( + width: 14, + height: 14, + child: FlowySvg( + FlowySvgs.drag_element_s, + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ), + const HSpace(6), SizedBox( height: 26, child: SortChoiceButton( @@ -122,8 +168,8 @@ class DatabaseSortItem extends StatelessWidget { popoverMutex: popoverMutex, ), ), - const HSpace(6), const Spacer(), + const HSpace(6), deleteButton, ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart index f7663cf470494..30fe44f10192f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/sort/sort_menu.dart @@ -31,27 +31,25 @@ class SortMenu extends StatelessWidget { )..add(const SortMenuEvent.initial()), child: BlocBuilder( builder: (context, state) { - if (state.sortInfos.isNotEmpty) { - return AppFlowyPopover( - controller: PopoverController(), - constraints: BoxConstraints.loose(const Size(320, 200)), - direction: PopoverDirection.bottomWithLeftAligned, - offset: const Offset(0, 5), - popupBuilder: (BuildContext popoverContext) { - return SingleChildScrollView( - child: SortEditor( - viewId: state.viewId, - fieldController: - context.read().fieldController, - sortInfos: state.sortInfos, - ), - ); - }, - child: SortChoiceChip(sortInfos: state.sortInfos), - ); + if (state.sortInfos.isEmpty) { + return const SizedBox.shrink(); } - return const SizedBox.shrink(); + return AppFlowyPopover( + controller: PopoverController(), + constraints: BoxConstraints.loose(const Size(320, 200)), + direction: PopoverDirection.bottomWithLeftAligned, + offset: const Offset(0, 5), + margin: const EdgeInsets.fromLTRB(6.0, 0.0, 6.0, 6.0), + popupBuilder: (BuildContext popoverContext) { + return SortEditor( + viewId: state.viewId, + fieldController: context.read().fieldController, + sortInfos: state.sortInfos, + ); + }, + child: SortChoiceChip(sortInfos: state.sortInfos), + ); }, ), ); 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 3e94429489a72..fd8958cea661b 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 @@ -169,6 +169,7 @@ class _DatabasePropertyCellState extends State { FlowySvg( widget.fieldInfo.fieldType.svgData, color: Theme.of(context).iconTheme.color, + size: const Size.square(16), ), ], ), diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index 734d7b474a1de..d37cc705e2299 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -816,7 +816,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "async-trait", @@ -838,7 +838,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "async-trait", @@ -867,7 +867,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "collab", @@ -886,7 +886,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "bytes", @@ -901,7 +901,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "chrono", @@ -938,7 +938,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "async-stream", @@ -977,7 +977,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "collab", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index d46b59109c39c..370d1edc573a6 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -34,18 +34,38 @@ lru = "0.12.0" [dependencies] serde_json.workspace = true serde.workspace = true -tauri = { version = "1.5", features = ["clipboard-all", "fs-all", "shell-open"] } +tauri = { version = "1.5", features = [ + "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"] } +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"] } +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" [features] @@ -72,10 +92,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } diff --git a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts index 377dfec2f59c2..808c62e0d2eb9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/application/database/sort/sort_listeners.ts @@ -12,7 +12,7 @@ const deleteSortsFromChange = (database: Database, changeset: SortChangesetNotif const insertSortsFromChange = (database: Database, changeset: SortChangesetNotificationPB) => { changeset.insert_sorts.forEach(sortPB => { - database.sorts.push(pbToSort(sortPB)); + database.sorts.push(pbToSort(sortPB.sort)); }); }; diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 9c333f613345f..24ddff37be92e 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.toml +++ b/frontend/appflowy_web/wasm-libs/Cargo.toml @@ -1,9 +1,5 @@ [workspace] -members = [ - "af-wasm", - "af-user", - "af-persistence", -] +members = ["af-wasm", "af-user", "af-persistence"] resolver = "2" [workspace.dependencies] @@ -15,7 +11,7 @@ parking_lot = { version = "0.12.1" } tracing = { version = "0.1.22" } serde = { version = "1.0.194", features = ["derive"] } serde_json = "1.0" -collab-integrate = { path = "../../rust-lib/collab-integrate"} +collab-integrate = { path = "../../rust-lib/collab-integrate" } flowy-notification = { path = "../../rust-lib/flowy-notification" } flowy-user-pub = { path = "../../rust-lib/flowy-user-pub" } flowy-server = { path = "../../rust-lib/flowy-server" } @@ -42,7 +38,6 @@ wasm-bindgen-futures = "0.4.40" serde-wasm-bindgen = "0.4" - [profile.dev] opt-level = 0 lto = false @@ -70,10 +65,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 1030d588b2645..494c8af709cc9 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -744,7 +744,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "async-trait", @@ -766,7 +766,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "async-trait", @@ -795,7 +795,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "collab", @@ -814,7 +814,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "bytes", @@ -829,7 +829,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "chrono", @@ -866,7 +866,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "async-stream", @@ -905,7 +905,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=a6b0932581b4544a0800a0451b9522e6caab5570#a6b0932581b4544a0800a0451b9522e6caab5570" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" dependencies = [ "anyhow", "collab", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index fdf144fe6663a..48ae3727f1271 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -115,10 +115,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "a6b0932581b4544a0800a0451b9522e6caab5570" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } 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 d12706b81f0a2..4d2d9283c7839 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 @@ -84,7 +84,7 @@ pub struct DeleteFilterPayloadPB { pub field_type: FieldType, #[pb(index = 3)] - #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + #[validate(custom = "crate::entities::utils::validate_filter_id")] pub filter_id: String, #[pb(index = 4)] @@ -103,7 +103,7 @@ pub struct UpdateFilterPayloadPB { /// Create a new filter if the filter_id is None #[pb(index = 3, one_of)] - #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + #[validate(custom = "crate::entities::utils::validate_filter_id")] pub filter_id: Option, #[pb(index = 4)] diff --git a/frontend/rust-lib/flowy-database2/src/entities/mod.rs b/frontend/rust-lib/flowy-database2/src/entities/mod.rs index 63fd455832c95..6bacbd0539761 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/mod.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/mod.rs @@ -35,3 +35,25 @@ pub use share_entities::*; pub use sort_entities::*; pub use type_option_entities::*; pub use view_entities::*; + +mod utils { + use fancy_regex::Regex; + use lib_infra::impl_regex_validator; + use validator::ValidationError; + + impl_regex_validator!( + validate_filter_id, + Regex::new(r"^[A-Za-z0-9_-]{6}$").unwrap(), + "invalid filter_id" + ); + impl_regex_validator!( + validate_sort_id, + Regex::new(r"^s:[A-Za-z0-9_-]{6}$").unwrap(), + "invalid sort_id" + ); + impl_regex_validator!( + validate_group_id, + Regex::new(r"^g:[A-Za-z0-9_-]{6}$").unwrap(), + "invalid group_id" + ); +} 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 1305986ffbab2..e1cefc07cc75a 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/setting_entities.rs @@ -15,7 +15,7 @@ use crate::entities::{ }; use crate::services::setting::{BoardLayoutSetting, CalendarLayoutSetting}; -use super::BoardLayoutSettingPB; +use super::{BoardLayoutSettingPB, ReorderSortPayloadPB}; /// [DatabaseViewSettingPB] defines the setting options for the grid. Such as the filter, group, and sort. #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] @@ -39,9 +39,8 @@ pub struct DatabaseViewSettingPB { pub field_settings: RepeatedFieldSettingsPB, } -#[derive(Debug, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)] +#[derive(Debug, Default, Clone, PartialEq, Eq, ProtoBuf_Enum, EnumIter)] #[repr(u8)] -#[derive(Default)] pub enum DatabaseLayoutPB { #[default] Grid = 0, @@ -96,6 +95,10 @@ pub struct DatabaseSettingChangesetPB { #[pb(index = 7, one_of)] #[validate] + pub reorder_sort: Option, + + #[pb(index = 8, one_of)] + #[validate] pub delete_sort: Option, } diff --git a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs index 4814a8979207b..e7c6eb762c065 100644 --- a/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs +++ b/frontend/rust-lib/flowy-database2/src/entities/sort_entities.rs @@ -41,6 +41,15 @@ impl std::convert::From for SortPB { } } +#[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] +pub struct SortWithIndexPB { + #[pb(index = 1)] + pub index: u32, + + #[pb(index = 2)] + pub sort: SortPB, +} + #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct RepeatedSortPB { #[pb(index = 1)] @@ -105,12 +114,28 @@ pub struct UpdateSortPayloadPB { /// Create a new sort if the sort_id is None #[pb(index = 4, one_of)] + #[validate(custom = "super::utils::validate_sort_id")] pub sort_id: Option, #[pb(index = 5)] pub condition: SortConditionPB, } +#[derive(Debug, Default, Clone, Validate, ProtoBuf)] +pub struct ReorderSortPayloadPB { + #[pb(index = 1)] + #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + pub view_id: String, + + #[pb(index = 2)] + #[validate(custom = "super::utils::validate_sort_id")] + pub from_sort_id: String, + + #[pb(index = 3)] + #[validate(custom = "super::utils::validate_sort_id")] + pub to_sort_id: String, +} + #[derive(ProtoBuf, Debug, Default, Clone, Validate)] pub struct DeleteSortPayloadPB { #[pb(index = 1)] @@ -118,7 +143,7 @@ pub struct DeleteSortPayloadPB { pub view_id: String, #[pb(index = 2)] - #[validate(custom = "lib_infra::validator_fn::required_not_empty_str")] + #[validate(custom = "super::utils::validate_sort_id")] pub sort_id: String, } @@ -128,7 +153,7 @@ pub struct SortChangesetNotificationPB { pub view_id: String, #[pb(index = 2)] - pub insert_sorts: Vec, + pub insert_sorts: Vec, #[pb(index = 3)] pub delete_sorts: Vec, diff --git a/frontend/rust-lib/flowy-database2/src/event_handler.rs b/frontend/rust-lib/flowy-database2/src/event_handler.rs index 7c25519d5276d..72dde979293be 100644 --- a/frontend/rust-lib/flowy-database2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-database2/src/event_handler.rs @@ -88,28 +88,32 @@ pub(crate) async fn update_database_setting_handler( ) -> Result<(), FlowyError> { let manager = upgrade_manager(manager)?; let params = data.try_into_inner()?; - let editor = manager.get_database_with_view_id(¶ms.view_id).await?; + let database_editor = manager.get_database_with_view_id(¶ms.view_id).await?; if let Some(update_filter) = params.update_filter { - editor + database_editor .create_or_update_filter(update_filter.try_into()?) .await?; } if let Some(delete_filter) = params.delete_filter { - editor.delete_filter(delete_filter).await?; + database_editor.delete_filter(delete_filter).await?; } if let Some(update_sort) = params.update_sort { - let _ = editor.create_or_update_sort(update_sort).await?; + let _ = database_editor.create_or_update_sort(update_sort).await?; + } + + if let Some(reorder_sort) = params.reorder_sort { + database_editor.reorder_sort(reorder_sort).await?; } if let Some(delete_sort) = params.delete_sort { - editor.delete_sort(delete_sort).await?; + database_editor.delete_sort(delete_sort).await?; } if let Some(layout_type) = params.layout_type { - editor + database_editor .update_view_layout(¶ms.view_id, layout_type.into()) .await?; } 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 4c6f2e757ac80..e2e882391941a 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 @@ -228,10 +228,16 @@ impl DatabaseEditor { pub async fn create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult { let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; - let sort = view_editor.insert_or_update_sort(params).await?; + let sort = view_editor.v_create_or_update_sort(params).await?; Ok(sort) } + pub async fn reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> { + let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; + view_editor.v_reorder_sort(params).await?; + Ok(()) + } + pub async fn delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { let view_editor = self.database_views.get_view_editor(¶ms.view_id).await?; view_editor.v_delete_sort(params).await?; @@ -1459,6 +1465,13 @@ impl DatabaseViewOperation for DatabaseViewOperationImpl { self.database.lock().insert_sort(view_id, sort); } + fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str) { + self + .database + .lock() + .move_sort(view_id, from_sort_id, to_sort_id); + } + fn remove_sort(&self, view_id: &str, sort_id: &str) { self.database.lock().remove_sort(view_id, sort_id); } 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 35ce86767445c..04e68c3b23599 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 @@ -17,8 +17,8 @@ use lib_dispatch::prelude::af_spawn; use crate::entities::{ CalendarEventPB, DatabaseLayoutMetaPB, DatabaseLayoutSettingPB, DeleteFilterPayloadPB, DeleteSortPayloadPB, FieldType, FieldVisibility, GroupChangesPB, GroupPB, InsertedRowPB, - LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, RowMetaPB, - RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, + LayoutSettingChangeset, LayoutSettingParams, RemoveCalculationChangesetPB, ReorderSortPayloadPB, + RowMetaPB, RowsChangePB, SortChangesetNotificationPB, SortPB, UpdateCalculationChangesetPB, UpdateFilterParams, UpdateSortPayloadPB, }; use crate::notification::{send_notification, DatabaseNotification}; @@ -499,7 +499,7 @@ impl DatabaseViewEditor { } #[tracing::instrument(level = "trace", skip(self), err)] - pub async fn insert_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult { + pub async fn v_create_or_update_sort(&self, params: UpdateSortPayloadPB) -> FlowyResult { let is_exist = params.sort_id.is_some(); let sort_id = match params.sort_id { None => gen_database_sort_id(), @@ -513,9 +513,11 @@ impl DatabaseViewEditor { condition: params.condition.into(), }; - let mut sort_controller = self.sort_controller.write().await; self.delegate.insert_sort(&self.view_id, sort.clone()); - let changeset = if is_exist { + + let mut sort_controller = self.sort_controller.write().await; + + let notification = if is_exist { sort_controller .apply_changeset(SortChangeset::from_update(sort.clone())) .await @@ -525,10 +527,29 @@ impl DatabaseViewEditor { .await }; drop(sort_controller); - notify_did_update_sort(changeset).await; + notify_did_update_sort(notification).await; Ok(sort) } + pub async fn v_reorder_sort(&self, params: ReorderSortPayloadPB) -> FlowyResult<()> { + self + .delegate + .move_sort(&self.view_id, ¶ms.from_sort_id, ¶ms.to_sort_id); + + let notification = self + .sort_controller + .write() + .await + .apply_changeset(SortChangeset::from_reorder( + params.from_sort_id, + params.to_sort_id, + )) + .await; + + notify_did_update_sort(notification).await; + Ok(()) + } + pub async fn v_delete_sort(&self, params: DeleteSortPayloadPB) -> FlowyResult<()> { let notification = self .sort_controller 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 291f98ac03243..40ad9778b58b7 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 @@ -75,6 +75,8 @@ pub trait DatabaseViewOperation: Send + Sync + 'static { fn insert_sort(&self, view_id: &str, sort: Sort); + fn move_sort(&self, view_id: &str, from_sort_id: &str, to_sort_id: &str); + fn remove_sort(&self, view_id: &str, sort_id: &str); fn get_all_sorts(&self, view_id: &str) -> Vec; 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 88f2440b46ce2..599a67303cc51 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -13,8 +13,8 @@ use flowy_error::FlowyResult; use lib_infra::future::Fut; use lib_infra::priority_task::{QualityOfService, Task, TaskContent, TaskDispatcher}; -use crate::entities::FieldType; use crate::entities::SortChangesetNotificationPB; +use crate::entities::{FieldType, SortWithIndexPB}; use crate::services::cell::CellCache; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; use crate::services::field::{default_order, TypeOptionCellExt}; @@ -184,7 +184,10 @@ impl SortController { if let Some(insert_sort) = changeset.insert_sort { if let Some(sort) = self.delegate.get_sort(&self.view_id, &insert_sort.id).await { - notification.insert_sorts.push(sort.as_ref().into()); + notification.insert_sorts.push(SortWithIndexPB { + index: self.sorts.len() as u32, + sort: sort.as_ref().into(), + }); self.sorts.push(sort); } } @@ -209,6 +212,23 @@ impl SortController { } } + if let Some((from_id, to_id)) = changeset.reorder_sort { + let moved_sort = self.delegate.get_sort(&self.view_id, &from_id).await; + let from_index = self.sorts.iter().position(|sort| sort.id == from_id); + let to_index = self.sorts.iter().position(|sort| sort.id == to_id); + + if let (Some(sort), Some(from_index), Some(to_index)) = (moved_sort, from_index, to_index) { + self.sorts.remove(from_index); + self.sorts.insert(to_index, sort.clone()); + + notification.delete_sorts.push(sort.as_ref().into()); + notification.insert_sorts.push(SortWithIndexPB { + index: to_index as u32, + sort: sort.as_ref().into(), + }); + } + } + if !notification.is_empty() { self .gen_task(SortEvent::SortDidChanged, QualityOfService::UserInteractive) 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 fe262877d516c..15cb7d4c2a1d5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -126,6 +126,7 @@ pub struct SortChangeset { pub(crate) insert_sort: Option, pub(crate) update_sort: Option, pub(crate) delete_sort: Option, + pub(crate) reorder_sort: Option<(String, String)>, } impl SortChangeset { @@ -134,6 +135,7 @@ impl SortChangeset { insert_sort: Some(sort), update_sort: None, delete_sort: None, + reorder_sort: None, } } @@ -142,6 +144,7 @@ impl SortChangeset { insert_sort: None, update_sort: Some(sort), delete_sort: None, + reorder_sort: None, } } @@ -150,6 +153,16 @@ impl SortChangeset { insert_sort: None, update_sort: None, delete_sort: Some(sort_id), + reorder_sort: None, + } + } + + pub fn from_reorder(from_sort_id: String, to_sort_id: String) -> Self { + Self { + insert_sort: None, + update_sort: None, + delete_sort: None, + reorder_sort: Some((from_sort_id, to_sort_id)), } } } diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs index bd9ff68d47b9a..7fe1874984be9 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/multi_sort_test.rs @@ -47,3 +47,55 @@ async fn sort_checkbox_and_then_text_by_descending_test() { ]; test.run_scripts(scripts).await; } + +#[tokio::test] +async fn reorder_sort_test() { + let mut test = DatabaseSortTest::new().await; + let checkbox_field = test.get_first_field(FieldType::Checkbox); + let text_field = test.get_first_field(FieldType::RichText); + // Use the same sort set up as above + let scripts = vec![ + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "No", "Yes", ""], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], + }, + InsertSort { + field: checkbox_field.clone(), + condition: SortCondition::Descending, + }, + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "Yes", "No", "No", "", "No"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "", "AE", "C", "CB", "DA"], + }, + ]; + test.run_scripts(scripts).await; + + let sorts = test.editor.get_all_sorts(&test.view_id).await.items; + let scripts = vec![ + ReorderSort { + from_sort_id: sorts[1].id.clone(), + to_sort_id: sorts[0].id.clone(), + }, + AssertCellContentOrder { + field_id: checkbox_field.id.clone(), + orders: vec!["Yes", "Yes", "No", "No", "", "No", "Yes"], + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""], + }, + ]; + test.run_scripts(scripts).await; +} 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 21d7f0557d42a..7e12235fe1a5f 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 @@ -7,10 +7,12 @@ use collab_database::rows::RowId; use futures::stream::StreamExt; use tokio::sync::broadcast::Receiver; -use flowy_database2::entities::{DeleteSortPayloadPB, FieldType, UpdateSortPayloadPB}; +use flowy_database2::entities::{ + DeleteSortPayloadPB, FieldType, ReorderSortPayloadPB, UpdateSortPayloadPB, +}; use flowy_database2::services::cell::stringify_cell_data; use flowy_database2::services::database_view::DatabaseViewChanged; -use flowy_database2::services::sort::{Sort, SortCondition}; +use flowy_database2::services::sort::SortCondition; use crate::database::database_editor::DatabaseEditorTest; @@ -19,6 +21,10 @@ pub enum SortScript { field: Field, condition: SortCondition, }, + ReorderSort { + from_sort_id: String, + to_sort_id: String, + }, DeleteSort { sort_id: String, }, @@ -41,7 +47,6 @@ pub enum SortScript { pub struct DatabaseSortTest { inner: DatabaseEditorTest, - pub current_sort_rev: Option, recv: Option>, } @@ -50,7 +55,6 @@ impl DatabaseSortTest { let editor_test = DatabaseEditorTest::new_grid().await; Self { inner: editor_test, - current_sort_rev: None, recv: None, } } @@ -77,8 +81,25 @@ impl DatabaseSortTest { field_type: FieldType::from(field.field_type), condition: condition.into(), }; - let sort_rev = self.editor.create_or_update_sort(params).await.unwrap(); - self.current_sort_rev = Some(sort_rev); + let _ = self.editor.create_or_update_sort(params).await.unwrap(); + }, + SortScript::ReorderSort { + from_sort_id, + to_sort_id, + } => { + self.recv = Some( + self + .editor + .subscribe_view_changed(&self.view_id) + .await + .unwrap(), + ); + let params = ReorderSortPayloadPB { + view_id: self.view_id.clone(), + from_sort_id, + to_sort_id, + }; + self.editor.reorder_sort(params).await.unwrap(); }, SortScript::DeleteSort { sort_id } => { self.recv = Some( @@ -93,7 +114,6 @@ impl DatabaseSortTest { sort_id, }; self.editor.delete_sort(params).await.unwrap(); - self.current_sort_rev = None; }, SortScript::AssertCellContentOrder { field_id, orders } => { let mut cells = vec![]; diff --git a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs index a3eac1db8d391..2620c3149b476 100644 --- a/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs +++ b/frontend/rust-lib/flowy-database2/tests/database/sort_test/single_sort_test.rs @@ -85,16 +85,21 @@ async fn sort_change_notification_by_update_text_test() { async fn sort_text_by_ascending_and_delete_sort_test() { let mut test = DatabaseSortTest::new().await; let text_field = test.get_first_field(FieldType::RichText).clone(); - let scripts = vec![InsertSort { - field: text_field.clone(), - condition: SortCondition::Ascending, - }]; - test.run_scripts(scripts).await; - let sort = test.current_sort_rev.as_ref().unwrap(); let scripts = vec![ - DeleteSort { - sort_id: sort.id.clone(), + InsertSort { + field: text_field.clone(), + condition: SortCondition::Ascending, + }, + AssertCellContentOrder { + field_id: text_field.id.clone(), + orders: vec!["A", "AE", "AE", "C", "CB", "DA", ""], }, + ]; + test.run_scripts(scripts).await; + + let sort = test.editor.get_all_sorts(&test.view_id).await.items[0].clone(); + let scripts = vec![ + DeleteSort { sort_id: sort.id }, AssertCellContentOrder { field_id: text_field.id.clone(), orders: vec!["A", "", "C", "DA", "AE", "AE", "CB"], diff --git a/frontend/rust-lib/lib-infra/src/validator_fn.rs b/frontend/rust-lib/lib-infra/src/validator_fn.rs index feaa55292f7ad..6d7a034b0c632 100644 --- a/frontend/rust-lib/lib-infra/src/validator_fn.rs +++ b/frontend/rust-lib/lib-infra/src/validator_fn.rs @@ -15,3 +15,28 @@ pub fn required_valid_path(s: &str) -> Result<(), ValidationError> { (_, _) => Err(ValidationError::new("invalid_path")), } } + +#[macro_export] +/// Macro to implement a custom validator function for a regex expression. +/// This is intended to replace `validator` crate's own regex validator, which +/// isn't compatible with `fancy_regex`. +/// +/// # Arguments: +/// +/// - name of the validator function +/// - the `fancy_regex::Regex` object +/// - error message of the `ValidationError` +/// +macro_rules! impl_regex_validator { + ($validator: ident, $regex: expr, $error: expr) => { + pub(crate) fn $validator(arg: &str) -> Result<(), ValidationError> { + let check = $regex.is_match(arg).unwrap(); + + if check { + Ok(()) + } else { + Err(ValidationError::new($error)) + } + } + }; +} From e20d938ce3051705868971e4b79eb42464e51060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dorian=20P=C3=A9ron?= <72708393+RenjiSann@users.noreply.github.com> Date: Mon, 5 Feb 2024 09:53:18 +0100 Subject: [PATCH 11/50] chore: update translations (#4581) --- frontend/resources/translations/ca-ES.json | 4 +- frontend/resources/translations/de-DE.json | 4 +- frontend/resources/translations/es-VE.json | 4 +- frontend/resources/translations/fr-FR.json | 52 ++++++++++++++++++---- frontend/resources/translations/it-IT.json | 4 +- frontend/resources/translations/pt-BR.json | 4 +- frontend/resources/translations/ru-RU.json | 4 +- frontend/resources/translations/vi-VN.json | 4 +- frontend/resources/translations/zh-CN.json | 7 ++- frontend/resources/translations/zh-TW.json | 4 +- 10 files changed, 63 insertions(+), 28 deletions(-) diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 21a1661a1d4d2..20f4dd3cc379a 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -428,13 +428,13 @@ "typeAValue": "Escriu un valor...", "layout": "Disseny", "databaseLayout": "Disseny", - "viewList": "Visualitzacions de la base de dades", "editView": "Edita la vista", "boardSettings": "Configuració del tauler", "calendarSettings": "Configuració del calendari", "createView": "Vista nova", "duplicateView": "Vista duplicada", - "deleteView": "Suprimeix la vista" + "deleteView": "Suprimeix la vista", + "viewList": "Visualitzacions de la base de dades" }, "textFilter": { "contains": "Conté", diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index e84b71d345a17..cd0c4a3a83df5 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -470,14 +470,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" + "numberOfVisibleFields": "{} zeigen", + "viewList": "Datenbank-Ansichten" }, "textFilter": { "contains": "Enthält", diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 2901a5f5b3f2b..94a2a215391d8 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -468,14 +468,14 @@ "typeAValue": "Escriba un valor...", "layout": "Disposición", "databaseLayout": "Disposición", - "viewList": "Vistas de base de datos", "editView": "Editar vista", "boardSettings": "Configuración del tablero", "calendarSettings": "Configuración del calendario", "createView": "Nueva vista", "duplicateView": "Duplicar vista", "deleteView": "Eliminar vista", - "Properties": "Propiedades" + "Properties": "Propiedades", + "viewList": "Vistas de base de datos" }, "textFilter": { "contains": "Contiene", diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 94e16bbf18394..a0c20ed2d6b72 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -61,6 +61,8 @@ "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", + "exportLogFiles": "Exporter les logs", "reachOut": "Contactez-nous sur Discord" } }, @@ -282,10 +284,12 @@ "cloudLocal": "Locale", "cloudSupabase": "Supabase", "cloudSupabaseUrl": "URL de supabase", + "cloudSupabaseUrlCanNotBeEmpty": "L'URL supabase ne peut pas être vide", "cloudSupabaseAnonKey": "Clé anonyme supabase", "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de la supabase n'est pas vide", "cloudAppFlowy": "AppFlowy Cloud Bêta", "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", + "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", "clickToCopy": "Cliquez pour copier", "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", "selfHostContent": "document", @@ -295,6 +299,7 @@ "cloudWSURLHint": "Saisissez l'adresse websocket de votre serveur", "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 cryptage pour sécuriser vos données avec ce secret. Rangez-le 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 secret de cryptage pour", "clickToCopySecret": "Cliquez pour copier le secret", @@ -586,7 +591,8 @@ "newProperty": "Nouvelle colonne", "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?", "newColumn": "Nouvelle colonne", - "format": "Format" + "format": "Format", + "reminderOnDateTooltip": "Cette cellule a un rappel programmé" }, "rowPage": { "newField": "Ajouter un nouveau champ", @@ -789,7 +795,9 @@ "toContinue": "pour continuer", "newDatabase": "Nouvelle Base de données", "linkToDatabase": "Lien vers la Base de données" - } + }, + "date": "Date", + "emoji": "Emoji" }, "textBlock": { "placeholder": "Tapez '/' pour les commandes" @@ -835,7 +843,9 @@ "saveImageToGallery": "Enregistrer l'image", "failedToAddImageToGallery": "Échec de l'ajout d'une image à la galerie", "successToAddImageToGallery": "Image ajoutée à la galerie avec succès", - "unableToLoadImage": "Impossible de charger l'image" + "unableToLoadImage": "Impossible de charger l'image", + "maximumImageSize": "La taille d'image maximale est 10Mo", + "uploadImageErrorImageSizeTooBig": "L'image doit faire au plus 10Mo" }, "codeBlock": { "language": { @@ -923,6 +933,10 @@ "previousMonth": "Le mois précédent", "nextMonth": "Le mois prochain" }, + "mobileEventScreen": { + "emptyTitle": "Pas d'événements", + "emptyBody": "Cliquez sur le bouton plus pour créer un événement à cette date." + }, "settings": { "showWeekNumbers": "Afficher les numéros de semaine", "showWeekends": "Afficher les week-ends", @@ -930,10 +944,10 @@ "layoutDateField": "Calendrier de mise en page par", "changeLayoutDateField": "Modifier le champ de mise en page", "noDateTitle": "Pas de date", + "noDateHint": "Les événements non planifiés s'afficheront ici", "unscheduledEventsTitle": "Événements imprévus", "clickToAdd": "Cliquez pour ajouter au calendrier", - "name": "Disposition du calendrier", - "noDateHint": "Les événements non planifiés s'afficheront ici" + "name": "Disposition du calendrier" }, "referencedCalendarPrefix": "Vue", "quickJumpYear": "Sauter à" @@ -1023,7 +1037,23 @@ "includeTime": "Inclure le temps", "isRange": "Date de fin", "timeFormat": "Format du temps", - "clearDate": "Effacer la date" + "clearDate": "Effacer la date", + "reminderLabel": "Rappel", + "selectReminder": "Sélectionnez un rappel", + "reminderOptions": { + "none": "Aucun", + "atTimeOfEvent": "Heure de l'événement", + "fiveMinsBefore": "5 minutes avant", + "tenMinsBefore": "10 minutes avant", + "fifteenMinsBefore": "15 minutes avant", + "thirtyMinsBefore": "30 minutes avant", + "oneHourBefore": "1 heure avant", + "twoHoursBefore": "2 heures avant", + "onDayOfEvent": "Le jour de l'événement", + "oneDayBefore": "1 jour avant", + "twoDaysBefore": "2 jours avant", + "oneWeekBefore": "1 semaine avant" + } }, "relativeDates": { "yesterday": "Hier", @@ -1087,9 +1117,11 @@ "highlight": "Surligner", "color": "Couleur", "image": "Image", + "date": "Date", "italic": "Italique", "link": "Lien", "numberedList": "Liste numérotée", + "numberedListShortForm": "Numéroté", "quote": "Citation", "strikethrough": "Barré", "text": "Texte", @@ -1188,7 +1220,10 @@ "colClear": "Effacer le ontenu", "rowClear": "Effacer le ontenu", "slashPlaceHolder": "Tapez '/' pour insérer un bloc ou commencez à écrire", - "typeSomething": "Écrivez quelque chose..." + "typeSomething": "Écrivez quelque chose...", + "quoteListShortForm": "Citation", + "mathEquationShortForm": "Formule", + "codeBlockShortForm": "Code" }, "favorite": { "noFavorite": "Aucune page favorite", @@ -1212,5 +1247,6 @@ "date": "Date", "addField": "Ajouter un champ", "userIcon": "Icône utilisateur" - } + }, + "noLogFiles": "Il n'y a pas de log" } \ No newline at end of file diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index e1870cc93b208..cc9b8cbc042ba 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -469,14 +469,14 @@ "typeAValue": "Digita un valore...", "layout": "Disposizione", "databaseLayout": "Disposizione", - "viewList": "Viste del database", "editView": "Modifica vista", "boardSettings": "Impostazioni della bacheca", "calendarSettings": "Impostazioni del calendario", "createView": "Nuova vista", "duplicateView": "Duplica vista", "deleteView": "Elimina vista", - "numberOfVisibleFields": "{} mostrato" + "numberOfVisibleFields": "{} mostrato", + "viewList": "Viste del database" }, "textFilter": { "contains": "Contiene", diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 62b5a1b60edca..5ba443feca4c8 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -478,7 +478,6 @@ "typeAValue": "Digite um valor...", "layout": "Disposição", "databaseLayout": "Disposição", - "viewList": "Visualizações de banco de dados", "editView": "Editar visualização", "boardSettings": "Configurações do quadro", "calendarSettings": "Configurações do calendário", @@ -486,7 +485,8 @@ "duplicateView": "Visualização duplicada", "deleteView": "Excluir visualização", "numberOfVisibleFields": "{} mostrando", - "Properties": "Propriedades" + "Properties": "Propriedades", + "viewList": "Visualizações de banco de dados" }, "textFilter": { "contains": "Contém", diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 70e69d725dea2..1a5776a380afa 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -472,14 +472,14 @@ "typeAValue": "Введите значение...", "layout": "Вид", "databaseLayout": "Вид базы данных", - "viewList": "Представление базы данных", "editView": "Редактировать представление", "boardSettings": "Настройки доски", "calendarSettings": "Настройки календаря", "createView": "Новое представление", "duplicateView": "Дублировать представление", "deleteView": "Удалить представление", - "numberOfVisibleFields": "{} показано" + "numberOfVisibleFields": "{} показано", + "viewList": "Представление базы данных" }, "textFilter": { "contains": "Содержит", diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 9f8017a5459d1..27923f68a54eb 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -459,12 +459,12 @@ "typeAValue": "Nhập một giá trị...", "layout": "Layout", "databaseLayout": "Layout", - "viewList": "Database Views", "editView": "Chỉnh sửa chế độ xem", "boardSettings": "Cài đặt bảng", "calendarSettings": "Cài đặt lịch", "deleteView": "Xóa chế độ xem", - "Properties": "Thuộc tính" + "Properties": "Thuộc tính", + "viewList": "Database Views" }, "textFilter": { "contains": "Chứa", diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index a5792e91335cd..1c9901005589d 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -237,8 +237,7 @@ "yes": "是", "Done": "完成", "Cancel": "取消", - "OK": "确定", - "tryAgain": "重新尝试" + "OK": "确定" }, "label": { "welcome": "欢迎!", @@ -483,7 +482,6 @@ "typeAValue": "输入一个值...", "layout": "布局", "databaseLayout": "布局", - "viewList": "数据库视图", "editView": "编辑视图", "boardSettings": "看板设置", "calendarSettings": "日历设置", @@ -491,7 +489,8 @@ "duplicateView": "复制视图", "deleteView": "删除视图", "numberOfVisibleFields": "显示 {}", - "Properties": "属性" + "Properties": "属性", + "viewList": "数据库视图" }, "textFilter": { "contains": "包含", diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 124594ea0a134..2a6cfa2411712 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -478,14 +478,14 @@ "typeAValue": "輸入一個值……", "layout": "配置", "databaseLayout": "配置", - "viewList": "資料庫檢視", "editView": "編輯檢視", "boardSettings": "看板設定", "calendarSettings": "日曆設定", "createView": "建立檢視", "duplicateView": "複製檢視", "deleteView": "刪除檢視", - "numberOfVisibleFields": "{} 顯示" + "numberOfVisibleFields": "{} 顯示", + "viewList": "資料庫檢視" }, "textFilter": { "contains": "包含", From 95c4cb241a16ebd8ad0d53482cd662d79c05b3e3 Mon Sep 17 00:00:00 2001 From: ansizheshi <89528405+ansizheshi@users.noreply.github.com> Date: Mon, 5 Feb 2024 17:39:45 +0800 Subject: [PATCH 12/50] chore: update zh-CN translations (#4599) --- frontend/resources/translations/zh-CN.json | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index 1c9901005589d..e0e0dae358358 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -61,6 +61,8 @@ "failedToLoad": "出了些问题!无法加载工作区。请尝试重启 AppFlowy。", "errorActions": { "reportIssue": "上报问题", + "reportIssueOnGithub": "在 Github 上提出 issue", + "exportLogFiles": "导出日志文件", "reachOut": "Discord 联系我们" } }, @@ -295,6 +297,7 @@ "cloudWSURLHint": "输入您的服务器的 Websocket 地址", "restartApp": "重启", "restartAppTip": "重新启动应用程序以使更改生效。请注意,这可能会注销您当前的帐户", + "changeServerTip": "更改同步服务器后,你必须单击“重启”按钮才能使更改生效", "enableEncryptPrompt": "启用加密以使用此密钥保护您的数据。安全存放;一旦启用,就无法关闭。如果丢失,您的数据将无法恢复。点击复制", "inputEncryptPrompt": "请输入您的加密密钥", "clickToCopySecret": "点击复制密钥", @@ -840,7 +843,9 @@ "saveImageToGallery": "保存图片", "failedToAddImageToGallery": "无法将片像添加到图库", "successToAddImageToGallery": "图片已成功添加到图库", - "unableToLoadImage": "无法加载图片" + "unableToLoadImage": "无法加载图片", + "maximumImageSize": "支持上传的最大图片大小为 10MB", + "uploadImageErrorImageSizeTooBig": "图片大小必须小于 10MB" }, "codeBlock": { "language": { @@ -1049,6 +1054,7 @@ "twoHoursBefore": "2小时前", "oneDayBefore": "1天前", "twoDaysBefore": "2天前", + "oneWeekBefore": "1周以前", "custom": "自定义" } }, @@ -1240,5 +1246,6 @@ "date": "日期", "addField": "添加字段", "userIcon": "用户图标" - } -} \ No newline at end of file + }, + "noLogFiles": "没有日志文件" +} From ef4bce25d816812c382146182744bb0a25ca95e8 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Mon, 5 Feb 2024 21:54:42 +0800 Subject: [PATCH 13/50] feat: sort by last modified time or created time (#4601) --- .../application/field/field_info.dart | 2 + .../appflowy_backend/ios/Classes/binding.h | 2 +- .../src/services/database_view/view_editor.rs | 55 ++++++++++-------- .../src/services/sort/controller.rs | 58 +++++++++++++------ 4 files changed, 74 insertions(+), 43 deletions(-) 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 81592e09c6b84..717776f075b53 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 @@ -71,6 +71,8 @@ class FieldInfo with _$FieldInfo { case FieldType.DateTime: case FieldType.SingleSelect: case FieldType.MultiSelect: + case FieldType.LastEditedTime: + case FieldType.CreatedTime: return true; default: return false; diff --git a/frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h b/frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h index 710af508fd51e..3fe1f39faaf71 100644 --- a/frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h +++ b/frontend/appflowy_flutter/packages/appflowy_backend/ios/Classes/binding.h @@ -3,7 +3,7 @@ #include #include -int64_t init_sdk(char *data); +int64_t init_sdk(int64_t port, char *data); void async_event(int64_t port, const uint8_t *input, uintptr_t len); 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 04e68c3b23599..42dcc256a31e1 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 @@ -160,6 +160,9 @@ impl DatabaseViewEditor { send_notification(&self.view_id, DatabaseNotification::DidUpdateViewRows) .payload(changes) .send(); + self + .gen_view_tasks(row_detail.row.id.clone(), "".to_string()) + .await; } pub async fn v_did_duplicate_row(&self, row_detail: &RowDetail) { @@ -253,30 +256,9 @@ impl DatabaseViewEditor { // Each row update will trigger a calculations, filter and sort operation. We don't want // to block the main thread, so we spawn a new task to do the work. - let row_id = row_detail.row.id.clone(); - let weak_filter_controller = Arc::downgrade(&self.filter_controller); - let weak_sort_controller = Arc::downgrade(&self.sort_controller); - let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); - af_spawn(async move { - if let Some(filter_controller) = weak_filter_controller.upgrade() { - filter_controller - .did_receive_row_changed(row_id.clone()) - .await; - } - if let Some(sort_controller) = weak_sort_controller.upgrade() { - sort_controller - .read() - .await - .did_receive_row_changed(row_id.clone()) - .await; - } - - if let Some(calculations_controller) = weak_calculations_controller.upgrade() { - calculations_controller - .did_receive_cell_changed(field_id) - .await; - } - }); + self + .gen_view_tasks(row_detail.row.id.clone(), field_id) + .await; } pub async fn v_filter_rows(&self, row_details: &mut Vec>) { @@ -1073,4 +1055,29 @@ impl DatabaseViewEditor { None } } + + async fn gen_view_tasks(&self, row_id: RowId, field_id: String) { + let weak_filter_controller = Arc::downgrade(&self.filter_controller); + let weak_sort_controller = Arc::downgrade(&self.sort_controller); + let weak_calculations_controller = Arc::downgrade(&self.calculations_controller); + af_spawn(async move { + if let Some(filter_controller) = weak_filter_controller.upgrade() { + filter_controller + .did_receive_row_changed(row_id.clone()) + .await; + } + if let Some(sort_controller) = weak_sort_controller.upgrade() { + sort_controller + .read() + .await + .did_receive_row_changed(row_id) + .await; + } + if let Some(calculations_controller) = weak_calculations_controller.upgrade() { + calculations_controller + .did_receive_cell_changed(field_id) + .await; + } + }); + } } 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 599a67303cc51..b1d709728f534 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/controller.rs @@ -17,7 +17,9 @@ use crate::entities::SortChangesetNotificationPB; use crate::entities::{FieldType, SortWithIndexPB}; use crate::services::cell::CellCache; use crate::services::database_view::{DatabaseViewChanged, DatabaseViewChangedNotifier}; -use crate::services::field::{default_order, TypeOptionCellExt}; +use crate::services::field::{ + default_order, TimestampCellData, TimestampCellDataWrapper, TypeOptionCellExt, +}; use crate::services::sort::{ ReorderAllRowsResult, ReorderSingleRowResult, Sort, SortChangeset, SortCondition, }; @@ -81,9 +83,13 @@ impl SortController { } pub async fn did_receive_row_changed(&self, row_id: RowId) { - let task_type = SortEvent::RowDidChanged(row_id); if !self.sorts.is_empty() { - self.gen_task(task_type, QualityOfService::Background).await; + self + .gen_task( + SortEvent::RowDidChanged(row_id), + QualityOfService::Background, + ) + .await; } } @@ -252,14 +258,36 @@ fn cmp_row( .find(|field_rev| field_rev.id == sort.field_id) { None => default_order(), - Some(field_rev) => cmp_cell( - left.cells.get(&sort.field_id), - right.cells.get(&sort.field_id), - field_rev, - field_type, - cell_data_cache, - sort.condition, - ), + Some(field_rev) => { + let timestamp_cells = match field_type { + FieldType::LastEditedTime | FieldType::CreatedTime => { + let (left_cell, right_cell) = if field_type.is_created_time() { + (left.created_at, right.created_at) + } else { + (left.modified_at, right.modified_at) + }; + let (left_cell, right_cell) = ( + TimestampCellDataWrapper::from((field_type, TimestampCellData::new(left_cell))), + TimestampCellDataWrapper::from((field_type, TimestampCellData::new(right_cell))), + ); + Some((Some(left_cell.into()), Some(right_cell.into()))) + }, + _ => None, + }; + + cmp_cell( + timestamp_cells + .as_ref() + .map_or_else(|| left.cells.get(&sort.field_id), |cell| cell.0.as_ref()), + timestamp_cells + .as_ref() + .map_or_else(|| right.cells.get(&sort.field_id), |cell| cell.1.as_ref()), + field_rev, + field_type, + cell_data_cache, + sort.condition, + ) + }, } } @@ -276,13 +304,7 @@ fn cmp_cell( { None => default_order(), Some(handler) => { - let cal_order = || { - let order = - handler.handle_cell_compare(left_cell, right_cell, field.as_ref(), sort_condition); - Option::::Some(order) - }; - - cal_order().unwrap_or_else(default_order) + handler.handle_cell_compare(left_cell, right_cell, field.as_ref(), sort_condition) }, } } From 271f781ccea762aa41437d90348b03008b88ea58 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 5 Feb 2024 21:53:14 +0700 Subject: [PATCH 14/50] fix: application will raise an error when selecting unsplash image in local mode (#4604) --- .../lib/shared/appflowy_network_image.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart index 4d89e4eef3798..72c6be583e380 100644 --- a/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart +++ b/frontend/appflowy_flutter/lib/shared/appflowy_network_image.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:appflowy/shared/custom_image_cache_manager.dart'; import 'package:appflowy/util/string_extension.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; @@ -56,7 +57,12 @@ class FlowyNetworkImage extends StatelessWidget { final header = {}; final token = userProfilePB?.token; if (token != null) { - header['Authorization'] = 'Bearer ${jsonDecode(token)['access_token']}'; + try { + final decodedToken = jsonDecode(token); + header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; + } catch (e) { + Log.error('unable to decode token: $e'); + } } return header; } From a9c6b80f4a0899fb235da4aad6d887621bf8c469 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:46:36 +0800 Subject: [PATCH 15/50] chore: add spacing between field buttons in setting popover (#4612) --- .../database/widgets/setting/setting_property_list.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 fd8958cea661b..97a0cf12992d1 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 @@ -15,7 +15,6 @@ import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:styled_widget/styled_widget.dart'; class DatabasePropertyList extends StatefulWidget { const DatabasePropertyList({ @@ -86,7 +85,7 @@ class _DatabasePropertyListState extends State { .add(DatabasePropertyEvent.moveField(from, to)); }, onReorderStart: (_) => _popoverMutex.close(), - padding: const EdgeInsets.symmetric(vertical: 6.0), + padding: const EdgeInsets.symmetric(vertical: 4.0), children: cells, ); }, @@ -138,8 +137,9 @@ class _DatabasePropertyCellState extends State { constraints: BoxConstraints.loose(const Size(240, 400)), triggerActions: PopoverTriggerFlags.none, margin: EdgeInsets.zero, - child: SizedBox( + child: Container( height: GridSize.popoverItemHeight, + margin: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), child: FlowyButton( hoverColor: AFThemeExtension.of(context).lightGreyHover, text: FlowyText.medium( @@ -192,7 +192,7 @@ class _DatabasePropertyCellState extends State { icon: visibleIcon, ), onTap: () => _popoverController.show(), - ).padding(horizontal: 6.0), + ), ), popupBuilder: (BuildContext context) { return FieldEditor( From b781c9aa0f1f0ac52c6cff213c87631d4b944669 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 7 Feb 2024 10:47:17 +0800 Subject: [PATCH 16/50] fix: deleting a sorting field doesn't delete existing sorts (#4611) --- .../src/services/database_view/view_editor.rs | 43 +++++++++++++------ .../src/services/sort/entities.rs | 18 +++----- 2 files changed, 34 insertions(+), 27 deletions(-) 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 42dcc256a31e1..48e731cbe15ac 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 @@ -769,6 +769,35 @@ impl DatabaseViewEditor { Ok(()) } + pub async fn v_did_delete_field(&self, deleted_field_id: &str) { + let sorts = self.delegate.get_all_sorts(&self.view_id); + + if let Some(sort) = sorts.iter().find(|sort| sort.field_id == deleted_field_id) { + self.delegate.remove_sort(&self.view_id, &sort.id); + let notification = self + .sort_controller + .write() + .await + .apply_changeset(SortChangeset::from_delete(sort.id.clone())) + .await; + if !notification.is_empty() { + notify_did_update_sort(notification).await; + } + } + + self + .calculations_controller + .did_receive_field_deleted(deleted_field_id.to_string()) + .await; + } + + pub async fn v_did_update_field_type(&self, field_id: &str, new_field_type: &FieldType) { + self + .calculations_controller + .did_receive_field_type_changed(field_id.to_owned(), new_field_type.to_owned()) + .await; + } + /// Notifies the view's field type-option data is changed /// For the moment, only the groups will be generated after the type-option data changed. A /// [Field] has a property named type_options contains a list of type-option data. @@ -1023,20 +1052,6 @@ impl DatabaseViewEditor { Ok(()) } - pub async fn v_did_delete_field(&self, field_id: &str) { - self - .calculations_controller - .did_receive_field_deleted(field_id.to_owned()) - .await; - } - - pub async fn v_did_update_field_type(&self, field_id: &str, new_field_type: &FieldType) { - self - .calculations_controller - .did_receive_field_type_changed(field_id.to_owned(), new_field_type.to_owned()) - .await; - } - async fn mut_group_controller(&self, f: F) -> Option where F: FnOnce(&mut Box, Field) -> FlowyResult, 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 15cb7d4c2a1d5..68246f8b1eac5 100644 --- a/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs +++ b/frontend/rust-lib/flowy-database2/src/services/sort/entities.rs @@ -121,7 +121,7 @@ pub struct ReorderSingleRowResult { pub new_index: usize, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct SortChangeset { pub(crate) insert_sort: Option, pub(crate) update_sort: Option, @@ -133,36 +133,28 @@ impl SortChangeset { pub fn from_insert(sort: Sort) -> Self { Self { insert_sort: Some(sort), - update_sort: None, - delete_sort: None, - reorder_sort: None, + ..Default::default() } } pub fn from_update(sort: Sort) -> Self { Self { - insert_sort: None, update_sort: Some(sort), - delete_sort: None, - reorder_sort: None, + ..Default::default() } } pub fn from_delete(sort_id: String) -> Self { Self { - insert_sort: None, - update_sort: None, delete_sort: Some(sort_id), - reorder_sort: None, + ..Default::default() } } pub fn from_reorder(from_sort_id: String, to_sort_id: String) -> Self { Self { - insert_sort: None, - update_sort: None, - delete_sort: None, reorder_sort: Some((from_sort_id, to_sort_id)), + ..Default::default() } } } From db5372c18efb3f821a80c6874c49c561da42c727 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Wed, 7 Feb 2024 05:17:48 +0100 Subject: [PATCH 17/50] feat: hotkey to open settings dialog (#4620) * feat: hotkey to open settings dialog * chore: change to cmd/ctrl+, --- .../workspace/presentation/home/hotkeys.dart | 7 ++ .../home/menu/sidebar/sidebar_user.dart | 103 ++++++++++++------ 2 files changed, 79 insertions(+), 31 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart index 1611c422853aa..2e1dfc2b5abe4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -7,6 +7,7 @@ import 'package:appflowy/workspace/application/home/home_setting_bloc.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_user.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:provider/provider.dart'; @@ -107,9 +108,15 @@ class HomeHotKeys extends StatelessWidget { getIt().add(const RenameViewEvent.open()), ).register(); + _asyncRegistration(context); + return child; } + Future _asyncRegistration(BuildContext context) async { + (await openSettingsHotKey(context))?.register(); + } + void _selectTab(BuildContext context, int change) { final bloc = context.read(); bloc.add(TabsEvent.selectTab(bloc.state.currentIndex + change)); 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 bfebad71e779a..0532d8cfabb87 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 @@ -1,8 +1,12 @@ +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/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/startup/startup.dart'; +import 'package:appflowy/user/application/auth/auth_service.dart'; import 'package:appflowy/workspace/application/menu/menu_user_bloc.dart'; +import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; import 'package:appflowy/workspace/presentation/notifications/widgets/notification_button.dart'; import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart'; import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart'; @@ -11,11 +15,42 @@ 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'; +import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:flowy_infra_ui/style_widget/text.dart'; import 'package:flowy_infra_ui/widget/flowy_tooltip.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; + +final GlobalKey _settingsDialogKey = GlobalKey(); + +Future openSettingsHotKey(BuildContext context) async { + final userProfileOrFailure = await getIt().getUser(); + + return userProfileOrFailure.fold( + (e) { + Log.error('Failed to get user $e'); + return null; + }, + (userProfile) => HotKeyItem( + hotKey: HotKey( + KeyCode.comma, + scope: HotKeyScope.inapp, + modifiers: [ + PlatformExtension.isMacOS ? KeyModifier.meta : KeyModifier.control, + ], + ), + keyDownHandler: (_) { + if (_settingsDialogKey.currentContext == null) { + _showSettingsDialog(context, userProfile); + } else { + Navigator.of(context, rootNavigator: true) + .popUntil((route) => route.isFirst); + } + }, + ), + ); +} class SidebarUser extends StatelessWidget { const SidebarUser({ @@ -83,36 +118,7 @@ class UserSettingButton extends StatelessWidget { return FlowyTooltip( message: LocaleKeys.settings_menu_open.tr(), child: IconButton( - onPressed: () { - showDialog( - context: context, - builder: (dialogContext) { - return BlocProvider.value( - value: BlocProvider.of(dialogContext), - child: SettingsDialog( - userProfile, - didLogout: () async { - // Pop the dialog using the dialog context - Navigator.of(dialogContext).pop(); - await runAppFlowy(); - }, - dismissDialog: () { - if (Navigator.of(dialogContext).canPop()) { - Navigator.of(dialogContext).pop(); - } else { - Log.warn("Can't pop dialog context"); - } - }, - restartApp: () async { - // Pop the dialog using the dialog context - Navigator.of(dialogContext).pop(); - await runAppFlowy(); - }, - ), - ); - }, - ); - }, + onPressed: () => _showSettingsDialog(context, userProfile), icon: SizedBox.square( dimension: 20, child: FlowySvg( @@ -124,3 +130,38 @@ class UserSettingButton extends StatelessWidget { ); } } + +void _showSettingsDialog( + BuildContext context, + UserProfilePB userProfile, +) { + showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + key: _settingsDialogKey, + value: BlocProvider.of(dialogContext), + child: SettingsDialog( + userProfile, + didLogout: () async { + // Pop the dialog using the dialog context + Navigator.of(dialogContext).pop(); + await runAppFlowy(); + }, + dismissDialog: () { + if (Navigator.of(dialogContext).canPop()) { + Navigator.of(dialogContext).pop(); + } else { + Log.warn("Can't pop dialog context"); + } + }, + restartApp: () async { + // Pop the dialog using the dialog context + Navigator.of(dialogContext).pop(); + await runAppFlowy(); + }, + ), + ); + }, + ); +} From 3129fa6cc19df894e344d19b93e1cdf00f1a8c1a Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Wed, 7 Feb 2024 05:18:13 +0100 Subject: [PATCH 18/50] fix: use cmd+. to toggle sidebar on macOS (#4621) * fix: use cmd+. to hide sidebar on macOS * fix: update missed tooltip --- .../lib/workspace/presentation/home/hotkeys.dart | 2 +- .../presentation/home/menu/sidebar/sidebar_top_menu.dart | 3 +-- .../lib/workspace/presentation/home/navigation.dart | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart index 2e1dfc2b5abe4..e49c2786b3fa5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/hotkeys.dart @@ -43,7 +43,7 @@ class HomeHotKeys extends StatelessWidget { // Collapse sidebar menu HotKeyItem( hotKey: HotKey( - KeyCode.backslash, + Platform.isMacOS ? KeyCode.period : KeyCode.backslash, modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control], // Set hotkey scope (default is HotKeyScope.system) scope: HotKeyScope.inapp, // Set as inapp-wide hotkey. 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 ef044b06d73e8..1c7452584abbe 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 @@ -65,8 +65,7 @@ class SidebarTopMenu extends StatelessWidget { text: '${LocaleKeys.sideBar_closeSidebar.tr()}\n', ), TextSpan( - // TODO(Lucas.Xu): it doesn't work on macOS. - text: Platform.isMacOS ? '⌘+\\' : 'Ctrl+\\', + text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\', ), ], ); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart index d0c595b5bbaab..8d5a0259eeec1 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/navigation.dart @@ -184,7 +184,7 @@ TextSpan sidebarTooltipTextSpan(BuildContext context, String hintText) => text: "$hintText\n", ), TextSpan( - text: Platform.isMacOS ? "⌘+\\" : "Ctrl+\\", + text: Platform.isMacOS ? "⌘+." : "Ctrl+\\", ), ], ); From 88e9d63a13fc175e1df65c08c583f18e732f1f45 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:20:26 +0800 Subject: [PATCH 19/50] fix: show newChecklistTask on hover over entire list item in row detail page (#4622) --- .../desktop_row_detail_checklist_cell.dart | 38 ++++++------- .../widgets/cell/editable_cell_builder.dart | 54 ++++++------------- .../cell/editable_cell_skeleton/checkbox.dart | 4 +- .../editable_cell_skeleton/checklist.dart | 2 +- .../cell/editable_cell_skeleton/date.dart | 2 +- .../cell/editable_cell_skeleton/number.dart | 2 +- .../editable_cell_skeleton/select_option.dart | 2 +- .../cell/editable_cell_skeleton/text.dart | 2 +- .../editable_cell_skeleton/timestamp.dart | 2 +- .../widgets/row/cells/cell_container.dart | 10 ++-- .../database/widgets/row/row_property.dart | 10 +++- 11 files changed, 53 insertions(+), 75 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart index 255dbdd38be54..3c4576e4c38c1 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_checklist_cell.dart @@ -10,7 +10,7 @@ import 'package:easy_localization/easy_localization.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'; +import 'package:provider/provider.dart'; import '../editable_cell_skeleton/checklist.dart'; @@ -113,40 +113,34 @@ class _ChecklistItemsState extends State { ), const VSpace(4), ...children, - const ChecklistItemControl(), + ChecklistItemControl(cellNotifer: widget.cellContainerNotifier), ], ), ); } } -class ChecklistItemControl extends StatefulWidget { - const ChecklistItemControl({super.key}); +class ChecklistItemControl extends StatelessWidget { + const ChecklistItemControl({super.key, required this.cellNotifer}); - @override - State createState() => _ChecklistItemControlState(); -} - -class _ChecklistItemControlState extends State { - bool _isHover = false; + final CellContainerNotifier cellNotifer; @override Widget build(BuildContext context) { - return MouseRegion( - onHover: (_) => setState(() => _isHover = true), - onExit: (_) => setState(() => _isHover = false), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => context - .read() - .add(const ChecklistCellEvent.createNewTask("")), - child: Padding( - padding: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), - child: SizedBox( + return ChangeNotifierProvider.value( + value: cellNotifer, + child: Consumer( + builder: (buildContext, notifier, _) => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context + .read() + .add(const ChecklistCellEvent.createNewTask("")), + child: Container( + margin: const EdgeInsets.fromLTRB(8.0, 4.0, 8.0, 0), height: 12, child: AnimatedSwitcher( duration: const Duration(milliseconds: 150), - child: _isHover + child: notifier.isHover ? FlowyTooltip( message: LocaleKeys.grid_checklist_addNew.tr(), child: Row( diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart index a5e190400e0bf..e49347eeafa5d 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart @@ -190,11 +190,9 @@ class EditableCellBuilder { } abstract class CellEditable { - RequestFocusListener get requestFocus; + SingleListenerChangeNotifier get requestFocus; CellContainerNotifier get cellContainerNotifier; - - // ValueNotifier get onCellEditing; } typedef AccessoryBuilder = List Function( @@ -204,9 +202,6 @@ typedef AccessoryBuilder = List Function( abstract class CellAccessory extends Widget { const CellAccessory({super.key}); - // The hover will show if the isHover's value is true - ValueNotifier? get onAccessoryHover; - AccessoryBuilder? get accessoryBuilder; } @@ -217,20 +212,11 @@ abstract class EditableCellWidget extends StatefulWidget @override final CellContainerNotifier cellContainerNotifier = CellContainerNotifier(); - // When the cell is focused, we assume that the accessory also be hovered. - @override - ValueNotifier get onAccessoryHover => ValueNotifier(false); - - // @override - // final ValueNotifier onCellEditing = ValueNotifier(false); - @override - List Function( - GridCellAccessoryBuildContext buildContext, - )? get accessoryBuilder => null; + AccessoryBuilder? get accessoryBuilder => null; @override - final RequestFocusListener requestFocus = RequestFocusListener(); + final requestFocus = SingleListenerChangeNotifier(); @override final Map shortcutHandlers = {}; @@ -240,28 +226,25 @@ abstract class GridCellState extends State { @override void initState() { super.initState(); - - widget.requestFocus.setListener(requestBeginFocus); + widget.requestFocus.addListener(onRequestFocus); } @override void didUpdateWidget(covariant T oldWidget) { if (oldWidget != this) { - widget.requestFocus.setListener(requestBeginFocus); + widget.requestFocus.addListener(onRequestFocus); } super.didUpdateWidget(oldWidget); } @override void dispose() { - widget.onAccessoryHover.dispose(); - widget.requestFocus.removeAllListener(); widget.requestFocus.dispose(); super.dispose(); } /// Subclass can override this method to request focus. - void requestBeginFocus(); + void onRequestFocus(); String? onCopy() => null; } @@ -287,7 +270,7 @@ abstract class GridEditableTextCell } @override - void requestBeginFocus() { + void onRequestFocus() { if (!focusNode.hasFocus && focusNode.canRequestFocus) { FocusScope.of(context).requestFocus(focusNode); } @@ -304,28 +287,25 @@ abstract class GridEditableTextCell Future focusChanged() async {} } -class RequestFocusListener extends ChangeNotifier { +class SingleListenerChangeNotifier extends ChangeNotifier { VoidCallback? _listener; - void setListener(VoidCallback listener) { + @override + void addListener(VoidCallback listener) { if (_listener != null) { removeListener(_listener!); } - _listener = listener; - addListener(listener); + super.addListener(listener); } - void removeAllListener() { - if (_listener != null) { - removeListener(_listener!); - _listener = null; - } + @override + void dispose() { + _listener = null; + super.dispose(); } - void notify() { - notifyListeners(); - } + void notify() => notifyListeners(); } class SingleListenerFocusNode extends FocusNode { @@ -374,7 +354,7 @@ class EditableCellSkinMap { FieldType.Checklist => checklistSkin != null, FieldType.CreatedTime || FieldType.LastEditedTime => - throw timestampSkin != null, + timestampSkin != null, FieldType.DateTime => dateSkin != null, FieldType.MultiSelect || FieldType.SingleSelect => diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart index 41d7e22ed641c..4b7bd2c442d00 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart @@ -80,9 +80,7 @@ class _CheckboxCellState extends GridCellState { } @override - void requestBeginFocus() { - cellBloc.add(const CheckboxCellEvent.select()); - } + void onRequestFocus() => cellBloc.add(const CheckboxCellEvent.select()); @override String? onCopy() { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart index 434cd19e8f430..2f3407aea6714 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart @@ -85,7 +85,7 @@ class GridChecklistCellState extends GridCellState { } @override - void requestBeginFocus() { + void onRequestFocus() { if (widget.skin is DesktopGridChecklistCellSkin) { _popover.show(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart index bfabe0e97ff61..d6c2ae84346e4 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/date.dart @@ -84,7 +84,7 @@ class _DateCellState extends GridCellState { } @override - void requestBeginFocus() { + void onRequestFocus() { _popover.show(); widget.cellContainerNotifier.isFocus = true; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart index bf5f887a9fcb5..689b411550e2b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/number.dart @@ -96,7 +96,7 @@ class _NumberCellState extends GridEditableTextCell { SingleListenerFocusNode focusNode = SingleListenerFocusNode(); @override - void requestBeginFocus() { + void onRequestFocus() { focusNode.requestFocus(); } 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 08f6c1c75039a..de3908e585ba1 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 @@ -92,5 +92,5 @@ class _SelectOptionCellState extends GridCellState { } @override - void requestBeginFocus() => _popover.show(); + void onRequestFocus() => _popover.show(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart index 8e3c06edcaf14..622b6a471d112 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/text.dart @@ -97,7 +97,7 @@ class _TextCellState extends GridEditableTextCell { SingleListenerFocusNode focusNode = SingleListenerFocusNode(); @override - void requestBeginFocus() { + void onRequestFocus() { focusNode.requestFocus(); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart index 7391971775022..5296ec79436d7 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart @@ -83,7 +83,7 @@ class _TimestampCellState extends GridCellState { } @override - void requestBeginFocus() { + void onRequestFocus() { widget.cellContainerNotifier.isFocus = true; } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart index 82ff1cce087b8..a878192106d04 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/row/cells/cell_container.dart @@ -98,7 +98,7 @@ class _GridCellEnterRegion extends StatelessWidget { return Selector2( selector: (context, regionNotifier, cellNotifier) => !cellNotifier.isFocus && - (cellNotifier.onEnter || regionNotifier.onEnter && isPrimary), + (cellNotifier.isHover || regionNotifier.onEnter && isPrimary), builder: (context, showAccessory, _) { final List children = [child]; @@ -113,9 +113,9 @@ class _GridCellEnterRegion extends StatelessWidget { return MouseRegion( cursor: SystemMouseCursors.click, onEnter: (p) => - CellContainerNotifier.of(context, listen: false).onEnter = true, + CellContainerNotifier.of(context, listen: false).isHover = true, onExit: (p) => - CellContainerNotifier.of(context, listen: false).onEnter = false, + CellContainerNotifier.of(context, listen: false).isHover = false, child: Stack( alignment: AlignmentDirectional.center, fit: StackFit.expand, @@ -138,7 +138,7 @@ class CellContainerNotifier extends ChangeNotifier { } } - set onEnter(bool value) { + set isHover(bool value) { if (_onEnter != value) { _onEnter = value; notifyListeners(); @@ -147,7 +147,7 @@ class CellContainerNotifier extends ChangeNotifier { bool get isFocus => _isFocus; - bool get onEnter => _onEnter; + bool get isHover => _onEnter; static CellContainerNotifier of(BuildContext context, {bool listen = true}) { return Provider.of(context, listen: listen); 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 1718d3a67e155..a5b92ab9cbf6a 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 @@ -189,8 +189,14 @@ class _PropertyCellState extends State<_PropertyCell> { margin: const EdgeInsets.only(bottom: 8), constraints: const BoxConstraints(minHeight: 30), child: MouseRegion( - onEnter: (event) => _isFieldHover.value = true, - onExit: (event) => _isFieldHover.value = false, + onEnter: (event) { + _isFieldHover.value = true; + cell.cellContainerNotifier.isHover = true; + }, + onExit: (event) { + _isFieldHover.value = false; + cell.cellContainerNotifier.isHover = false; + }, child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ From 0db21b91a3eea8b26c04c2f7731e2e7904ea3157 Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:49:53 +0800 Subject: [PATCH 20/50] fix: unequal grid header and body widths (#4626) --- .../database/grid/presentation/widgets/header/grid_header.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart index 26eae515cf46a..b9ba4f074966d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/grid_header.dart @@ -7,6 +7,7 @@ import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart import 'package:appflowy_backend/log.dart'; import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:easy_localization/easy_localization.dart'; +import 'package:flowy_infra/size.dart'; import 'package:flowy_infra/theme_extension.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flutter/material.dart'; @@ -151,6 +152,7 @@ class _CellTrailing extends StatelessWidget { Widget build(BuildContext context) { return Container( width: GridSize.trailHeaderPadding, + margin: EdgeInsets.only(right: GridSize.scrollBarSize + Insets.m), decoration: BoxDecoration( border: Border( bottom: BorderSide(color: Theme.of(context).dividerColor), From 92ceb2cd7d685ca188a17f22224c11e0f0210779 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Wed, 7 Feb 2024 18:38:53 +0100 Subject: [PATCH 21/50] fix: dispose of resources in time (#4625) * fix: limit length when renaming views * chore: clean up database listeners * fix: dispose of controllers properly * fix: dispose of resources properly * fix: deleting filters with same name * chore: extend DatabaseTabBarItemBuilder * fix: null check on null value --- .../bottom_sheet_edit_link_widget.dart | 1 - .../bottom_sheet_rename_widget.dart | 2 - .../database/board/mobile_board_content.dart | 6 ++ .../board/widgets/mobile_board_trailing.dart | 6 ++ .../field/mobile_full_field_editor.dart | 2 - .../view/edit_database_view_screen.dart | 6 ++ .../edit_username_bottom_sheet.dart | 1 + .../cell/bloc/checkbox_cell_bloc.dart | 21 ++-- .../cell/bloc/checklist_cell_bloc.dart | 20 ++-- .../application/cell/bloc/date_cell_bloc.dart | 19 ++-- .../cell/bloc/number_cell_bloc.dart | 20 ++-- .../cell/bloc/select_option_cell_bloc.dart | 20 ++-- .../application/cell/bloc/text_cell_bloc.dart | 20 ++-- .../cell/bloc/timestamp_cell_bloc.dart | 20 ++-- .../application/cell/bloc/url_cell_bloc.dart | 20 ++-- .../application/layout/layout_listener.dart | 52 --------- .../setting/setting_controller.dart | 62 ----------- .../database/application/tab_bar_bloc.dart | 5 + .../board/presentation/board_page.dart | 2 +- .../application/calendar_setting_bloc.dart | 12 +-- .../calendar/presentation/calendar_page.dart | 2 +- .../calculations/calculations_bloc.dart | 1 + .../application/filter/filter_menu_bloc.dart | 2 +- .../database/grid/presentation/grid_page.dart | 8 +- .../grid/presentation/mobile_grid_page.dart | 2 +- .../widgets/filter/filter_menu.dart | 7 +- .../grid/presentation/widgets/row/row.dart | 12 +-- .../database/tab_bar/tab_bar_view.dart | 6 ++ .../database/widgets/card/card_bloc.dart | 22 ++-- .../widgets/cell/editable_cell_builder.dart | 19 ++-- .../cell/editable_cell_skeleton/url.dart | 1 + .../mobile_row_detail_url_cell.dart | 4 +- .../cell_editor/checklist_cell_editor.dart | 14 +++ .../cell_editor/select_option_editor.dart | 1 + .../cell_editor/select_option_text_field.dart | 1 - .../find_and_replace_menu.dart | 9 +- .../editor_plugins/header/cover_editor.dart | 12 +-- .../header/custom_cover_picker.dart | 10 +- .../inline_math_equation.dart | 6 ++ .../math_equation_block_component.dart | 4 +- .../appflowy_mobile_toolbar.dart | 1 - .../widgets/inline_actions_handler.dart | 7 ++ .../lib/plugins/trash/trash_page.dart | 7 ++ .../screens/encrypt_secret_screen.dart | 7 ++ .../mobile_workspace_start_screen.dart | 8 +- .../workspace/application/tabs/tabs_bloc.dart | 6 ++ .../application/tabs/tabs_state.dart | 6 ++ .../presentation/home/home_stack.dart | 4 + .../home/menu/view/view_item.dart | 1 + .../src/default_emoji_picker_view.dart | 4 +- .../widgets/setting_appflowy_cloud.dart | 12 +-- .../widgets/setting_supabase_cloud.dart | 12 +-- .../settings_customize_shortcuts_view.dart | 4 +- .../settings/widgets/settings_user_view.dart | 14 +-- .../presentation/widgets/dialogs.dart | 19 +++- .../widgets/rename_view_popover.dart | 2 + .../packages/flowy_infra/lib/notifier.dart | 6 -- .../example/lib/keyboard/keyboard_screen.dart | 6 ++ .../scrolling/styled_scrollview.dart | 4 +- .../lib/style_widget/text_field.dart | 26 +++-- .../lib/style_widget/text_input.dart | 101 +++++++++++------- 61 files changed, 383 insertions(+), 334 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_listener.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_controller.dart diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart index 871811c2bfd9a..bed69e1cc4298 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart @@ -44,7 +44,6 @@ class _MobileBottomSheetEditLinkWidgetState void dispose() { textController.dispose(); hrefController.dispose(); - super.dispose(); } 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 1d40bd6e5d363..618e19e53d6a5 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 @@ -27,14 +27,12 @@ class _MobileBottomSheetRenameWidgetState @override void initState() { super.initState(); - controller = TextEditingController(text: widget.name); } @override void dispose() { controller.dispose(); - super.dispose(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart index f050945665858..c01f3ca048c10 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_content.dart @@ -38,6 +38,12 @@ class _MobileBoardContentState extends State { scrollController = ScrollController(); } + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final screenWidth = MediaQuery.of(context).size.width; diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart index 1c6f8090fd012..2cc59d73d3056 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_board_trailing.dart @@ -19,6 +19,12 @@ class _MobileBoardTrailingState extends State { bool isEditing = false; + @override + void dispose() { + _textController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; 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 ae4af39007b06..bea19ff0a4668 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 @@ -196,7 +196,6 @@ class _MobileFieldEditorState extends State { @override void initState() { super.initState(); - values = widget.defaultValues; controller.text = values.name; } @@ -204,7 +203,6 @@ class _MobileFieldEditorState extends State { @override void dispose() { controller.dispose(); - super.dispose(); } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart index 9603f28803c10..a612d74e33033 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart @@ -83,6 +83,12 @@ class _NameAndIconState extends State<_NameAndIcon> { textEditingController.text = widget.view.name; } + @override + void dispose() { + textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return FlowyOptionTile.textField( diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart index d88747ea244dc..04fe9e9cf7bad 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/personal_info/edit_username_bottom_sheet.dart @@ -23,6 +23,7 @@ class _EditUsernameBottomSheetState extends State { late TextEditingController _textFieldController; final _formKey = GlobalKey(); + @override void initState() { super.initState(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart index 0d3b1d37eee20..eca21c8ba361b 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checkbox_cell_bloc.dart @@ -16,6 +16,16 @@ class CheckboxCellBloc extends Bloc { final CheckboxCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) { @@ -35,17 +45,6 @@ class CheckboxCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (cellData) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart index 50bace5827ac3..f315e8f768ca2 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/checklist_cell_bloc.dart @@ -32,6 +32,16 @@ class ChecklistCellBloc extends Bloc { final ChecklistCellBackendService _checklistCellService; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -79,16 +89,6 @@ class ChecklistCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (data) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart index 409ae12576689..9497f28ff5a35 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/date_cell_bloc.dart @@ -17,6 +17,16 @@ class DateCellBloc extends Bloc { final DateCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -35,15 +45,6 @@ class DateCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (data) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart index 8b6b95d1082ec..8593c42f1e989 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/number_cell_bloc.dart @@ -15,6 +15,16 @@ class NumberCellBloc extends Bloc { final NumberCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -45,16 +55,6 @@ class NumberCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (cellContent) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart index 392db0492c0dd..eb1ff6979031a 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_bloc.dart @@ -17,6 +17,16 @@ class SelectOptionCellBloc final SelectOptionCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -36,16 +46,6 @@ class SelectOptionCellBloc ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (selectOptionCellData) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart index 3c57f04749a97..3a2371cf4b834 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/text_cell_bloc.dart @@ -15,6 +15,16 @@ class TextCellBloc extends Bloc { final TextCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) { @@ -41,16 +51,6 @@ class TextCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (cellContent) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart index c757b44efa080..240ce7182ca1d 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/timestamp_cell_bloc.dart @@ -17,6 +17,16 @@ class TimestampCellBloc extends Bloc { final TimestampCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -35,16 +45,6 @@ class TimestampCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (data) { 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 6ab0086e26c8c..dbd2258cc143e 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 @@ -16,6 +16,16 @@ class URLCellBloc extends Bloc { final URLCellController cellController; void Function()? _onCellChangedFn; + @override + Future close() async { + if (_onCellChangedFn != null) { + cellController.removeListener(_onCellChangedFn!); + _onCellChangedFn = null; + } + await cellController.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -39,16 +49,6 @@ class URLCellBloc extends Bloc { ); } - @override - Future close() async { - if (_onCellChangedFn != null) { - cellController.removeListener(_onCellChangedFn!); - _onCellChangedFn = null; - } - await cellController.dispose(); - return super.close(); - } - void _startListening() { _onCellChangedFn = cellController.addListener( onCellChanged: (cellData) { diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_listener.dart deleted file mode 100644 index 1f3301f5b8dd8..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/layout/layout_listener.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'dart:typed_data'; - -import 'package:appflowy/core/notification/grid_notification.dart'; -import 'package:flowy_infra/notifier.dart'; -import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:dartz/dartz.dart'; - -/// Listener for database layout changes. -class DatabaseLayoutListener { - DatabaseLayoutListener(this.viewId); - - final String viewId; - - PublishNotifier>? _layoutNotifier = - PublishNotifier(); - DatabaseNotificationListener? _listener; - - void start({ - required void Function(Either) - onLayoutChanged, - }) { - _layoutNotifier?.addPublishListener(onLayoutChanged); - _listener = DatabaseNotificationListener( - objectId: viewId, - handler: _handler, - ); - } - - void _handler( - DatabaseNotification ty, - Either result, - ) { - switch (ty) { - case DatabaseNotification.DidUpdateDatabaseLayout: - result.fold( - (payload) => _layoutNotifier?.value = - left(DatabaseLayoutMetaPB.fromBuffer(payload).layout), - (error) => _layoutNotifier?.value = right(error), - ); - break; - default: - break; - } - } - - Future stop() async { - await _listener?.stop(); - _layoutNotifier?.dispose(); - _layoutNotifier = null; - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_controller.dart deleted file mode 100644 index 2147b4d1ded84..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/database/application/setting/setting_controller.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart'; -import 'setting_listener.dart'; -import 'setting_service.dart'; - -typedef OnError = void Function(FlowyError); -typedef OnSettingUpdated = void Function(DatabaseViewSettingPB); - -class SettingController { - SettingController({ - required this.viewId, - }) : _settingBackendSvc = SettingBackendService(viewId: viewId), - _listener = DatabaseSettingListener(viewId: viewId) { - // Load setting - _settingBackendSvc.getSetting().then((result) { - result.fold( - (newSetting) => updateSetting(newSetting), - (err) => _onError?.call(err), - ); - }); - - // Listen on the setting changes - _listener.start( - onSettingUpdated: (result) { - result.fold( - (newSetting) => updateSetting(newSetting), - (err) => _onError?.call(err), - ); - }, - ); - } - - final String viewId; - final SettingBackendService _settingBackendSvc; - final DatabaseSettingListener _listener; - - OnSettingUpdated? _onSettingUpdated; - OnError? _onError; - DatabaseViewSettingPB? _setting; - DatabaseViewSettingPB? get setting => _setting; - - void startListening({ - required OnSettingUpdated onSettingUpdated, - required OnError onError, - }) { - assert(_onSettingUpdated == null, 'Should call once'); - assert(_onError == null, 'Should call once'); - _onSettingUpdated = onSettingUpdated; - _onError = onError; - } - - void updateSetting(DatabaseViewSettingPB newSetting) { - _setting = newSetting; - _onSettingUpdated?.call(newSetting); - } - - void dispose() { - _onSettingUpdated = null; - _onError = null; - _listener.stop(); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart index 8f7a8b948fa92..6d5f0b4984626 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/application/tab_bar_bloc.dart @@ -131,6 +131,7 @@ class DatabaseTabBarBloc Future close() async { for (final tabBar in state.tabBars) { await state.tabBarControllerByViewId[tabBar.viewId]?.dispose(); + tabBar.dispose(); } return super.close(); } @@ -261,6 +262,10 @@ class DatabaseTabBar extends Equatable { @override List get props => [view.hashCode]; + + void dispose() { + _builder.dispose(); + } } typedef OnViewUpdated = void Function(ViewPB newView); 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 4503fa2b20cb5..16b2d45f953fe 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 @@ -33,7 +33,7 @@ import '../application/board_bloc.dart'; import 'toolbar/board_setting_bar.dart'; import 'widgets/board_hidden_groups.dart'; -class BoardPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { +class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { @override Widget content( BuildContext context, diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart index ee4f34586b7cf..583575688735f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/application/calendar_setting_bloc.dart @@ -26,6 +26,12 @@ class CalendarSettingBloc final DatabaseController _databaseController; final DatabaseLayoutSettingListener _listener; + @override + Future close() async { + await _listener.stop(); + return super.close(); + } + void _dispatch() { on((event, emit) { event.when( @@ -108,12 +114,6 @@ class CalendarSettingBloc }, ); } - - @override - Future close() async { - await _listener.stop(); - return super.close(); - } } @freezed diff --git a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart index 756c385067bfd..9382ad3de5579 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/calendar/presentation/calendar_page.dart @@ -30,7 +30,7 @@ import 'calendar_day.dart'; import 'layout/sizes.dart'; import 'toolbar/calendar_setting_bar.dart'; -class CalendarPageTabBarBuilderImpl implements DatabaseTabBarItemBuilder { +class CalendarPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder { @override Widget content( BuildContext context, diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart index 0eb60dd3eb52d..28d8b820e8caf 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/calculations/calculations_bloc.dart @@ -29,6 +29,7 @@ class CalculationsBloc extends Bloc { @override Future close() async { _fieldController.removeListener(onFieldsListener: _onReceiveFields); + await _calculationsListener.stop(); await super.close(); } 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 6c98287ad757a..08e45305dea3a 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 @@ -73,7 +73,7 @@ class GridFilterMenuBloc } @override - Future close() { + Future close() async { if (_onFilterFn != null) { fieldController.removeListener(onFiltersListener: _onFilterFn!); _onFilterFn = null; diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart index 6437be29a2440..bca5f06dfb175 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/grid_page.dart @@ -45,7 +45,7 @@ class ToggleExtensionNotifier extends ChangeNotifier { } } -class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { +class DesktopGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { final _toggleExtension = ToggleExtensionNotifier(); @override @@ -86,6 +86,12 @@ class DesktopGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { ); } + @override + void dispose() { + _toggleExtension.dispose(); + super.dispose(); + } + ValueKey _makeValueKey(DatabaseController controller) { return ValueKey(controller.viewId); } diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart index dcfa0f724de02..23b51c437f37e 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/mobile_grid_page.dart @@ -29,7 +29,7 @@ import 'widgets/header/mobile_grid_header.dart'; import 'widgets/mobile_fab.dart'; import 'widgets/row/mobile_row.dart'; -class MobileGridTabBarBuilderImpl implements DatabaseTabBarItemBuilder { +class MobileGridTabBarBuilderImpl extends DatabaseTabBarItemBuilder { @override Widget content( BuildContext context, 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 a59e84dd4741b..caca16a1ac971 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 @@ -35,7 +35,12 @@ class FilterMenu extends StatelessWidget { final List children = []; children.addAll( state.filters - .map((filterInfo) => FilterMenuItem(filterInfo: filterInfo)) + .map( + (filterInfo) => FilterMenuItem( + key: ValueKey(filterInfo.filter.id), + filterInfo: filterInfo, + ), + ) .toList(), ); diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart index 44a30a3384868..3869925b7e551 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/row/row.dart @@ -326,6 +326,12 @@ class _RowEnterRegionState extends State<_RowEnterRegion> { _rowStateNotifier = RegionStateNotifier(); } + @override + Future dispose() async { + _rowStateNotifier.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( @@ -338,10 +344,4 @@ class _RowEnterRegionState extends State<_RowEnterRegion> { ), ); } - - @override - Future dispose() async { - _rowStateNotifier.dispose(); - super.dispose(); - } } 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 0da24d9e3df15..a047f64de36bc 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 @@ -41,6 +41,12 @@ abstract class DatabaseTabBarItemBuilder { BuildContext context, DatabaseController controller, ); + + /// Should be called in case a builder has resources it + /// needs to dispose of. + /// + // If we add any logic in this method, add @mustCallSuper ! + void dispose() {} } class DatabaseTabBarView extends StatefulWidget { diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart index 51c4ec746da6b..ba2e8f0e00fcc 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/card/card_bloc.dart @@ -47,6 +47,16 @@ class CardBloc extends Bloc { VoidCallback? _rowCallback; + @override + Future close() async { + if (_rowCallback != null) { + _rowCache.removeRowListener(_rowCallback!); + _rowCallback = null; + } + await _rowListener.stop(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { @@ -73,16 +83,6 @@ class CardBloc extends Bloc { ); } - @override - Future close() async { - if (_rowCallback != null) { - _rowCache.removeRowListener(_rowCallback!); - _rowCallback = null; - } - await _rowListener.stop(); - return super.close(); - } - Future _startListening() async { _rowCallback = _rowCache.addListener( rowId: rowId, @@ -113,7 +113,7 @@ List _makeCells( cellContexts.removeWhere((cellContext) { final fieldInfo = fieldController.getField(cellContext.fieldId); return fieldInfo == null || - !fieldInfo.fieldSettings!.visibility.isVisibleState() || + !(fieldInfo.fieldSettings?.visibility.isVisibleState() ?? false) || (groupFieldId != null && cellContext.fieldId == groupFieldId); }); return cellContexts.toList(); diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart index e49347eeafa5d..8348232f4ade6 100755 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell/editable_cell_builder.dart @@ -1,9 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.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_backend/protobuf/flowy-database2/protobuf.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; + +import '../row/accessory/cell_accessory.dart'; +import '../row/accessory/cell_shortcuts.dart'; +import '../row/cells/cell_container.dart'; import 'editable_cell_skeleton/checkbox.dart'; import 'editable_cell_skeleton/checklist.dart'; @@ -13,9 +17,6 @@ import 'editable_cell_skeleton/select_option.dart'; import 'editable_cell_skeleton/text.dart'; import 'editable_cell_skeleton/timestamp.dart'; import 'editable_cell_skeleton/url.dart'; -import '../row/accessory/cell_accessory.dart'; -import '../row/accessory/cell_shortcuts.dart'; -import '../row/cells/cell_container.dart'; enum EditableCellStyle { desktopGrid, @@ -113,11 +114,12 @@ class EditableCellBuilder { CellContext cellContext, { required EditableCellSkinMap skinMap, }) { - final cellController = makeCellController(databaseController, cellContext); + final DatabaseController(:fieldController) = databaseController; + final fieldType = fieldController.getField(cellContext.fieldId)!.fieldType; + final key = ValueKey( "${databaseController.viewId}${cellContext.fieldId}${cellContext.rowId}", ); - final fieldType = cellController.fieldType; assert(skinMap.has(fieldType)); return switch (fieldType) { FieldType.Checkbox => EditableCheckboxCell( @@ -239,6 +241,7 @@ abstract class GridCellState extends State { @override void dispose() { + widget.requestFocus.removeListener(onRequestFocus); widget.requestFocus.dispose(); super.dispose(); } 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 1e0a1af4bec6a..7deb6c85e4828 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 @@ -89,6 +89,7 @@ class _GridURLCellState extends GridEditableTextCell { @override void dispose() { + widget._cellDataNotifier.dispose(); _textEditingController.dispose(); cellBloc.close(); super.dispose(); 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 11635ceb7d825..e48a82c054449 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 @@ -78,13 +78,13 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { 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: (_) { - final controller = TextEditingController(text: content); return TextField( controller: controller, autofocus: true, @@ -95,6 +95,6 @@ class MobileRowDetailURLCellSkin extends IEditableURLCellSkin { }, ); }, - ); + ).then((_) => controller.dispose()); } } diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index f368452813688..6e69ded8a5a2f 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -184,6 +184,14 @@ class _ChecklistItemState extends State { } } + @override + void dispose() { + _textController.dispose(); + _focusNode.dispose(); + _debounceOnChanged?.cancel(); + super.dispose(); + } + @override void didUpdateWidget(ChecklistItem oldWidget) { super.didUpdateWidget(oldWidget); @@ -300,6 +308,12 @@ class _NewTaskItemState extends State { } } + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( 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 index ced0f94760057..f10f83fcc0e40 100644 --- 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 @@ -37,6 +37,7 @@ class _SelectOptionCellEditorState extends State { @override void dispose() { popoverMutex.dispose(); + textEditingController.dispose(); super.dispose(); } 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 0e4c8a944a4ab..2879bc8887a1d 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 @@ -162,7 +162,6 @@ class _SelectOptionTextFieldState extends State { }, ), child: SingleChildScrollView( - controller: ScrollController(), scrollDirection: Axis.horizontal, child: Wrap(spacing: 4, children: children), ), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart index 12a19614462eb..ce756b9ffd538 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/find_and_replace/find_and_replace_menu.dart @@ -108,7 +108,8 @@ class _FindMenuState extends State { widget.searchService.currentSelectedIndex.removeListener(_setState); widget.searchService.dispose(); findTextEditingController.removeListener(_searchPattern); - + findTextEditingController.dispose(); + findTextFieldFocusNode.dispose(); super.dispose(); } @@ -241,6 +242,12 @@ class _ReplaceMenuState extends State { late final FocusNode replaceTextFieldFocusNode; final replaceTextEditingController = TextEditingController(); + @override + void dispose() { + replaceTextEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Row( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart index 26b22e97ac42f..37753b7724766 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/cover_editor.dart @@ -351,6 +351,12 @@ class CoverColorPicker extends StatefulWidget { class _CoverColorPickerState extends State { final scrollController = ScrollController(); + @override + void dispose() { + scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( @@ -375,12 +381,6 @@ class _CoverColorPickerState extends State { ); } - @override - void dispose() { - super.dispose(); - scrollController.dispose(); - } - Widget _buildColorItems(List options, String? selectedColor) { return Row( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart index 85767a5220fee..09b0ac3beccf2 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/custom_cover_picker.dart @@ -110,9 +110,13 @@ class _NetworkImageUrlInputState extends State { @override void initState() { super.initState(); - urlController.addListener(() { - setState(() {}); - }); + urlController.addListener(() => setState(() {})); + } + + @override + void dispose() { + urlController.dispose(); + super.dispose(); } @override diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart index b76b9a48ad7c5..543cee120757b 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/inline_math_equation/inline_math_equation.dart @@ -120,6 +120,12 @@ class _MathInputTextFieldState extends State { ); } + @override + void dispose() { + textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return SizedBox( diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 6afe34118c5db..0040f73df24c6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -187,10 +187,10 @@ class MathEquationBlockComponentWidgetState } void showEditingDialog() { + final controller = TextEditingController(text: formula); showDialog( context: context, builder: (context) { - final controller = TextEditingController(text: formula); return AlertDialog( backgroundColor: Theme.of(context).canvasColor, title: Text( @@ -234,7 +234,7 @@ class MathEquationBlockComponentWidgetState actionsAlignment: MainAxisAlignment.spaceAround, ); }, - ); + ).then((_) => controller.dispose()); } void updateMathEquation(String mathEquation, BuildContext context) { 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 b44d1c6053872..ba286b7b9f503 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 @@ -478,7 +478,6 @@ class _ToolbarItemListViewState extends State<_ToolbarItemListView> { void dispose() { widget.editorState.selectionNotifier .removeListener(_debounceUpdatePilotPosition); - super.dispose(); } diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index 03dce860bdb48..8e442320193b0 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -134,6 +134,13 @@ class _InlineActionsHandlerState extends State { startOffset = widget.editorState.selection?.endIndex ?? 0; } + @override + void dispose() { + _scrollController.dispose(); + _focusNode.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Focus( diff --git a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart index afabb6a57ab0a..b50f95342abfe 100644 --- a/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/trash/trash_page.dart @@ -28,6 +28,13 @@ class TrashPage extends StatefulWidget { class _TrashPageState extends State { final ScrollController _scrollController = ScrollController(); + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { const horizontalPadding = 80.0; diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart index 0cefb6e7d8b86..24fefbfc341b1 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/encrypt_secret_screen.dart @@ -28,6 +28,13 @@ class EncryptSecretScreen extends StatefulWidget { class _EncryptSecretScreenState extends State { final TextEditingController _textEditingController = TextEditingController(); + + @override + void dispose() { + _textEditingController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart index 6c95d1076cf63..b3c1f1cd0afe9 100644 --- a/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart +++ b/frontend/appflowy_flutter/lib/user/presentation/screens/workspace_start_screen/mobile_workspace_start_screen.dart @@ -24,13 +24,19 @@ class MobileWorkspaceStartScreen extends StatefulWidget { class _MobileWorkspaceStartScreenState extends State { WorkspacePB? selectedWorkspace; + final TextEditingController controller = TextEditingController(); + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { final style = Theme.of(context); final size = MediaQuery.of(context).size; const double spacing = 16.0; - final TextEditingController controller = TextEditingController(); final List> workspaceEntries = >[]; for (final WorkspacePB workspace in widget.workspaceState.workspaces) { 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 4770822f1aeae..c36a557c42ae4 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_bloc.dart @@ -22,6 +22,12 @@ class TabsBloc extends Bloc { late final MenuSharedState menuSharedState; + @override + Future close() { + state.dispose(); + return super.close(); + } + void _dispatch() { on( (event, emit) async { diff --git a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart index caa9c8d3b90cb..dd08e505d648a 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/tabs/tabs_state.dart @@ -98,4 +98,10 @@ class TabsState { currentIndex: newIndex ?? currentIndex, pageManagers: pageManagers ?? _pageManagers, ); + + void dispose() { + for (final manager in pageManagers) { + manager.dispose(); + } + } } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart index d835956cdfa23..a941c9a284cd3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart @@ -253,6 +253,10 @@ class PageManager { ), ); } + + void dispose() { + _notifier.dispose(); + } } class HomeTopBar extends StatelessWidget { 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 53d50f240c44c..5ce8d542888fe 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 @@ -498,6 +498,7 @@ class _SingleInnerViewItemState extends State { title: LocaleKeys.disclosureAction_rename.tr(), autoSelectAllText: true, value: widget.view.name, + maxLength: 256, confirm: (newValue) { context.read().add(ViewEvent.rename(newValue)); }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart index e4eb5546c8e1c..f2bdbf4faa2c3 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/src/default_emoji_picker_view.dart @@ -25,6 +25,7 @@ class DefaultEmojiPickerViewState extends State final FocusNode _emojiFocusNode = FocusNode(); EmojiCategoryGroup searchEmojiList = EmojiCategoryGroup(EmojiCategory.SEARCH, []); + final scrollController = ScrollController(); @override void initState() { @@ -70,6 +71,7 @@ class DefaultEmojiPickerViewState extends State _emojiFocusNode.dispose(); _pageController?.dispose(); _tabController?.dispose(); + scrollController.dispose(); super.dispose(); } @@ -224,8 +226,6 @@ class DefaultEmojiPickerViewState extends State Widget _buildPage(double emojiSize, EmojiCategoryGroup emojiCategoryGroup) { // Display notice if recent has no entries yet - final scrollController = ScrollController(); - if (emojiCategoryGroup.category == EmojiCategory.RECENT && emojiCategoryGroup.emoji.isEmpty) { return _buildNoRecent(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart index ab9473baac294..f84fe9f374bf4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart @@ -277,6 +277,12 @@ class CloudURLInputState extends State { _controller = TextEditingController(text: widget.url); } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return TextField( @@ -306,12 +312,6 @@ class CloudURLInputState extends State { onChanged: widget.onChanged, ); } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } } class AppFlowyCloudEnableSync extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart index 56a59aa58d958..43e5472ef8b7a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_supabase_cloud.dart @@ -273,6 +273,12 @@ class SupabaseInputState extends State { _controller = TextEditingController(text: widget.url); } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return TextField( @@ -298,12 +304,6 @@ class SupabaseInputState extends State { onChanged: widget.onChanged, ); } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } } class SupabaseSelfhostTip extends StatelessWidget { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart index f391ac2ad7395..382c3d9833fd5 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart @@ -138,10 +138,10 @@ class ShortcutsListTile extends StatelessWidget { } void showKeyListenerDialog(BuildContext widgetContext) { + final controller = TextEditingController(text: shortcutEvent.command); showDialog( context: widgetContext, builder: (builderContext) { - final controller = TextEditingController(text: shortcutEvent.command); final formKey = GlobalKey(); return AlertDialog( title: Text(LocaleKeys.settings_shortcuts_updateShortcutStep.tr()), @@ -184,7 +184,7 @@ class ShortcutsListTile extends StatelessWidget { ), ); }, - ); + ).then((_) => controller.dispose()); } String? _validateForConflicts(BuildContext context, String command) { diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart index d468051820465..7ef8ccc1dfa19 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_user_view.dart @@ -246,6 +246,13 @@ class UserNameInputState extends State { _controller = TextEditingController(text: widget.name); } + @override + void dispose() { + _controller.dispose(); + _debounce?.cancel(); + super.dispose(); + } + @override Widget build(BuildContext context) { return TextField( @@ -277,13 +284,6 @@ class UserNameInputState extends State { }, ); } - - @override - void dispose() { - _controller.dispose(); - _debounce?.cancel(); - super.dispose(); - } } @visibleForTesting diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart index 24305ad7e3311..1b431b58d65fe 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/dialogs.dart @@ -19,6 +19,7 @@ class NavigatorTextFieldDialog extends StatefulWidget { required this.value, required this.confirm, this.cancel, + this.maxLength, }); final String value; @@ -26,6 +27,7 @@ class NavigatorTextFieldDialog extends StatefulWidget { final void Function()? cancel; final void Function(String) confirm; final bool autoSelectAllText; + final int? maxLength; @override State createState() => @@ -38,6 +40,7 @@ class _NavigatorTextFieldDialogState extends State { @override void initState() { + super.initState(); newValue = widget.value; controller.text = newValue; if (widget.autoSelectAllText) { @@ -46,7 +49,12 @@ class _NavigatorTextFieldDialogState extends State { extentOffset: newValue.length, ); } - super.initState(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); } @override @@ -63,9 +71,12 @@ class _NavigatorTextFieldDialogState extends State { FlowyFormTextInput( hintText: LocaleKeys.dialogCreatePageNameHint.tr(), controller: controller, - textStyle: Theme.of(context).textTheme.bodySmall?.copyWith( - fontSize: FontSizes.s16, - ), + textStyle: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(fontSize: FontSizes.s16), + maxLength: widget.maxLength, + showCounter: false, autoFocus: true, onChanged: (text) { newValue = text; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart index 0eab4b9284189..b12ae6644a98e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/rename_view_popover.dart @@ -65,8 +65,10 @@ class _RenameViewPopoverState extends State { width: 220, child: FlowyTextField( controller: _controller, + maxLength: 256, onSubmitted: _updateViewName, onCanceled: () => _updateViewName(_controller.text), + showCounter: false, ), ), ], diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart index bbb55bf8858c3..41017faafb377 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/notifier.dart @@ -48,9 +48,3 @@ class PublishNotifier extends ChangeNotifier { ); } } - -class Notifier extends ChangeNotifier { - void notify() { - notifyListeners(); - } -} diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart index fde544365be51..d7704366f2b1b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart @@ -30,6 +30,12 @@ class _KeyboardScreenState extends State { final TextEditingController _controller = TextEditingController(text: 'Hello Flowy'); + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart index ac8bd140b2c9a..d255555ca0df5 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart @@ -45,7 +45,9 @@ class StyledSingleChildScrollViewState @override void dispose() { - // scrollController.dispose(); + if (widget.controller == null) { + scrollController.dispose(); + } super.dispose(); } 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 a6e6ce9a0ae7a..ffedd06bd14ca 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 @@ -79,6 +79,7 @@ class FlowyTextFieldState extends State { focusNode.addListener(notifyDidEndEditing); controller = widget.controller ?? TextEditingController(); + if (widget.text != null) { controller.text = widget.text!; } @@ -95,6 +96,19 @@ class FlowyTextFieldState extends State { } } + @override + void dispose() { + focusNode.removeListener(notifyDidEndEditing); + if (widget.focusNode == null) { + focusNode.dispose(); + } + if (widget.controller == null) { + controller.dispose(); + } + _debounceOnChanged?.cancel(); + super.dispose(); + } + void _debounceOnChangedText(Duration duration, String text) { _debounceOnChanged?.cancel(); _debounceOnChanged = Timer(duration, () async { @@ -200,15 +214,6 @@ class FlowyTextFieldState extends State { ); } - @override - void dispose() { - focusNode.removeListener(notifyDidEndEditing); - if (widget.focusNode == null) { - focusNode.dispose(); - } - super.dispose(); - } - void notifyDidEndEditing() { if (!focusNode.hasFocus) { if (controller.text.isNotEmpty && widget.submitOnLeave) { @@ -222,8 +227,7 @@ class FlowyTextFieldState extends State { String? _suffixText() { if (widget.maxLength != null) { return ' ${controller.text.length}/${widget.maxLength}'; - } else { - return null; } + return null; } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart index c7e6368aa8199..7aad277b11de7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart @@ -17,6 +17,8 @@ class FlowyFormTextInput extends StatelessWidget { final TextStyle? textStyle; final TextAlign textAlign; final int? maxLines; + final int? maxLength; + final bool showCounter; final TextEditingController? controller; final TextCapitalization? capitalization; final Function(String)? onChanged; @@ -24,23 +26,25 @@ class FlowyFormTextInput extends StatelessWidget { final Function(bool)? onFocusChanged; final Function(FocusNode)? onFocusCreated; - const FlowyFormTextInput( - {Key? key, - this.label, - this.autoFocus, - this.initialValue, - this.onChanged, - this.onEditingComplete, - this.hintText, - this.onFocusChanged, - this.onFocusCreated, - this.controller, - this.contentPadding, - this.capitalization, - this.textStyle, - this.textAlign = TextAlign.center, - this.maxLines}) - : super(key: key); + const FlowyFormTextInput({ + super.key, + this.label, + this.autoFocus, + this.initialValue, + this.onChanged, + this.onEditingComplete, + this.hintText, + this.onFocusChanged, + this.onFocusCreated, + this.controller, + this.contentPadding, + this.capitalization, + this.textStyle, + this.textAlign = TextAlign.center, + this.maxLines, + this.maxLength, + this.showCounter = true, + }); @override Widget build(BuildContext context) { @@ -57,16 +61,17 @@ class FlowyFormTextInput extends StatelessWidget { onFocusChanged: onFocusChanged, controller: controller, maxLines: maxLines, - inputDecoration: InputDecoration( - isDense: true, - contentPadding: contentPadding ?? kDefaultTextInputPadding, - border: const ThinUnderlineBorder( - borderSide: BorderSide(width: 5, color: Colors.red)), - hintStyle: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).hintColor.withOpacity(0.7)), - hintText: hintText, + maxLength: maxLength, + showCounter: showCounter, + contentPadding: contentPadding ?? kDefaultTextInputPadding, + hintText: hintText, + hintStyle: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor.withOpacity(0.7)), + isDense: true, + inputBorder: const ThinUnderlineBorder( + borderSide: BorderSide(width: 5, color: Colors.red), ), ); } @@ -82,6 +87,8 @@ class StyledSearchTextInput extends StatefulWidget { final IconData? icon; final String? initialValue; final int? maxLines; + final int? maxLength; + final bool showCounter; final TextEditingController? controller; final TextCapitalization? capitalization; final TextInputType? type; @@ -89,11 +96,14 @@ class StyledSearchTextInput extends StatefulWidget { final bool? autoValidate; final bool? enableSuggestions; final bool? autoCorrect; + final bool isDense; final String? errorText; final String? hintText; + final TextStyle? hintStyle; final Widget? prefixIcon; final Widget? suffixIcon; final InputDecoration? inputDecoration; + final InputBorder? inputBorder; final Function(String)? onChanged; final Function()? onEditingComplete; @@ -105,7 +115,7 @@ class StyledSearchTextInput extends StatefulWidget { final VoidCallback? onTap; const StyledSearchTextInput({ - Key? key, + super.key, this.label, this.autoFocus = false, this.obscureText = false, @@ -118,6 +128,7 @@ class StyledSearchTextInput extends StatefulWidget { this.autoValidate = false, this.enableSuggestions = true, this.autoCorrect = true, + this.isDense = false, this.errorText, this.style, this.contentPadding, @@ -133,9 +144,13 @@ class StyledSearchTextInput extends StatefulWidget { this.onSaved, this.onTap, this.hintText, + this.hintStyle, this.capitalization, this.maxLines, - }) : super(key: key); + this.maxLength, + this.showCounter = false, + this.inputBorder, + }); @override StyledSearchTextInputState createState() => StyledSearchTextInputState(); @@ -175,7 +190,9 @@ class StyledSearchTextInputState extends State { @override void dispose() { - _controller.dispose(); + if (widget.controller == null) { + _controller.dispose(); + } _focusNode.dispose(); super.dispose(); } @@ -208,28 +225,40 @@ class StyledSearchTextInputState extends State { showCursor: true, enabled: widget.enabled, maxLines: widget.maxLines, + maxLength: widget.maxLength, textCapitalization: widget.capitalization ?? TextCapitalization.none, textAlign: widget.textAlign, decoration: widget.inputDecoration ?? InputDecoration( prefixIcon: widget.prefixIcon, suffixIcon: widget.suffixIcon, + counterText: "", + suffixText: widget.showCounter ? _suffixText() : "", contentPadding: widget.contentPadding ?? EdgeInsets.all(Insets.m), - border: const OutlineInputBorder(borderSide: BorderSide.none), - isDense: true, + border: widget.inputBorder ?? + const OutlineInputBorder(borderSide: BorderSide.none), + isDense: widget.isDense, icon: widget.icon == null ? null : Icon(widget.icon), errorText: widget.errorText, errorMaxLines: 2, hintText: widget.hintText, - hintStyle: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Theme.of(context).hintColor), + hintStyle: widget.hintStyle ?? + Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: Theme.of(context).hintColor), labelText: widget.label, ), ), ); } + + String? _suffixText() { + if (widget.controller != null && widget.maxLength != null) { + return ' ${widget.controller!.text.length}/${widget.maxLength}'; + } + return null; + } } class ThinUnderlineBorder extends InputBorder { From 9d71464f1a505187a0d86d79ceeaa359172ead79 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:49:17 +0100 Subject: [PATCH 22/50] fix: select name by default (#4630) --- .../bottom_sheet/bottom_sheet_rename_widget.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 618e19e53d6a5..0b0ce92b34677 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 @@ -27,7 +27,11 @@ class _MobileBottomSheetRenameWidgetState @override void initState() { super.initState(); - controller = TextEditingController(text: widget.name); + controller = TextEditingController(text: widget.name) + ..selection = TextSelection( + baseOffset: 0, + extentOffset: widget.name.length, + ); } @override From 60fc5bb2c8463a5f66aaeb4c2d5d0a9e4e1211fe Mon Sep 17 00:00:00 2001 From: "Kilu.He" <108015703+qinluhe@users.noreply.github.com> Date: Thu, 8 Feb 2024 14:22:44 +0800 Subject: [PATCH 23/50] fix: tauri folder bugs (#4589) --- .../appflowy_tauri/src-tauri/tauri.conf.json | 5 +- .../src/appflowy_app/AppMain.hooks.ts | 69 ++++----- .../application/document/document.types.ts | 1 + .../application/folder/page.service.ts | 3 +- .../src/appflowy_app/assets/details.svg | 6 +- .../_shared/button_menu/ButtonMenu.tsx | 69 --------- .../DeleteConfirmDialog.tsx | 44 ++++-- .../confirm_dialog}/RenameDialog.tsx | 39 ++--- .../_shared/drag_block/drag.hooks.ts | 12 +- .../KeyboardNavigation.tsx | 12 +- .../components/database/Database.tsx | 82 +++++++++-- .../components/property/PropertyActions.tsx | 2 +- .../components/tab_bar/DatabaseTabBar.tsx | 28 ++-- .../components/tab_bar/ViewActions.tsx | 47 +++--- .../document_header/DocumentHeader.tsx | 2 +- .../blocks/database/DatabaseList.hooks.ts | 23 +-- .../editor/components/blocks/text/Text.tsx | 2 +- .../inline_formula/InlineFormula.tsx | 2 +- .../inline_nodes/mention/MentionLeaf.tsx | 105 +++++++++++-- .../block_actions/BlockActionsToolbar.tsx | 5 +- .../block_actions/BlockOperationMenu.tsx | 3 +- .../components/editor/editor.scss | 25 +++- .../editor/plugins/shortcuts/hotkey.ts | 14 +- .../plugins/shortcuts/shortcuts.hooks.ts | 3 +- .../components/layout/FooterPanel.tsx | 8 +- .../appflowy_app/components/layout/Layout.tsx | 4 +- .../layout/bread_crumb/BreadCrumb.tsx | 19 ++- .../layout/bread_crumb/Breadcrumb.hooks.ts | 13 +- .../CollapseMenuButton.tsx | 47 ++++-- .../components/layout/layout.scss | 12 ++ .../layout/nested_page/AddButton.tsx | 85 +++++------ .../layout/nested_page/DeleteDialog.tsx | 2 +- .../layout/nested_page/MoreButton.tsx | 138 ++++++++++-------- .../layout/nested_page/NestedPage.hooks.ts | 59 +++++--- .../layout/nested_page/NestedPage.tsx | 42 ++++-- .../layout/nested_page/NestedPageTitle.tsx | 84 +++++++---- .../layout/nested_page/OperationMenu.tsx | 103 +++++++++++++ .../components/layout/side_bar/Resizer.tsx | 1 + .../components/layout/side_bar/SideBar.tsx | 7 +- .../components/layout/side_bar/UserInfo.tsx | 55 ++++--- .../components/layout/top_bar/TopBar.tsx | 9 -- .../layout/user_setting/AppearanceSetting.tsx | 21 ++- .../layout/user_setting/LanguageSetting.tsx | 2 +- .../components/layout/user_setting/Menu.tsx | 4 +- .../layout/user_setting/UserSetting.tsx | 9 +- .../layout/workspace_manager/MoreButton.tsx | 64 -------- .../layout/workspace_manager/NestedPages.tsx | 2 +- .../workspace_manager/NewPageButton.tsx | 40 +++-- .../layout/workspace_manager/TrashButton.tsx | 6 +- .../workspace_manager/Workspace.hooks.ts | 22 ++- .../layout/workspace_manager/Workspace.tsx | 51 ++++++- .../workspace_manager/WorkspaceManager.tsx | 11 +- .../workspace_manager/WorkspaceTitle.tsx | 38 ----- .../appflowy_app/components/trash/Trash.tsx | 9 +- .../components/trash/TrashItem.tsx | 10 +- .../src/appflowy_app/hooks/page.hooks.tsx | 26 ++++ .../stores/reducers/current-user/slice.ts | 1 + .../stores/reducers/pages/async_actions.ts | 4 +- .../stores/reducers/pages/slice.ts | 81 +++++++++- .../src/appflowy_app/utils/get_modifier.ts | 12 ++ .../src/appflowy_app/utils/mui.ts | 5 +- .../appflowy_tauri/src/styles/template.css | 4 - frontend/resources/translations/en.json | 4 +- 63 files changed, 1048 insertions(+), 669 deletions(-) delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx rename frontend/appflowy_tauri/src/appflowy_app/components/_shared/{delete_confirm_dialog => confirm_dialog}/DeleteConfirmDialog.tsx (53%) rename frontend/appflowy_tauri/src/appflowy_app/components/{layout/nested_page => _shared/confirm_dialog}/RenameDialog.tsx (66%) create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss create mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx delete mode 100644 frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx create mode 100644 frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts diff --git a/frontend/appflowy_tauri/src-tauri/tauri.conf.json b/frontend/appflowy_tauri/src-tauri/tauri.conf.json index 7e9b0692a3306..c0da4386af11d 100644 --- a/frontend/appflowy_tauri/src-tauri/tauri.conf.json +++ b/frontend/appflowy_tauri/src-tauri/tauri.conf.json @@ -19,7 +19,10 @@ }, "fs": { "all": true, - "scope": ["$APPLOCALDATA/**", "$APPLOCALDATA/images/*"], + "scope": [ + "$APPLOCALDATA/**", + "$APPLOCALDATA/images/*" + ], "readFile": true, "writeFile": true, "readDir": true, diff --git a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts index cd61b2108d5f6..8d4088ca25ec8 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/AppMain.hooks.ts @@ -1,64 +1,59 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store'; -import { useCallback, useEffect, useMemo } from 'react'; +import { useEffect, useMemo } from 'react'; import { currentUserActions } 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'; import { useTranslation } from 'react-i18next'; -import { ThemeModePB } from '@/services/backend'; import { UserService } from '$app/application/user/user.service'; export function useUserSetting() { const dispatch = useAppDispatch(); const { i18n } = useTranslation(); - - const handleSystemThemeChange = useCallback(() => { - const mode = window.matchMedia('(prefers-color-scheme: dark)').matches ? ThemeMode.Dark : ThemeMode.Light; - - dispatch(currentUserActions.setUserSetting({ themeMode: mode })); - }, [dispatch]); - - const loadUserSetting = useCallback(async () => { - const settings = await UserService.getAppearanceSetting(); - - if (!settings) return; - dispatch(currentUserActions.setUserSetting(settings)); - - if (settings.themeMode === ThemeModePB.System) { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); - - handleSystemThemeChange(); - - mediaQuery.addEventListener('change', handleSystemThemeChange); - } - - await i18n.changeLanguage(settings.language); - }, [dispatch, handleSystemThemeChange, i18n]); + const { + themeMode = ThemeMode.System, + isDark = false, + theme: themeType = ThemeType.Default, + } = useAppSelector((state) => { + return state.currentUser.userSetting || {}; + }); useEffect(() => { - void loadUserSetting(); - }, [loadUserSetting]); + void (async () => { + const settings = await UserService.getAppearanceSetting(); - const { themeMode = ThemeMode.Light, theme: themeType = ThemeType.Default } = useAppSelector((state) => { - return state.currentUser.userSetting || {}; - }); + if (!settings) return; + dispatch(currentUserActions.setUserSetting(settings)); + await i18n.changeLanguage(settings.language); + })(); + }, [dispatch, i18n]); useEffect(() => { const html = document.documentElement; - html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark)); - html?.setAttribute('data-theme', themeType); - }, [themeType, themeMode]); + html?.setAttribute('data-dark-mode', String(isDark)); + }, [isDark]); useEffect(() => { - return () => { - const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleSystemThemeChange = () => { + if (themeMode !== ThemeMode.System) return; + dispatch( + currentUserActions.setUserSetting({ + isDark: mediaQuery.matches, + }) + ); + }; + mediaQuery.addEventListener('change', handleSystemThemeChange); + + return () => { mediaQuery.removeEventListener('change', handleSystemThemeChange); }; - }, [dispatch, handleSystemThemeChange]); + }, [dispatch, themeMode]); - const muiTheme = useMemo(() => createTheme(getDesignTokens(themeMode)), [themeMode]); + const muiTheme = useMemo(() => createTheme(getDesignTokens(isDark)), [isDark]); return { muiTheme, 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 4497dfff80b82..98b493581def0 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 @@ -138,6 +138,7 @@ export interface MentionPage { id: string; name: string; layout: ViewLayoutPB; + parentId: string; icon?: { ty: ViewIconTypePB; value: string; 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 b529571c91441..25aa0033a4280 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 @@ -49,8 +49,7 @@ export const createOrphanPage = async ( return Promise.reject(result.val); }; -export const duplicatePage = async (id: string) => { - const page = await getPage(id); +export const duplicatePage = async (page: Page) => { const payload = ViewPB.fromObject(page); const result = await FolderEventDuplicateView(payload); diff --git a/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg b/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg index af6127ce5d181..22c6830916496 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg +++ b/frontend/appflowy_tauri/src/appflowy_app/assets/details.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx deleted file mode 100644 index c099eb3d5d220..0000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/button_menu/ButtonMenu.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useCallback, useState } from 'react'; -import { List, MenuItem, Popover, Portal, Theme } from '@mui/material'; -import { PopoverOrigin } from '@mui/material/Popover/Popover'; -import { SxProps } from '@mui/system'; - -interface ButtonPopoverListProps { - isVisible: boolean; - children: React.ReactNode; - popoverOptions: { - key: React.Key; - icon: React.ReactNode; - label: React.ReactNode | string; - onClick: () => void; - }[]; - popoverOrigin: { - anchorOrigin: PopoverOrigin; - transformOrigin: PopoverOrigin; - }; - onClose?: () => void; - sx?: SxProps; -} - -function ButtonPopoverList({ popoverOrigin, isVisible, children, popoverOptions, onClose, sx }: ButtonPopoverListProps) { - const [anchorEl, setAnchorEl] = useState(); - const open = Boolean(anchorEl); - const visible = isVisible || open; - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = useCallback(() => { - setAnchorEl(undefined); - }, []); - - return ( - <> - {visible &&
{children}
} - - { - handleClose(); - onClose?.(); - }} - > - - {popoverOptions.map((option) => ( - { - option.onClick(); - handleClose(); - }} - className={'flex items-center gap-1 rounded-none px-2 text-xs font-medium'} - > - {option.icon} - {option.label} - - ))} - - - - - ); -} - -export default ButtonPopoverList; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx similarity index 53% rename from frontend/appflowy_tauri/src/appflowy_app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx index 5723505b4c55c..b46ce37345ba9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/DeleteConfirmDialog.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { useCallback } from 'react'; import DialogContent from '@mui/material/DialogContent'; import { Button, DialogActions, Divider } from '@mui/material'; import Dialog from '@mui/material/Dialog'; import { useTranslation } from 'react-i18next'; +import { Log } from '$app/utils/log'; interface Props { open: boolean; @@ -15,9 +16,36 @@ interface Props { function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) { const { t } = useTranslation(); + const onDone = useCallback(async () => { + try { + await onOk(); + onClose(); + } catch (e) { + Log.error(e); + } + }, [onClose, onOk]); + return ( - e.stopPropagation()} open={open} onClose={onClose}> - + { + if (e.key === 'Escape') { + e.preventDefault(); + e.stopPropagation(); + onClose(); + } + + if (e.key === 'Enter') { + e.preventDefault(); + e.stopPropagation(); + void onDone(); + } + }} + onMouseDown={(e) => e.stopPropagation()} + open={open} + onClose={onClose} + > +
{title}
{subtitle &&
{subtitle}
}
@@ -26,15 +54,7 @@ function DeleteConfirmDialog({ open, title, subtitle, onOk, onClose }: Props) { - diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/RenameDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx similarity index 66% rename from frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/RenameDialog.tsx rename to frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx index 71c00878f8981..4c99d37e87e73 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/RenameDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/confirm_dialog/RenameDialog.tsx @@ -1,10 +1,10 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState } from 'react'; import DialogTitle from '@mui/material/DialogTitle'; import DialogContent from '@mui/material/DialogContent'; import Dialog from '@mui/material/Dialog'; import { useTranslation } from 'react-i18next'; import TextField from '@mui/material/TextField'; -import { Button, DialogActions } from '@mui/material'; +import { Button, DialogActions, Divider } from '@mui/material'; function RenameDialog({ defaultValue, @@ -25,20 +25,31 @@ function RenameDialog({ setValue(defaultValue); setError(false); }, [defaultValue]); + + const onDone = useCallback(async () => { + try { + await onOk(value); + onClose(); + } catch (e) { + setError(true); + } + }, [onClose, onOk, value]); + return ( e.stopPropagation()} open={open} onClose={onClose}> - {t('menuAppHeader.renameDialog')} - + {t('menuAppHeader.renameDialog')} + { e.stopPropagation(); if (e.key === 'Enter') { e.preventDefault(); - void onOk(value); + void onDone(); } if (e.key === 'Escape') { @@ -54,18 +65,12 @@ function RenameDialog({ variant='standard' /> - - - - + diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts index b7607616debef..85f0507fff5f6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/drag_block/drag.hooks.ts @@ -24,8 +24,12 @@ export function useDrag(props: Props) { setIsDraggingOver(false); setIsDragging(false); setDropPosition(undefined); + const currentTarget = e.currentTarget; + + if (currentTarget.parentElement?.closest(`[data-drop-enabled="false"]`)) return; + if (currentTarget.closest(`[data-dragging="true"]`)) return; const dragId = e.dataTransfer.getData('dragId'); - const targetRect = e.currentTarget.getBoundingClientRect(); + const targetRect = currentTarget.getBoundingClientRect(); const { clientY } = e; const position = calcPosition(targetRect, clientY); @@ -37,8 +41,12 @@ export function useDrag(props: Props) { e.stopPropagation(); e.preventDefault(); if (isDragging) return; + const currentTarget = e.currentTarget; + + if (currentTarget.parentElement?.closest(`[data-drop-enabled="false"]`)) return; + if (currentTarget.closest(`[data-dragging="true"]`)) return; setIsDraggingOver(true); - const targetRect = e.currentTarget.getBoundingClientRect(); + const targetRect = currentTarget.getBoundingClientRect(); const { clientY } = e; const position = calcPosition(targetRect, clientY); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx index df6485ab3b4be..b7c6db9db0c6b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/_shared/keyboard_navigation/KeyboardNavigation.tsx @@ -1,7 +1,6 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { MenuItem, Typography } from '@mui/material'; import { scrollIntoView } from '$app/components/_shared/keyboard_navigation/utils'; -import { ReactEditor, useSlateStatic } from 'slate-react'; import { useTranslation } from 'react-i18next'; /** @@ -35,7 +34,7 @@ export interface KeyboardNavigationOption { * - onBlur: called when the keyboard navigation is blurred */ export interface KeyboardNavigationProps { - scrollRef: React.RefObject; + scrollRef?: React.RefObject; focusRef?: React.RefObject; options: KeyboardNavigationOption[]; onSelected?: (optionKey: T) => void; @@ -68,7 +67,6 @@ function KeyboardNavigation({ onFocus, }: KeyboardNavigationProps) { const { t } = useTranslation(); - const editor = useSlateStatic(); const ref = useRef(null); const mouseY = useRef(null); const defaultKeyRef = useRef(defaultFocusedKey); @@ -108,7 +106,7 @@ function KeyboardNavigation({ if (focusedKey === undefined) return; onSelected?.(focusedKey); - const scrollElement = scrollRef.current; + const scrollElement = scrollRef?.current; if (!scrollElement) return; @@ -262,15 +260,15 @@ function KeyboardNavigation({ let element: HTMLElement | null | undefined = focusRef?.current; if (!element) { - element = ReactEditor.toDOMNode(editor, editor); + element = document.activeElement as HTMLElement; } - element.addEventListener('keydown', onKeyDown); + element?.addEventListener('keydown', onKeyDown); return () => { element?.removeEventListener('keydown', onKeyDown); }; } - }, [disableFocus, editor, onKeyDown, focusRef]); + }, [disableFocus, onKeyDown, focusRef]); return (
(({ selectedViewId, setSelectedViewId }, ref) => { const innerRef = useRef(); const databaseRef = (ref ?? innerRef) as React.MutableRefObject; - const viewId = useViewId(); + + const [page, setPage] = useState(null); const { t } = useTranslation(); const [notFound, setNotFound] = useState(false); - const [childViewIds, setChildViewIds] = useState([]); + const [childViews, setChildViews] = useState([]); const [editRecordRowId, setEditRecordRowId] = useState(null); const [openCollections, setOpenCollections] = useState([]); @@ -34,7 +37,7 @@ export const Database = forwardRef(({ selectedViewId, set await databaseViewService .getDatabaseViews(viewId) .then((value) => { - setChildViewIds(value.map((view) => view.id)); + setChildViews(value); }) .catch((err) => { if (err.code === ErrorCode.RecordNotFound) { @@ -43,10 +46,39 @@ export const Database = forwardRef(({ selectedViewId, set }); }, []); + const handleGetPage = useCallback(async () => { + try { + const page = await getPage(viewId); + + setPage(page); + } catch (e) { + setNotFound(true); + } + }, [viewId]); + useEffect(() => { + void handleGetPage(); void handleResetDatabaseViews(viewId); const unsubscribePromise = subscribeNotifications( { + [FolderNotification.DidUpdateView]: (changeset) => { + setChildViews((prev) => { + const index = prev.findIndex((view) => view.id === changeset.id); + + if (index === -1) { + return prev; + } + + const newViews = [...prev]; + + newViews[index] = { + ...newViews[index], + name: changeset.name, + }; + + return newViews; + }); + }, [FolderNotification.DidUpdateChildViews]: (changeset) => { if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) { return; @@ -61,11 +93,35 @@ export const Database = forwardRef(({ selectedViewId, set ); return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); - }, [handleResetDatabaseViews, viewId]); + }, [handleGetPage, handleResetDatabaseViews, viewId]); + + useEffect(() => { + const parentId = page?.parentId; + + if (!parentId) return; + + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateChildViews]: (changeset) => { + if (changeset.delete_child_views.includes(viewId)) { + setNotFound(true); + } + }, + }, + { + id: parentId, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page, viewId]); const value = useMemo(() => { - return Math.max(0, childViewIds.indexOf(selectedViewId ?? viewId)); - }, [childViewIds, selectedViewId, viewId]); + return Math.max( + 0, + childViews.findIndex((view) => view.id === (selectedViewId ?? viewId)) + ); + }, [childViews, selectedViewId, viewId]); const onToggleCollection = useCallback( (id: string, forceOpen?: boolean) => { @@ -110,7 +166,7 @@ export const Database = forwardRef(({ selectedViewId, set pageId={viewId} setSelectedViewId={setSelectedViewId} selectedViewId={selectedViewId} - childViewIds={childViewIds} + childViews={childViews} /> (({ selectedViewId, set axis={'x'} index={value} > - {childViewIds.map((id, index) => ( - - - {selectedViewId === id && ( + {childViews.map((view, index) => ( + + + {selectedViewId === view.id && ( <>
onToggleCollection(id, forceOpen)} + onToggleCollection={(forceOpen?: boolean) => onToggleCollection(view.id, forceOpen)} />
- + {editRecordRowId && ( void; pageId: string; @@ -26,26 +25,21 @@ const DatabaseIcons: { [ViewLayoutPB.Calendar]: GridSvg, }; -export const DatabaseTabBar: FC = ({ pageId, childViewIds, selectedViewId, setSelectedViewId }) => { +export const DatabaseTabBar: FC = ({ pageId, childViews, selectedViewId, setSelectedViewId }) => { const { t } = useTranslation(); const [contextMenuAnchorEl, setContextMenuAnchorEl] = useState(null); const [contextMenuView, setContextMenuView] = useState(null); const open = Boolean(contextMenuAnchorEl); - const views = useAppSelector((state) => { - const map = state.pages.pageMap; - - return childViewIds.map((id) => map[id]).filter(Boolean); - }); const handleChange = (_: React.SyntheticEvent, newValue: string) => { setSelectedViewId?.(newValue); }; useEffect(() => { - if (selectedViewId === undefined && views.length > 0) { - setSelectedViewId?.(views[0].id); + if (selectedViewId === undefined && childViews.length > 0) { + setSelectedViewId?.(childViews[0].id); } - }, [selectedViewId, setSelectedViewId, views]); + }, [selectedViewId, setSelectedViewId, childViews]); const openMenu = (view: Page) => { return (e: React.MouseEvent) => { @@ -56,16 +50,19 @@ export const DatabaseTabBar: FC = ({ pageId, childViewIds, }; }; + const isSelected = useMemo(() => childViews.some((view) => view.id === selectedViewId), [childViews, selectedViewId]); + + if (childViews.length === 0) return null; return (
- - {views.map((view) => { + + {childViews.map((view) => { const Icon = DatabaseIcons[view.layout]; return ( } @@ -81,6 +78,7 @@ export const DatabaseTabBar: FC = ({ pageId, childViewIds,
{open && contextMenuView && ( , action: async () => { @@ -48,31 +49,33 @@ function ViewActions({ view, ...props }: { view: Page } & MenuProps) { <> {options.map((option) => ( - +
{option.icon}
{option.label}
))}
- setOpenRenameDialog(false)} - onOk={async (val) => { - try { - await dispatch( - updatePageName({ - id: viewId, - name: val, - }) - ); - setOpenRenameDialog(false); - props.onClose?.({}, 'backdropClick'); - } catch (e) { - // toast.error(t('error.renameView')); - } - }} - defaultValue={view.name} - /> + {openRenameDialog && ( + setOpenRenameDialog(false)} + onOk={async (val) => { + try { + await dispatch( + updatePageName({ + id: viewId, + name: val, + }) + ); + setOpenRenameDialog(false); + props.onClose?.({}, 'backdropClick'); + } catch (e) { + // toast.error(t('error.renameView')); + } + }} + defaultValue={view.name} + /> + )} ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx index 209e5fd6941c2..a944547870fb6 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/document/document_header/DocumentHeader.tsx @@ -19,7 +19,7 @@ export function DocumentHeader({ page }: DocumentHeaderProps) { if (!page) return null; return ( -
+
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts index e9af1503128a4..c4753f9124da9 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/blocks/database/DatabaseList.hooks.ts @@ -1,24 +1,13 @@ import { useAppSelector } from '$app/stores/store'; -import { useEffect, useState } from 'react'; -import { Page } from '$app_reducers/pages/slice'; import { ViewLayoutPB } from '@/services/backend'; export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) { - const [list, setList] = useState([]); - const pages = useAppSelector((state) => state.pages.pageMap); - - useEffect(() => { - const list = Object.values(pages) - .map((page) => { - return page; - }) - .filter((page) => { - if (page.layout !== layout) return false; - return page.name.toLowerCase().includes(searchText.toLowerCase()); - }); - - setList(list); - }, [layout, pages, searchText]); + const list = useAppSelector((state) => { + return Object.values(state.pages.pageMap).filter((page) => { + if (page.layout !== layout) return false; + return page.name.toLowerCase().includes(searchText.toLowerCase()); + }); + }); return { list, 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 4e1642a8daa73..bf8df3c8e71b7 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 @@ -21,7 +21,7 @@ export const Text = memo( {renderIcon()} - {children} + {children} ); }) diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx index fe2eba8a59641..89ae42291ade1 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/inline_nodes/inline_formula/InlineFormula.tsx @@ -59,7 +59,7 @@ export const InlineFormula = memo( contentEditable={false} onDoubleClick={handleClick} onClick={handleClick} - className={`${attributes.className ?? ''} formula-inline relative rounded px-1 py-0.5 ${ + className={`${attributes.className ?? ''} formula-inline relative cursor-pointer rounded px-1 py-0.5 ${ selected ? 'selected' : '' }`} > 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 4a4baebf2a5f3..fb6f630214f8f 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 @@ -6,17 +6,29 @@ import { useNavigate } from 'react-router-dom'; import { pageTypeMap } from '$app_reducers/pages/slice'; import { getPage } from '$app/application/folder/page.service'; import { useSelected } from 'slate-react'; +import { ReactComponent as EyeClose } from '$app/assets/eye_close.svg'; +import { notify } from '$app/components/editor/components/tools/notify'; +import { subscribeNotifications } from '$app/application/notification'; +import { FolderNotification } from '@/services/backend'; export function MentionLeaf({ children, mention }: { mention: Mention; children: React.ReactNode }) { const { t } = useTranslation(); const [page, setPage] = useState(null); + const [error, setError] = useState(false); const navigate = useNavigate(); const selected = useSelected(); const loadPage = useCallback(async () => { + setError(true); if (!mention.page) return; - const page = await getPage(mention.page); + try { + const page = await getPage(mention.page); - setPage(page); + setPage(page); + setError(false); + } catch { + setPage(null); + setError(true); + } }, [mention.page]); useEffect(() => { @@ -24,26 +36,87 @@ export function MentionLeaf({ children, mention }: { mention: Mention; children: }, [loadPage]); const openPage = useCallback(() => { - if (!page) return; + if (!page) { + notify.error(t('document.mention.deletedContent')); + return; + } + const pageType = pageTypeMap[page.layout]; navigate(`/page/${pageType}/${page.id}`); - }, [navigate, page]); + }, [navigate, page, t]); + + useEffect(() => { + if (!page) return; + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateView]: (changeset) => { + setPage((prev) => { + if (!prev) { + return prev; + } + + return { + ...prev, + name: changeset.name, + }; + }); + }, + }, + { + id: page.id, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page]); + + useEffect(() => { + const parentId = page?.parentId; + + if (!parentId) return; + + const unsubscribePromise = subscribeNotifications( + { + [FolderNotification.DidUpdateChildViews]: (changeset) => { + if (changeset.delete_child_views.includes(page.id)) { + setPage(null); + setError(true); + } + }, + }, + { + id: parentId, + } + ); + + return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); + }, [page]); return ( - {page && ( - - {page.icon?.value || } - {page.name || t('document.title.placeholder')} - - )} + + {page && ( + <> + {page.icon?.value || } + {page.name || t('document.title.placeholder')} + + )} + {error && ( + <> + + + + {t('document.mention.deleted')} + + )} + {children} 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 11e41246e57bc..2f5f7a19d630d 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 @@ -22,9 +22,12 @@ const Toolbar = () => { const handleOpen = useCallback(() => { if (!node || !node.blockId) return; setOpenContextMenu(true); + const path = ReactEditor.findPath(editor, node); + + editor.select(path); selectedBlockContext.clear(); selectedBlockContext.add(node.blockId); - }, [node, selectedBlockContext]); + }, [editor, node, selectedBlockContext]); const handleClose = useCallback(() => { setOpenContextMenu(false); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx index 15dc3214ef46c..624b9ff0f1c1c 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/components/tools/block_actions/BlockOperationMenu.tsx @@ -12,7 +12,8 @@ import KeyboardNavigation, { KeyboardNavigationOption, } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; import { Color } from '$app/components/editor/components/tools/block_actions/color'; -import { getModifier } from '$app/components/editor/plugins/shortcuts'; +import { getModifier } from '$app/utils/get_modifier'; + import isHotkey from 'is-hotkey'; import { EditorNodeType } from '$app/application/document/document.types'; import { EditorSelectedBlockContext } from '$app/components/editor/stores/selected'; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss index 6cf7ec5e9f8fb..33e1e5fbe8b2f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/editor.scss @@ -25,10 +25,8 @@ .block-element.block-align-center { > div > .text-element { justify-content: center; - } - } @@ -52,12 +50,35 @@ span[data-slate-placeholder="true"]:not(.inline-block-content) { ::selection { @apply bg-content-blue-100; } + .text-content { + &::selection { + @apply bg-transparent; + } + span { + &::selection { + @apply bg-content-blue-100; + } + } + } } + + [data-dark-mode="true"] [role="textbox"]{ ::selection { background-color: #1e79a2; } + + .text-content { + &::selection { + @apply bg-transparent; + } + span { + &::selection { + background-color: #1e79a2; + } + } + } } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts index 0fb85a84ceae7..c0a401ebfd00a 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/editor/plugins/shortcuts/hotkey.ts @@ -1,17 +1,5 @@ import { EditorMarkFormat } from '$app/application/document/document.types'; - -export const isMac = () => { - return navigator.userAgent.includes('Mac OS X'); -}; - -const MODIFIERS = { - control: 'Ctrl', - meta: '⌘', -}; - -export const getModifier = () => { - return isMac() ? MODIFIERS.meta : MODIFIERS.control; -}; +import { getModifier } from '$app/utils/get_modifier'; /** * Hotkeys shortcuts 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 437ec6a576f78..f375bd9f3cdd9 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 @@ -47,6 +47,7 @@ export function useShortcuts(editor: ReactEditor) { if (format) { e.preventDefault(); + if (CustomEditor.selectionIncludeRoot(editor)) return; return CustomEditor.toggleMark(editor, { key: format, value: true, @@ -66,7 +67,7 @@ export function useShortcuts(editor: ReactEditor) { if (isHotkey(item.hotkey, e)) { e.stopPropagation(); e.preventDefault(); - + if (CustomEditor.selectionIncludeRoot(editor)) return; if (item.markKey === EditorMarkFormat.Align) { CustomEditor.toggleAlign(editor, item.markValue as string); return; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx index 57ed692912ca4..4f468f546175d 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/FooterPanel.tsx @@ -1,12 +1,12 @@ export const FooterPanel = () => { return ( -
+
© 2024 AppFlowy. GitHub
-
- -
+ {/*
*/} + {/* */} + {/*
*/}
); }; 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 a0a508a9758bd..c5bbb17424904 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/Layout.tsx @@ -2,7 +2,7 @@ import React, { ReactNode, useEffect } 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 { FooterPanel } from '$app/components/layout/FooterPanel'; +import './layout.scss'; function Layout({ children }: { children: ReactNode }) { const { isCollapsed, width } = useAppSelector((state) => state.sidebar); @@ -38,8 +38,6 @@ 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 5ef81d2e172ce..986ba9337d191 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 @@ -6,10 +6,11 @@ import Typography from '@mui/material/Typography'; import { Page, pageTypeMap } from '$app_reducers/pages/slice'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; +import { getPageIcon } from '$app/hooks/page.hooks'; function Breadcrumb() { const { t } = useTranslation(); - const { pagePath, currentPage } = useLoadExpandedPages(); + const { isTrash, pagePath, currentPage } = useLoadExpandedPages(); const navigate = useNavigate(); const parentPages = useMemo(() => pagePath.slice(1, -1).filter(Boolean) as Page[], [pagePath]); @@ -22,21 +23,35 @@ function Breadcrumb() { [navigate] ); + if (!currentPage) { + if (isTrash) { + return {t('trash.text')}; + } + + return null; + } + return ( {parentPages?.map((page: Page) => ( { navigateToPage(page); }} > +
{getPageIcon(page)}
+ {page.name || t('document.title.placeholder')} ))} - {currentPage?.name || t('menuAppHeader.defaultNewPageName')} + +
{getPageIcon(currentPage)}
+ {currentPage?.name || t('menuAppHeader.defaultNewPageName')} +
); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts index 807a8f2f658d6..5d65e6ef08c3b 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/bread_crumb/Breadcrumb.hooks.ts @@ -2,11 +2,9 @@ import { useAppSelector } from '$app/stores/store'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useParams, useLocation } from 'react-router-dom'; import { Page } from '$app_reducers/pages/slice'; -import { useTranslation } from 'react-i18next'; import { getPage } from '$app/application/folder/page.service'; export function useLoadExpandedPages() { - const { t } = useTranslation(); const params = useParams(); const location = useLocation(); const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]); @@ -70,18 +68,9 @@ export function useLoadExpandedPages() { }); }, [pageMap]); - useEffect(() => { - if (isTrash) { - setPagePath([ - { - name: t('trash.text'), - }, - ]); - } - }, [isTrash, t]); - return { pagePath, currentPage, + isTrash, }; } 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 850ac0b703f14..e1b6f9e414b53 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,22 +1,51 @@ -import React from 'react'; -import { IconButton } from '@mui/material'; +import React, { useCallback, useEffect, 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 LeftSvg } from '$app/assets/left.svg'; -import { ReactComponent as RightSvg } from '$app/assets/right.svg'; +import { ReactComponent as ShowMenuIcon } from '$app/assets/show-menu.svg'; +import { useTranslation } from 'react-i18next'; +import { getModifier } from '$app/utils/get_modifier'; +import isHotkey from 'is-hotkey'; function CollapseMenuButton() { const isCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); const dispatch = useAppDispatch(); - const handleClick = () => { + const handleClick = useCallback(() => { dispatch(sidebarActions.toggleCollapse()); - }; + }, [dispatch]); + + const { t } = useTranslation(); + + const title = useMemo(() => { + return ( +
+
{isCollapsed ? t('sideBar.openSidebar') : t('sideBar.closeSidebar')}
+
{`${getModifier()} + \\`}
+
+ ); + }, [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 ( - - {isCollapsed ? : } - + + + {isCollapsed ? : } + + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss new file mode 100644 index 0000000000000..a708777326293 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/layout.scss @@ -0,0 +1,12 @@ +.workspaces { + ::-webkit-scrollbar { + width: 0px; + } +} + +.MuiPopover-root, .MuiPaper-root { + ::-webkit-scrollbar { + width: 0; + height: 0; + } +} \ No newline at end of file diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx index 266775515c9c2..1387f16f4d928 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/AddButton.tsx @@ -1,63 +1,64 @@ -import React, { useMemo } from 'react'; -import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu'; -import { IconButton } from '@mui/material'; +import React, { useCallback, useMemo } from 'react'; import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { useTranslation } from 'react-i18next'; import { ReactComponent as DocumentSvg } from '$app/assets/document.svg'; import { ReactComponent as GridSvg } from '$app/assets/grid.svg'; import { ViewLayoutPB } from '@/services/backend'; +import OperationMenu from '$app/components/layout/nested_page/OperationMenu'; -function AddButton({ isVisible, onAddPage }: { isVisible: boolean; onAddPage: (layout: ViewLayoutPB) => void }) { +function AddButton({ + isHovering, + setHovering, + onAddPage, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + onAddPage: (layout: ViewLayoutPB) => void; +}) { const { t } = useTranslation(); + + const onConfirm = useCallback( + (key: string) => { + switch (key) { + case 'document': + onAddPage(ViewLayoutPB.Document); + break; + case 'grid': + onAddPage(ViewLayoutPB.Grid); + break; + default: + break; + } + }, + [onAddPage] + ); + const options = useMemo( () => [ { - key: 'add-document', - label: t('document.menuName'), - icon: ( -
- -
- ), - onClick: () => { - onAddPage(ViewLayoutPB.Document); - }, + key: 'document', + title: t('document.menuName'), + icon: , }, { - key: 'add-grid', - label: t('grid.menuName'), - icon: ( -
- -
- ), - onClick: () => { - onAddPage(ViewLayoutPB.Grid); - }, + key: 'grid', + title: t('grid.menuName'), + icon: , }, ], - [onAddPage, t] + [t] ); return ( - - - - - + + ); } diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx index 83ed658ac1c0b..4af8a2f2f14a4 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/DeleteDialog.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { ViewLayoutPB } from '@/services/backend'; -import DeleteConfirmDialog from '$app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; function DeleteDialog({ layout, diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx index 92bb254bd340a..e0a36a5903846 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/MoreButton.tsx @@ -1,25 +1,28 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { IconButton } from '@mui/material'; -import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu'; import { ReactComponent as DetailsSvg } from '$app/assets/details.svg'; import { ReactComponent as EditSvg } from '$app/assets/edit.svg'; import { ReactComponent as CopySvg } from '$app/assets/copy.svg'; import { ReactComponent as TrashSvg } from '$app/assets/delete.svg'; -import RenameDialog from './RenameDialog'; +import RenameDialog from '../../_shared/confirm_dialog/RenameDialog'; import { Page } from '$app_reducers/pages/slice'; import DeleteDialog from '$app/components/layout/nested_page/DeleteDialog'; +import OperationMenu from '$app/components/layout/nested_page/OperationMenu'; +import { getModifier } from '$app/utils/get_modifier'; +import isHotkey from 'is-hotkey'; function MoreButton({ - isVisible, onDelete, onDuplicate, onRename, page, + isHovering, + setHovering, }: { - isVisible: boolean; + isHovering: boolean; + setHovering: (hovering: boolean) => void; onDelete: () => Promise; onDuplicate: () => Promise; onRename: (newName: string) => Promise; @@ -29,84 +32,97 @@ function MoreButton({ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const { t } = useTranslation(); + + const onConfirm = useCallback( + (key: string) => { + switch (key) { + case 'rename': + setRenameDialogOpen(true); + break; + case 'delete': + setDeleteDialogOpen(true); + break; + case 'duplicate': + void onDuplicate(); + + break; + default: + break; + } + }, + [onDuplicate] + ); + const options = useMemo( () => [ { - label: t('disclosureAction.rename'), + title: t('button.rename'), + icon: , key: 'rename', - icon: ( -
- -
- ), - onClick: () => { - setRenameDialogOpen(true); - }, }, { - label: t('button.delete'), key: 'delete', - onClick: () => { - setDeleteDialogOpen(true); - }, - icon: ( -
- -
- ), + title: t('button.delete'), + icon: , + caption: 'Del', }, { key: 'duplicate', - label: t('button.duplicate'), - onClick: onDuplicate, - icon: ( -
- -
- ), + title: t('button.duplicate'), + icon: , + caption: `${getModifier()}+D`, }, ], - [onDuplicate, t] + [t] + ); + + const onKeyDown = useCallback( + (e: KeyboardEvent) => { + if (isHotkey('del', e) || isHotkey('backspace', e)) { + e.preventDefault(); + e.stopPropagation(); + onConfirm('delete'); + return; + } + + if (isHotkey('mod+d', e)) { + e.stopPropagation(); + onConfirm('duplicate'); + return; + } + }, + [onConfirm] ); return ( <> - - - - - - setRenameDialogOpen(false)} - onOk={async (newName: string) => { - await onRename(newName); - setRenameDialogOpen(false); - }} - /> + + + setDeleteDialogOpen(false)} - onOk={async () => { - await onDelete(); + onClose={() => { setDeleteDialogOpen(false); }} + onOk={onDelete} /> + {renameDialogOpen && ( + setRenameDialogOpen(false)} + onOk={onRename} + /> + )} ); } 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 8b94bd81f5fce..f2c2164f8cc08 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,12 +1,11 @@ -import { useCallback, useEffect, useMemo } from 'react'; -import { Page, pagesActions, pageTypeMap } from '$app_reducers/pages/slice'; +import { useCallback, useEffect } from 'react'; +import { pagesActions, pageTypeMap, 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 { createPage, deletePage, duplicatePage, getChildPages } from '$app/application/folder/page.service'; import { subscribeNotifications } from '$app/application/notification'; -import debounce from 'lodash-es/debounce'; export function useLoadChildPages(pageId: string) { const dispatch = useAppDispatch(); @@ -20,14 +19,6 @@ export function useLoadChildPages(pageId: string) { } }, [dispatch, pageId, collapsed]); - const onPageChanged = useMemo(() => { - return debounce((page: Page) => { - console.log('DidUpdateView'); - - dispatch(pagesActions.onPageChanged(page)); - }, 200); - }, [dispatch]); - const loadPageChildren = useCallback( async (pageId: string) => { const childPages = await getChildPages(pageId); @@ -49,9 +40,19 @@ export function useLoadChildPages(pageId: string) { useEffect(() => { const unsubscribePromise = subscribeNotifications( { - [FolderNotification.DidUpdateView]: (_payload) => { - // const page = parserViewPBToPage(payload); - // onPageChanged(page); + [FolderNotification.DidUpdateView]: async (payload) => { + const childViews = payload.child_views; + + if (childViews.length === 0) { + return; + } + + dispatch( + pagesActions.addChildPages({ + id: pageId, + childPages: childViews.map(parserViewPBToPage), + }) + ); }, [FolderNotification.DidUpdateChildViews]: async (payload) => { if (payload.delete_child_views.length === 0 && payload.create_child_views.length === 0) { @@ -67,7 +68,7 @@ export function useLoadChildPages(pageId: string) { ); return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); - }, [pageId, onPageChanged, loadPageChildren]); + }, [pageId, loadPageChildren, dispatch]); return { toggleCollapsed, @@ -79,13 +80,16 @@ export function useLoadChildPages(pageId: string) { export function usePageActions(pageId: string) { const page = useAppSelector((state) => state.pages.pageMap[pageId]); 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.layout, pageId]); + }, [navigate, page, pageId]); const onAddPage = useCallback( async (layout: ViewLayoutPB) => { @@ -95,6 +99,18 @@ export function usePageActions(pageId: string) { parent_view_id: pageId, }); + dispatch( + pagesActions.addPage({ + page: { + id: newViewId, + parentId: pageId, + layout, + name: '', + }, + isLast: true, + }) + ); + dispatch(pagesActions.expandPage(pageId)); const pageType = pageTypeMap[layout]; @@ -104,12 +120,17 @@ export function usePageActions(pageId: string) { ); const onDeletePage = useCallback(async () => { + if (currentPageId === pageId) { + navigate(`/`); + } + await deletePage(pageId); - }, [pageId]); + dispatch(pagesActions.deletePages([pageId])); + }, [dispatch, currentPageId, navigate, pageId]); const onDuplicatePage = useCallback(async () => { - await duplicatePage(pageId); - }, [pageId]); + await duplicatePage(page); + }, [page]); const onRenamePage = useCallback( async (name: string) => { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx index 54e283e878d26..cf03327302318 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/NestedPage.tsx @@ -4,28 +4,44 @@ import { TransitionGroup } from 'react-transition-group'; import NestedPageTitle from '$app/components/layout/nested_page/NestedPageTitle'; import { useLoadChildPages, usePageActions } from '$app/components/layout/nested_page/NestedPage.hooks'; import { useDrag } from 'src/appflowy_app/components/_shared/drag_block'; -import { useAppDispatch } from '$app/stores/store'; +import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { movePageThunk } from '$app_reducers/pages/async_actions'; +import { ViewLayoutPB } from '@/services/backend'; function NestedPage({ pageId }: { pageId: string }) { const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId); const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId); const dispatch = useAppDispatch(); + const page = useAppSelector((state) => state.pages.pageMap[pageId]); + const disableChildren = useAppSelector((state) => { + if (!page) return true; + const layout = state.pages.pageMap[page.parentId]?.layout; + + return !(layout === undefined || layout === ViewLayoutPB.Document); + }); const children = useMemo(() => { + if (disableChildren) { + return []; + } + return collapsed ? [] : childPages; - }, [collapsed, childPages]); + }, [collapsed, childPages, disableChildren]); const onDragFinished = useCallback( (result: { dragId: string; position: 'before' | 'after' | 'inside' }) => { + const { dragId, position } = result; + + if (dragId === pageId) return; + if (position === 'inside' && page?.layout !== ViewLayoutPB.Document) return; void dispatch( movePageThunk({ - sourceId: result.dragId, + sourceId: dragId, targetId: pageId, - insertType: result.position, + insertType: position, }) ); }, - [dispatch, pageId] + [dispatch, page?.layout, pageId] ); const { onDrop, dropPosition, onDragOver, onDragLeave, onDragStart, onDragEnd, isDraggingOver, isDragging } = useDrag({ @@ -34,20 +50,20 @@ function NestedPage({ pageId }: { pageId: string }) { }); const className = useMemo(() => { - const defaultClassName = 'relative flex-1 flex flex-col w-full'; + const defaultClassName = 'relative flex-1 select-none flex flex-col w-full'; if (isDragging) { return `${defaultClassName} opacity-40`; } - if (isDraggingOver && dropPosition === 'inside') { + if (isDraggingOver && dropPosition === 'inside' && page?.layout === ViewLayoutPB.Document) { if (dropPosition === 'inside') { return `${defaultClassName} bg-content-blue-100`; } } else { return defaultClassName; } - }, [dropPosition, isDragging, isDraggingOver]); + }, [dropPosition, isDragging, isDraggingOver, page?.layout]); return (
{ 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 41058ee327206..448fdc441ad30 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 @@ -1,11 +1,14 @@ -import React, { useState } from 'react'; -import { ArrowRightSvg } from '$app/components/_shared/svg/ArrowRightSvg'; +import React, { useMemo, useState } from 'react'; import { useAppSelector } from '$app/stores/store'; import AddButton from './AddButton'; import MoreButton from './MoreButton'; import { ViewLayoutPB } from '@/services/backend'; import { useSelectedPage } from '$app/components/layout/nested_page/NestedPage.hooks'; import { useTranslation } from 'react-i18next'; +import { ReactComponent as MoreIcon } from '$app/assets/more.svg'; +import { IconButton } from '@mui/material'; +import { Page } from '$app_reducers/pages/slice'; +import { getPageIcon } from '$app/hooks/page.hooks'; function NestedPageTitle({ pageId, @@ -28,51 +31,68 @@ function NestedPageTitle({ }) { const { t } = useTranslation(); const page = useAppSelector((state) => { - return state.pages.pageMap[pageId]; + return state.pages.pageMap[pageId] as Page | undefined; }); + const disableChildren = useAppSelector((state) => { + if (!page) return true; + const layout = state.pages.pageMap[page.parentId]?.layout; + + return !(layout === undefined || layout === ViewLayoutPB.Document); + }); + const [isHovering, setIsHovering] = useState(false); const isSelected = useSelectedPage(pageId); + const pageIcon = useMemo(() => (page ? getPageIcon(page) : null), [page]); + return (
setIsHovering(true)} + onMouseMove={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} >
-
- - {page.icon ?
{page.icon.value}
: null} +
+ {disableChildren ? ( +
+ ) : ( + { + e.stopPropagation(); + toggleCollapsed(); + }} + style={{ + transform: collapsed ? 'rotate(0deg)' : 'rotate(90deg)', + }} + > + + + )} + + {pageIcon}
- {page.name || t('menuAppHeader.defaultNewPageName')} + {page?.name || t('menuAppHeader.defaultNewPageName')}
-
e.stopPropagation()} className={'min:w-14 flex items-center justify-end'}> - - +
e.stopPropagation()} className={'min:w-14 flex items-center justify-end px-2'}> + {page?.layout === ViewLayoutPB.Document && ( + + )} + {page && ( + + )}
diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx new file mode 100644 index 0000000000000..cef1d3307c1d4 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/nested_page/OperationMenu.tsx @@ -0,0 +1,103 @@ +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; +import { IconButton } from '@mui/material'; +import Popover from '@mui/material/Popover'; +import KeyboardNavigation from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; +import Tooltip from '@mui/material/Tooltip'; + +function OperationMenu({ + options, + onConfirm, + isHovering, + setHovering, + children, + tooltip, + onKeyDown, +}: { + isHovering: boolean; + setHovering: (hovering: boolean) => void; + options: { + key: string; + title: string; + icon: React.ReactNode; + caption?: string; + }[]; + children: React.ReactNode; + onConfirm: (key: string) => void; + tooltip: string; + onKeyDown?: (e: KeyboardEvent) => void; +}) { + const [anchorEl, setAnchorEl] = useState(null); + const renderItem = useCallback((title: string, icon: ReactNode, caption?: string) => { + return ( +
+ {icon} +
{title}
+
{caption || ''}
+
+ ); + }, []); + + const handleClose = useCallback(() => { + setAnchorEl(null); + setHovering(false); + }, [setHovering]); + + const optionList = useMemo(() => { + return options.map((option) => { + return { + key: option.key, + content: renderItem(option.title, option.icon, option.caption), + }; + }); + }, [options, renderItem]); + + const open = Boolean(anchorEl); + + const handleConfirm = useCallback( + (key: string) => { + onConfirm(key); + handleClose(); + }, + [handleClose, onConfirm] + ); + + return ( + <> + + { + setAnchorEl(e.currentTarget); + }} + className={`${!isHovering ? 'invisible' : ''} text-icon-primary`} + > + {children} + + + + + + + + ); +} + +export default OperationMenu; 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 5c7830ad3aded..5e284a94bef28 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 @@ -10,6 +10,7 @@ function Resizer() { const startX = useRef(0); const onResize = useCallback( (e: MouseEvent) => { + e.preventDefault(); const diff = e.clientX - startX.current; const newWidth = width + diff; 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 cfde67bde6e44..cee598b66d1f1 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,6 +1,5 @@ import React from 'react'; import { useAppSelector } from '$app/stores/store'; -import { ThemeMode } from '$app/stores/reducers/current-user/slice'; import { AppflowyLogoDark } from '$app/components/_shared/svg/AppflowyLogoDark'; import { AppflowyLogoLight } from '$app/components/_shared/svg/AppflowyLogoLight'; import CollapseMenuButton from '$app/components/layout/collapse_menu_button/CollapseMenuButton'; @@ -10,19 +9,19 @@ import WorkspaceManager from '$app/components/layout/workspace_manager/Workspace function SideBar() { const { isCollapsed, width, isResizing } = useAppSelector((state) => state.sidebar); - const isDark = useAppSelector((state) => state.currentUser?.userSetting?.themeMode === ThemeMode.Dark); + const isDark = useAppSelector((state) => state.currentUser?.userSetting?.isDark); return ( <>
-
+
{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 bde742f57a40b..b02620e88c43b 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,37 +1,46 @@ import React, { useState } from 'react'; import { useAppSelector } from '$app/stores/store'; -import { Avatar } from '@mui/material'; +import { Avatar, IconButton } from '@mui/material'; import PersonOutline from '@mui/icons-material/PersonOutline'; -import ArrowDropDown from '@mui/icons-material/ArrowDropDown'; import UserSetting from '../user_setting/UserSetting'; +import { ReactComponent as SettingIcon } from '$app/assets/settings.svg'; +import Tooltip from '@mui/material/Tooltip'; +import { useTranslation } from 'react-i18next'; function UserInfo() { const currentUser = useAppSelector((state) => state.currentUser); const [showUserSetting, setShowUserSetting] = useState(false); + const { t } = useTranslation(); + return ( <> -
{ - e.stopPropagation(); - setShowUserSetting(!showUserSetting); - }} - className={'flex cursor-pointer items-center px-6 text-text-title'} - > - - - - {currentUser.displayName} - +
+
+ + {currentUser.displayName ? currentUser.displayName[0] : } + + {currentUser.displayName} +
+ + + { + setShowUserSetting(!showUserSetting); + }} + > + + +
setShowUserSetting(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 178c596f6bdbd..d1b0fead634ff 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,8 +2,6 @@ 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 ShareButton from '$app/components/layout/share/Share'; -import MoreButton from '$app/components/layout/top_bar/MoreButton'; function TopBar() { const sidebarIsCollapsed = useAppSelector((state) => state.sidebar.isCollapsed); @@ -19,13 +17,6 @@ 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 index e2d0367a51a46..e28a467526c91 100644 --- 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 @@ -1,12 +1,11 @@ -import React, { useCallback, useEffect, useMemo } from 'react'; +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({ - theme = Theme.Default, - themeMode = ThemeMode.Light, + themeMode = ThemeMode.System, onChange, }: { theme?: Theme; @@ -15,13 +14,6 @@ function AppearanceSetting({ }) { const { t } = useTranslation(); - useEffect(() => { - const html = document.documentElement; - - html?.setAttribute('data-dark-mode', String(themeMode === ThemeMode.Dark)); - html?.setAttribute('data-theme', theme); - }, [theme, themeMode]); - const themeModeOptions = useMemo( () => [ { @@ -32,6 +24,10 @@ function AppearanceSetting({ value: ThemeMode.Dark, content: t('settings.appearance.themeMode.dark'), }, + { + value: ThemeMode.System, + content: t('settings.appearance.themeMode.system'), + }, ], [t] ); @@ -63,7 +59,7 @@ function AppearanceSetting({ }} > {options.map((option) => ( - + {option.content} ))} @@ -86,6 +82,9 @@ function AppearanceSetting({ onChange: (newValue) => { onChange({ themeMode: newValue as ThemeMode, + isDark: + newValue === ThemeMode.Dark || + (newValue === ThemeMode.System && window.matchMedia('(prefers-color-scheme: dark)').matches), }); }, }, 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 index 7ad9997567502..81e7d067a4442 100644 --- 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 @@ -61,7 +61,7 @@ function LanguageSetting({ }} > {languages.map((option) => ( - + {option.title} ))} 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 index 2d5bb0ff3c43a..9da3cb8f74242 100644 --- 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 @@ -27,7 +27,7 @@ function UserSettingMenu({ selected, onSelect }: { onSelect: (selected: MenuItem }, [t]); return ( -
+
{options.map((option) => { return (
{ onSelect(option.value); }} - className={`my-1 flex h-10 w-full cursor-pointer items-center justify-start rounded-md px-4 py-2 text-text-title ${ + 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' }`} > 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 index 7fd52de56a8f1..7cfbac4a7646d 100644 --- 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 @@ -5,10 +5,9 @@ import DialogTitle from '@mui/material/DialogTitle'; import Slide, { SlideProps } from '@mui/material/Slide'; import UserSettingMenu, { MenuItem } from './Menu'; import UserSettingPanel from './SettingPanel'; -import { Theme, UserSetting } from '$app/stores/reducers/current-user/slice'; +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 { ThemeModePB } from '@/services/backend'; import { useTranslation } from 'react-i18next'; import { UserService } from '$app/application/user/user.service'; @@ -29,8 +28,8 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void }) const language = newSetting.language || 'en'; void UserService.setAppearanceSetting({ - theme: newSetting.theme || Theme.Default, - theme_mode: newSetting.themeMode || ThemeModePB.Light, + theme: newSetting.theme, + theme_mode: newSetting.themeMode, locale: { language_code: language.split('-')[0], country_code: language.split('-')[1], @@ -48,7 +47,7 @@ function UserSettings({ open, onClose }: { open: boolean; onClose: () => void }) keepMounted={false} onClose={onClose} > - {t('settings.title')} + {t('settings.title')} { diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx deleted file mode 100644 index efdd8d11447c4..0000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/MoreButton.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import React, { useMemo } from 'react'; -import { WorkspaceItem } from '$app_reducers/workspace/slice'; -import { IconButton } from '@mui/material'; -import MoreIcon from '@mui/icons-material/MoreHoriz'; -import SettingsIcon from '@mui/icons-material/Settings'; -import { useTranslation } from 'react-i18next'; -import { DeleteOutline } from '@mui/icons-material'; -import ButtonPopoverList from '$app/components/_shared/button_menu/ButtonMenu'; - -function MoreButton({ - workspace, - isHovered, - onDelete, -}: { - isHovered: boolean; - workspace: WorkspaceItem; - onDelete: (id: string) => void; -}) { - const { t } = useTranslation(); - - const options = useMemo(() => { - return [ - { - key: 'settings', - icon: , - label: t('settings.title'), - onClick: () => { - // - }, - }, - { - key: 'delete', - icon: , - label: t('button.delete'), - onClick: () => onDelete(workspace.id), - }, - ]; - }, [onDelete, t, workspace.id]); - - return ( - <> - - - - - - - ); -} - -export default MoreButton; diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx index 80d61440b78f8..ec3335b6b325f 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NestedPages.tsx @@ -8,7 +8,7 @@ function WorkspaceNestedPages({ workspaceId }: { workspaceId: string }) { }); return ( -
+
{pageIds?.map((pageId) => ( ))} diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx index f82cb2208e137..537b7d2d9a2e5 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/NewPageButton.tsx @@ -1,35 +1,29 @@ import React from 'react'; -import AddSvg from '$app/components/_shared/svg/AddSvg'; import { useTranslation } from 'react-i18next'; -import { ViewLayoutPB } from '@/services/backend'; -import { useNavigate } from 'react-router-dom'; -import { createCurrentWorkspaceChildView } from '$app/application/folder/workspace.service'; +import { useWorkspaceActions } from '$app/components/layout/workspace_manager/Workspace.hooks'; +import Button from '@mui/material/Button'; +import { ReactComponent as AddSvg } from '$app/assets/add.svg'; function NewPageButton({ workspaceId }: { workspaceId: string }) { const { t } = useTranslation(); - const navigate = useNavigate(); + const { newPage } = useWorkspaceActions(workspaceId); return ( -
-
-
+ } + className={'flex w-full items-center justify-start text-xs hover:bg-transparent hover:text-fill-default'} + > {t('newPageText')} - +
); } 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 944c23df01c4c..e89af3686993a 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 @@ -30,14 +30,14 @@ function TrashButton() { onDragLeave={onDragLeave} data-page-id={'trash'} onClick={navigateToTrash} - className={`mx-1 my-3 flex h-[32px] w-[100%] items-center rounded-lg p-2 hover:bg-fill-list-hover ${ + className={`my-3 flex h-[32px] w-[100%] cursor-pointer items-center gap-2 rounded-lg p-3.5 text-xs font-medium hover:bg-fill-list-hover ${ selected ? 'bg-fill-list-active' : '' } ${isDraggingOver ? 'bg-fill-list-hover' : ''}`} > -
+
- {t('trash.text')} + {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 2b5ac678e8fd2..7d77b12d6934b 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 @@ -3,8 +3,10 @@ import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { workspaceActions, WorkspaceItem } from '$app_reducers/workspace/slice'; import { Page, pagesActions, parserViewPBToPage } from '$app_reducers/pages/slice'; import { subscribeNotifications } from '$app/application/notification'; -import { FolderNotification } from '@/services/backend'; +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'; export function useLoadWorkspaces() { const dispatch = useAppDispatch(); @@ -95,3 +97,21 @@ export function useLoadWorkspace(workspace: WorkspaceItem) { deleteWorkspace, }; } + +export function useWorkspaceActions(workspaceId: string) { + const navigate = useNavigate(); + + const newPage = useCallback(async () => { + const { id } = await createCurrentWorkspaceChildView({ + name: '', + layout: ViewLayoutPB.Document, + parent_view_id: workspaceId, + }); + + navigate(`/page/document/${id}`); + }, [navigate, 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 db9ce8f4b1447..165a9ab1d1fb5 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 @@ -1,20 +1,65 @@ -import React from 'react'; +import React, { useState } from 'react'; import { WorkspaceItem } from '$app_reducers/workspace/slice'; import NestedViews from '$app/components/layout/workspace_manager/NestedPages'; -import { useLoadWorkspace } from '$app/components/layout/workspace_manager/Workspace.hooks'; +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'; function Workspace({ workspace, opened }: { workspace: WorkspaceItem; opened: boolean }) { useLoadWorkspace(workspace); + const { t } = useTranslation(); + const { newPage } = useWorkspaceActions(workspace.id); + const [showPages, setShowPages] = useState(true); + const [showAdd, setShowAdd] = useState(false); + return ( <>
- +
{ + e.stopPropagation(); + e.preventDefault(); + setShowPages(!showPages); + }} + onMouseEnter={() => { + setShowAdd(true); + }} + onMouseLeave={() => { + setShowAdd(false); + }} + className={'mt-2 flex h-[22px] w-full cursor-pointer select-none items-center justify-between px-4'} + > + + + {t('sideBar.personal')} + + + {showAdd && ( + + { + e.stopPropagation(); + void newPage(); + }} + size={'small'} + > + + + + )} +
+ + {showPages && }
); 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 a7a90e785f343..c6404d435ce7a 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 @@ -8,18 +8,17 @@ function WorkspaceManager() { const { workspaces, currentWorkspace } = useLoadWorkspaces(); return ( -
-
+
+
{workspaces.map((workspace) => ( ))}
-
- -
- +
+ +
{currentWorkspace && }
); diff --git a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx b/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx deleted file mode 100644 index 4c9f8988a6031..0000000000000 --- a/frontend/appflowy_tauri/src/appflowy_app/components/layout/workspace_manager/WorkspaceTitle.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useState } from 'react'; -import MoreButton from '$app/components/layout/workspace_manager/MoreButton'; -import MenuItem from '@mui/material/MenuItem'; -import { WorkspaceItem } from '$app_reducers/workspace/slice'; - -function WorkspaceTitle({ - workspace, - openWorkspace, - onDelete, -}: { - openWorkspace: () => void; - onDelete: (id: string) => void; - workspace: WorkspaceItem; -}) { - const [isHovered, setIsHovered] = useState(false); - - return ( - openWorkspace()} - onMouseEnter={() => { - setIsHovered(true); - }} - onMouseLeave={() => { - setIsHovered(false); - }} - className={'hover:bg-fill-list-active'} - > -
-
{workspace.name}
-
- -
-
-
- ); -} - -export default WorkspaceTitle; 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 6f0a0f94f65b4..40f51d1fbf5dc 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx +++ b/frontend/appflowy_tauri/src/appflowy_app/components/trash/Trash.tsx @@ -3,9 +3,9 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import { DeleteOutline, RestoreOutlined } from '@mui/icons-material'; import { useLoadTrash, useTrashActions } from '$app/components/trash/Trash.hooks'; -import { Divider, List } from '@mui/material'; +import { List } from '@mui/material'; import TrashItem from '$app/components/trash/TrashItem'; -import DeleteConfirmDialog from '$app/components/_shared/delete_confirm_dialog/DeleteConfirmDialog'; +import DeleteConfirmDialog from '$app/components/_shared/confirm_dialog/DeleteConfirmDialog'; function Trash() { const { t } = useTranslation(); @@ -26,7 +26,7 @@ function Trash() { return (
-
{t('trash.text')}
+
{t('trash.text')}
-
+
{t('trash.pageHeader.fileName')}
{t('trash.pageHeader.lastModified')}
{t('trash.pageHeader.created')}
- {trash.map((item) => ( -
-
{item.name}
+
+
{item.name || t('document.title.placeholder')}
{dayjs.unix(item.modifiedTime).format('MM/DD/YYYY hh:mm A')}
{dayjs.unix(item.createTime).format('MM/DD/YYYY hh:mm A')}
- onPutback(item.id)} className={'mr-2'}> + onPutback(item.id)} className={'mr-2'}> - onDelete([item.id])}> + onDelete([item.id])}> diff --git a/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx b/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx new file mode 100644 index 0000000000000..49e01e75c0a76 --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/hooks/page.hooks.tsx @@ -0,0 +1,26 @@ +import { ViewLayoutPB } from '@/services/backend'; +import React from 'react'; +import { Page } from '$app_reducers/pages/slice'; +import { ReactComponent as DocumentIcon } from '$app/assets/document.svg'; +import { ReactComponent as GridIcon } from '$app/assets/grid.svg'; +import { ReactComponent as BoardIcon } from '$app/assets/board.svg'; +import { ReactComponent as CalendarIcon } from '$app/assets/date.svg'; + +export function getPageIcon(page: Page) { + if (page.icon) { + return page.icon.value; + } + + switch (page.layout) { + case ViewLayoutPB.Document: + return ; + case ViewLayoutPB.Grid: + return ; + case ViewLayoutPB.Board: + return ; + case ViewLayoutPB.Calendar: + return ; + default: + return null; + } +} 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 8bb8d42b0e75b..13ad58117583b 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 @@ -8,6 +8,7 @@ export interface UserSetting { theme?: Theme; themeMode?: ThemeMode; language?: string; + isDark?: boolean; } export enum Theme { 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 23f4d85cbcc45..ebb78bb7fa37f 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 @@ -14,7 +14,7 @@ export const movePageThunk = createAsyncThunk( thunkAPI ) => { const { sourceId, targetId, insertType } = payload; - const { getState } = thunkAPI; + const { getState, dispatch } = thunkAPI; const { pageMap, relationMap } = (getState() as RootState).pages; const sourcePage = pageMap[sourceId]; const targetPage = pageMap[targetId]; @@ -51,6 +51,8 @@ export const movePageThunk = createAsyncThunk( } } + dispatch(pagesActions.movePage({ id: sourceId, newParentId: parentId, prevId })); + await movePage({ view_id: sourceId, new_parent_id: parentId, 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 e5b08ae095a2a..8d3f07507ec5d 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 @@ -89,10 +89,85 @@ export const pagesSlice = createSlice({ } }, - removeChildPages(state, action: PayloadAction) { - const parentId = action.payload; + addPage( + state, + action: PayloadAction<{ + page: Page; + isLast?: boolean; + prevId?: string; + }> + ) { + const { page, prevId, isLast } = action.payload; + + state.pageMap[page.id] = page; + state.relationMap[page.id] = []; + + const parentId = page.parentId; + + if (isLast) { + state.relationMap[parentId]?.push(page.id); + } else { + const index = prevId ? state.relationMap[parentId]?.indexOf(prevId) ?? -1 : -1; + + state.relationMap[parentId]?.splice(index + 1, 0, page.id); + } + }, + + deletePages(state, action: PayloadAction) { + const ids = action.payload; + + ids.forEach((id) => { + const parentId = state.pageMap[id].parentId; + const parentChildren = state.relationMap[parentId]; + + state.relationMap[parentId] = parentChildren && parentChildren.filter((childId) => childId !== id); + delete state.relationMap[id]; + delete state.expandedIdMap[id]; + delete state.pageMap[id]; + }); + }, + + duplicatePage( + state, + action: PayloadAction<{ + id: string; + newId: string; + }> + ) { + const { id, newId } = action.payload; + const page = state.pageMap[id]; + const newPage = { ...page, id: newId }; + + state.pageMap[newPage.id] = newPage; + + const index = state.relationMap[page.parentId]?.indexOf(id); + + state.relationMap[page.parentId]?.splice(index ?? 0, 0, newId); + }, + + movePage( + state, + action: PayloadAction<{ + id: string; + newParentId: string; + prevId?: string; + }> + ) { + const { id, newParentId, prevId } = action.payload; + const parentId = state.pageMap[id].parentId; + const parentChildren = state.relationMap[parentId]; + + const index = parentChildren?.indexOf(id) ?? -1; + + if (index > -1) { + state.relationMap[parentId]?.splice(index, 1); + } + + state.pageMap[id].parentId = newParentId; + const newParentChildren = state.relationMap[newParentId] || []; + const prevIndex = prevId ? newParentChildren.indexOf(prevId) : -1; - delete state.relationMap[parentId]; + state.relationMap[newParentId]?.splice(prevIndex + 1, 0, id); }, expandPage(state, action: PayloadAction) { diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts new file mode 100644 index 0000000000000..a81e5e9093d5b --- /dev/null +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/get_modifier.ts @@ -0,0 +1,12 @@ +export const isMac = () => { + return navigator.userAgent.includes('Mac OS X'); +}; + +const MODIFIERS = { + control: 'Ctrl', + meta: '⌘', +}; + +export const getModifier = () => { + return isMac() ? MODIFIERS.meta : MODIFIERS.control; +}; diff --git a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts index 2bdf102a6e38f..fa2520bb7a0ea 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/utils/mui.ts @@ -1,11 +1,8 @@ -import { ThemeMode } from '$app/stores/reducers/current-user/slice'; import { ThemeOptions } from '@mui/material'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore -export const getDesignTokens = (mode: ThemeMode): ThemeOptions => { - const isDark = mode === ThemeMode.Dark; - +export const getDesignTokens = (isDark: boolean): ThemeOptions => { return { typography: { fontFamily: ['Poppins'].join(','), diff --git a/frontend/appflowy_tauri/src/styles/template.css b/frontend/appflowy_tauri/src/styles/template.css index e8fe35e4404fd..f17595b74a934 100644 --- a/frontend/appflowy_tauri/src/styles/template.css +++ b/frontend/appflowy_tauri/src/styles/template.css @@ -24,10 +24,6 @@ body { width: 8px; } -.MuiPopover-root::-webkit-scrollbar { - width: 0; - height: 0; -} :root[data-dark-mode=true] body { diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 8d4f9ce4dd9f9..93842e59c2534 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -879,7 +879,9 @@ "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" From 747fe40648e6b16c70d794af8c7b86e8431a4ff6 Mon Sep 17 00:00:00 2001 From: Ansah Mohammad Date: Thu, 8 Feb 2024 15:15:54 +0530 Subject: [PATCH 24/50] feat: toggle favourites from the documents top bar. (#4382) * feat: init star button * feat: implemented star button on the right bar * feat: Implemented toggle logic * chore: Added ToolTip for favorite button * refactor: renamed star to favorite * refactor: moved appplication logic into favorite_cubit * chore: arranged imports in alphabetical order * refactor: reused favorite_bloc for toggle * fix: fixed the icon no rebuild * refactor: moved the FavoriteBloc higher in the widget tree * refactor: moved FavoriteBloc inside multiBlocProvider of desktop_home_screen * chore: specified bloc type * chore: cleaned code * refactor: use any instead of map --- .../lib/plugins/document/document.dart | 6 +++ .../favorite/favorite_button.dart | 54 +++++++++++++++++++ .../home/desktop_home_screen.dart | 5 ++ .../home/menu/sidebar/sidebar.dart | 3 -- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/favorite/favorite_button.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/document.dart b/frontend/appflowy_flutter/lib/plugins/document/document.dart index c9cc4f2ef83b1..e6b0abebe2ca6 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/document.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/document.dart @@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/document_page.dart'; import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/presentation/more/more_button.dart'; import 'package:appflowy/plugins/document/presentation/share/share_button.dart'; +import 'package:appflowy/plugins/document/presentation/favorite/favorite_button.dart'; import 'package:appflowy/plugins/util.dart'; import 'package:appflowy/startup/plugin/plugin.dart'; import 'package:appflowy/workspace/presentation/home/home_stack.dart'; @@ -121,6 +122,11 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder children: [ DocumentShareButton(key: ValueKey(view.id), view: view), const HSpace(4), + DocumentFavoriteButton( + key: ValueKey('favorite_button_${view.id}'), + view: view, + ), + const HSpace(4), const DocumentMoreButton(), ], ); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/favorite/favorite_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/favorite/favorite_button.dart new file mode 100644 index 0000000000000..b82b68dee4061 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/favorite/favorite_button.dart @@ -0,0 +1,54 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; +import 'package:easy_localization/easy_localization.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 DocumentFavoriteButton extends StatelessWidget { + const DocumentFavoriteButton({ + super.key, + required this.view, + }); + + final ViewPB view; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final isFavorite = state.views.any((v) => v.id == view.id); + return _buildFavoriteButton(context, isFavorite); + }, + ); + } + + Widget _buildFavoriteButton(BuildContext context, bool isFavorite) { + return FlowyTooltip( + message: isFavorite + ? LocaleKeys.button_removeFromFavorites.tr() + : LocaleKeys.button_addToFavorites.tr(), + child: FlowyHover( + child: GestureDetector( + onTap: () => + context.read().add(FavoriteEvent.toggle(view)), + child: _buildFavoriteIcon(context, isFavorite), + ), + ), + ); + } + + Widget _buildFavoriteIcon(BuildContext context, bool isFavorite) { + return Padding( + padding: const EdgeInsets.all(6), + child: FlowySvg( + isFavorite ? FlowySvgs.favorite_s : FlowySvgs.unfavorite_s, + size: const Size(18, 18), + color: Theme.of(context).iconTheme.color, + ), + ); + } +} diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart index 9f17f22799ad0..3335b67c8ec5a 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/home/desktop_home_screen.dart @@ -13,6 +13,7 @@ import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart'; import 'package:appflowy/workspace/presentation/home/hotkeys.dart'; import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar.dart'; +import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart'; import 'package:appflowy/workspace/presentation/widgets/edit_panel/panel_animation.dart'; import 'package:appflowy/workspace/presentation/widgets/float_bubble/question_bubble.dart'; import 'package:appflowy_backend/dispatch/dispatch.dart'; @@ -85,6 +86,10 @@ class DesktopHomeScreen extends StatelessWidget { )..add(const HomeSettingEvent.initial()); }, ), + BlocProvider( + create: (context) => + FavoriteBloc()..add(const FavoriteEvent.initial()), + ), ], child: HomeHotKeys( child: Scaffold( 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 f3814a164f990..af6f475698da3 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 @@ -51,9 +51,6 @@ class HomeSideBar extends StatelessWidget { workspaceId: workspaceSetting.workspaceId, )..add(const MenuEvent.initial()), ), - BlocProvider( - create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()), - ), ], child: MultiBlocListener( listeners: [ From 3b51a6e6be1f0e41d6fc1f3ffcf8f63a73547909 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Thu, 8 Feb 2024 23:53:05 +0800 Subject: [PATCH 25/50] chore: reload workspace when fail to load at the first time (#4633) * chore: reload workspace when fail to load at the first time * fix: clippy * chore: update client api * chore: fix wasm build * chore: fix test --- frontend/appflowy_tauri/src-tauri/Cargo.lock | 107 +++++++++++++++--- frontend/appflowy_tauri/src-tauri/Cargo.toml | 16 +-- frontend/appflowy_web/wasm-libs/Cargo.lock | 106 ++++++++++++++--- frontend/appflowy_web/wasm-libs/Cargo.toml | 16 +-- .../wasm-libs/af-wasm/src/integrate/server.rs | 1 + frontend/rust-lib/Cargo.lock | 101 ++++++++++++++--- frontend/rust-lib/Cargo.toml | 16 +-- .../flowy-core/src/integrate/server.rs | 1 + .../rust-lib/flowy-core/src/integrate/user.rs | 11 +- frontend/rust-lib/flowy-core/src/lib.rs | 2 +- frontend/rust-lib/flowy-error/src/code.rs | 16 +-- frontend/rust-lib/flowy-error/src/errors.rs | 8 +- .../src/entities/parser/view/mod.rs | 2 - .../src/entities/parser/view/view_desc.rs | 21 ---- .../entities/parser/workspace/workspace_id.rs | 2 +- .../flowy-folder/src/entities/view.rs | 9 +- .../flowy-folder/src/event_handler.rs | 8 ++ .../rust-lib/flowy-folder/src/event_map.rs | 4 + frontend/rust-lib/flowy-folder/src/manager.rs | 32 +++++- .../rust-lib/flowy-folder/src/manager_init.rs | 32 ++++-- .../af_cloud/impls/user/cloud_service_impl.rs | 2 + .../flowy-server/src/af_cloud/server.rs | 21 +++- .../flowy-server/tests/af_cloud_test/util.rs | 11 +- frontend/rust-lib/flowy-user-pub/src/cloud.rs | 19 ++-- .../rust-lib/flowy-user/src/event_handler.rs | 15 ++- .../flowy-user/src/user_manager/manager.rs | 84 +++++++++----- 26 files changed, 467 insertions(+), 196 deletions(-) delete mode 100644 frontend/rust-lib/flowy-folder/src/entities/parser/view/view_desc.rs diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.lock b/frontend/appflowy_tauri/src-tauri/Cargo.lock index d37cc705e2299..47aa98b6a709e 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.lock +++ b/frontend/appflowy_tauri/src-tauri/Cargo.lock @@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -714,7 +714,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "again", "anyhow", @@ -732,6 +732,7 @@ dependencies = [ "getrandom 0.2.10", "gotrue", "gotrue-entity", + "governor", "mime", "mime_guess", "parking_lot 0.12.1", @@ -816,7 +817,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-trait", @@ -838,7 +839,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-trait", @@ -867,7 +868,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "collab", @@ -886,7 +887,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "bytes", @@ -901,7 +902,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "chrono", @@ -938,7 +939,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-stream", @@ -977,7 +978,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "collab", @@ -1311,7 +1312,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -2261,6 +2262,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.30" @@ -2577,7 +2584,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "futures-util", @@ -2594,7 +2601,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -2605,6 +2612,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "governor" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.1", + "quanta", + "rand 0.8.5", + "smallvec", +] + [[package]] name = "gtk" version = "0.15.5" @@ -3031,7 +3056,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "reqwest", @@ -3391,6 +3416,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "macroific" version = "1.3.1" @@ -3651,6 +3685,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[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" @@ -3667,6 +3707,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4562,6 +4608,22 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-xml" version = "0.28.2" @@ -4678,6 +4740,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "raw-window-handle" version = "0.5.2" @@ -4709,7 +4780,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -4732,7 +4803,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -5380,7 +5451,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -6860,7 +6931,7 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "futures-channel", "futures-util", @@ -7260,7 +7331,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/appflowy_tauri/src-tauri/Cargo.toml b/frontend/appflowy_tauri/src-tauri/Cargo.toml index 370d1edc573a6..f6a730e8369a5 100644 --- a/frontend/appflowy_tauri/src-tauri/Cargo.toml +++ b/frontend/appflowy_tauri/src-tauri/Cargo.toml @@ -82,7 +82,7 @@ custom-protocol = ["tauri/custom-protocol"] # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23ad1c4de34c8333521b364f2e1f69695d72bb5" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "29a0851f485957cc6410ccf9d261c781c1d2f757" } # Please use the following script to update collab. # Working directory: frontend # @@ -92,10 +92,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } diff --git a/frontend/appflowy_web/wasm-libs/Cargo.lock b/frontend/appflowy_web/wasm-libs/Cargo.lock index 8b15b1a27fab3..36621755a2ba1 100644 --- a/frontend/appflowy_web/wasm-libs/Cargo.lock +++ b/frontend/appflowy_web/wasm-libs/Cargo.lock @@ -221,7 +221,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -545,7 +545,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "again", "anyhow", @@ -563,6 +563,7 @@ dependencies = [ "getrandom 0.2.12", "gotrue", "gotrue-entity", + "governor", "mime", "mime_guess", "parking_lot 0.12.1", @@ -616,7 +617,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-trait", @@ -638,7 +639,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "collab", @@ -657,7 +658,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "bytes", @@ -672,7 +673,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "chrono", @@ -709,7 +710,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-stream", @@ -747,7 +748,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "collab", @@ -944,7 +945,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -1376,6 +1377,7 @@ dependencies = [ "collab", "collab-document", "collab-entity", + "collab-folder", "collab-plugins", "flowy-database-pub", "flowy-document-pub", @@ -1569,6 +1571,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.30" @@ -1690,7 +1698,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "futures-util", @@ -1707,7 +1715,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -1718,6 +1726,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "governor" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" +dependencies = [ + "cfg-if 1.0.0", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.1", + "quanta", + "rand 0.8.5", + "smallvec", +] + [[package]] name = "h2" version = "0.3.24" @@ -2023,7 +2049,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "reqwest", @@ -2258,6 +2284,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "macroific" version = "1.3.1" @@ -2424,6 +2459,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -2434,6 +2475,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "num-bigint" version = "0.4.4" @@ -3142,6 +3189,22 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quote" version = "1.0.35" @@ -3232,10 +3295,19 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -3258,7 +3330,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -3705,7 +3777,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -4647,7 +4719,7 @@ checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" [[package]] name = "websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "futures-channel", "futures-util", @@ -4954,4 +5026,4 @@ dependencies = [ [[patch.unused]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=d00b477a9b844d86b5caeff573ca395dc5bf7198#d00b477a9b844d86b5caeff573ca395dc5bf7198" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" diff --git a/frontend/appflowy_web/wasm-libs/Cargo.toml b/frontend/appflowy_web/wasm-libs/Cargo.toml index 24ddff37be92e..1cefba59adbce 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 = "d23ad1c4de34c8333521b364f2e1f69695d72bb5" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "29a0851f485957cc6410ccf9d261c781c1d2f757" } # 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 = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } 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 d5263fab06535..0dfbf45f05f59 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 @@ -41,6 +41,7 @@ impl ServerProviderWASM { self.config.clone(), true, self.device_id.clone(), + "0.0.1" )); *self.server.write() = Some(server.clone()); server diff --git a/frontend/rust-lib/Cargo.lock b/frontend/rust-lib/Cargo.lock index 494c8af709cc9..0ac19e2ddca02 100644 --- a/frontend/rust-lib/Cargo.lock +++ b/frontend/rust-lib/Cargo.lock @@ -163,7 +163,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "app-error" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -673,7 +673,7 @@ dependencies = [ [[package]] name = "client-api" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "again", "anyhow", @@ -691,6 +691,7 @@ dependencies = [ "getrandom 0.2.10", "gotrue", "gotrue-entity", + "governor", "mime", "mime_guess", "parking_lot 0.12.1", @@ -744,7 +745,7 @@ dependencies = [ [[package]] name = "collab" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-trait", @@ -766,7 +767,7 @@ dependencies = [ [[package]] name = "collab-database" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-trait", @@ -795,7 +796,7 @@ dependencies = [ [[package]] name = "collab-document" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "collab", @@ -814,7 +815,7 @@ dependencies = [ [[package]] name = "collab-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "bytes", @@ -829,7 +830,7 @@ dependencies = [ [[package]] name = "collab-folder" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "chrono", @@ -866,7 +867,7 @@ dependencies = [ [[package]] name = "collab-plugins" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "async-stream", @@ -905,7 +906,7 @@ dependencies = [ [[package]] name = "collab-user" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2ce95b948accd6d14b97ee886f4416295acd9c65#2ce95b948accd6d14b97ee886f4416295acd9c65" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=2de6d172f56fed29ee6f32b82040cca4867647ac#2de6d172f56fed29ee6f32b82040cca4867647ac" dependencies = [ "anyhow", "collab", @@ -1235,7 +1236,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "database-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -2407,7 +2408,7 @@ dependencies = [ [[package]] name = "gotrue" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "futures-util", @@ -2424,7 +2425,7 @@ dependencies = [ [[package]] name = "gotrue-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -2435,6 +2436,24 @@ dependencies = [ "serde_json", ] +[[package]] +name = "governor" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" +dependencies = [ + "cfg-if", + "dashmap", + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.1", + "quanta", + "rand 0.8.5", + "smallvec", +] + [[package]] name = "h2" version = "0.3.21" @@ -2800,7 +2819,7 @@ dependencies = [ [[package]] name = "infra" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "reqwest", @@ -3053,6 +3072,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "macroific" version = "1.3.1" @@ -3267,6 +3295,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.3" @@ -3277,6 +3311,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -4039,6 +4079,22 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a17e662a7a8291a865152364c20c7abc5e60486ab2001e8ec10b24862de0b9ab" +dependencies = [ + "crossbeam-utils", + "libc", + "mach2", + "once_cell", + "raw-cpuid", + "wasi 0.11.0+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quickcheck" version = "1.0.3" @@ -4196,6 +4252,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "10.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" +dependencies = [ + "bitflags 1.3.2", +] + [[package]] name = "rayon" version = "1.7.0" @@ -4230,7 +4295,7 @@ dependencies = [ [[package]] name = "realtime-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -4253,7 +4318,7 @@ dependencies = [ [[package]] name = "realtime-protocol" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "bincode", @@ -4841,7 +4906,7 @@ dependencies = [ [[package]] name = "shared-entity" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "app-error", @@ -6016,7 +6081,7 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc" [[package]] name = "websocket" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "futures-channel", "futures-util", @@ -6237,7 +6302,7 @@ dependencies = [ [[package]] name = "workspace-template" version = "0.1.0" -source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=d23ad1c4de34c8333521b364f2e1f69695d72bb5#d23ad1c4de34c8333521b364f2e1f69695d72bb5" +source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=29a0851f485957cc6410ccf9d261c781c1d2f757#29a0851f485957cc6410ccf9d261c781c1d2f757" dependencies = [ "anyhow", "async-trait", diff --git a/frontend/rust-lib/Cargo.toml b/frontend/rust-lib/Cargo.toml index 48ae3727f1271..827b2724ea07b 100644 --- a/frontend/rust-lib/Cargo.toml +++ b/frontend/rust-lib/Cargo.toml @@ -105,7 +105,7 @@ incremental = false # Run the script: # scripts/tool/update_client_api_rev.sh new_rev_id # ⚠️⚠️⚠️️ -client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23ad1c4de34c8333521b364f2e1f69695d72bb5" } +client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "29a0851f485957cc6410ccf9d261c781c1d2f757" } # Please use the following script to update collab. # Working directory: frontend # @@ -115,10 +115,10 @@ client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "d23 # To switch to the local path, run: # scripts/tool/update_collab_source.sh # ⚠️⚠️⚠️️ -collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } -collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2ce95b948accd6d14b97ee886f4416295acd9c65" } +collab = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-folder = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-document = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-database = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-plugins = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-user = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } +collab-entity = { git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "2de6d172f56fed29ee6f32b82040cca4867647ac" } diff --git a/frontend/rust-lib/flowy-core/src/integrate/server.rs b/frontend/rust-lib/flowy-core/src/integrate/server.rs index d65873c11fc2e..c9a1402c4d1c3 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/server.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/server.rs @@ -128,6 +128,7 @@ impl ServerProvider { config, *self.user_enable_sync.read(), self.config.device_id.clone(), + &self.config.app_version, )); Ok::, FlowyError>(server) diff --git a/frontend/rust-lib/flowy-core/src/integrate/user.rs b/frontend/rust-lib/flowy-core/src/integrate/user.rs index 1959c2dd6e425..e507221ef725f 100644 --- a/frontend/rust-lib/flowy-core/src/integrate/user.rs +++ b/frontend/rust-lib/flowy-core/src/integrate/user.rs @@ -7,7 +7,7 @@ use tracing::event; use collab_integrate::collab_builder::AppFlowyCollabBuilder; use flowy_database2::DatabaseManager; use flowy_document::manager::DocumentManager; -use flowy_error::FlowyResult; +use flowy_error::{FlowyError, FlowyResult}; use flowy_folder::manager::{FolderInitDataSource, FolderManager}; use flowy_user::event_map::UserStatusCallback; use flowy_user_pub::cloud::{UserCloudConfig, UserCloudServiceProvider}; @@ -177,8 +177,13 @@ impl UserStatusCallback for UserStatusCallbackImpl { } }, }, - Err(_) => FolderInitDataSource::LocalDisk { - create_if_not_exist: true, + Err(err) => match server_type { + Server::Local => FolderInitDataSource::LocalDisk { + create_if_not_exist: true, + }, + Server::AppFlowyCloud | Server::Supabase => { + return Err(FlowyError::from(err)); + }, }, }; diff --git a/frontend/rust-lib/flowy-core/src/lib.rs b/frontend/rust-lib/flowy-core/src/lib.rs index 1d9a34e28b2d6..041e407661ce2 100644 --- a/frontend/rust-lib/flowy-core/src/lib.rs +++ b/frontend/rust-lib/flowy-core/src/lib.rs @@ -185,7 +185,7 @@ impl AppFlowyCore { let cloned_user_manager = Arc::downgrade(&user_manager); if let Some(user_manager) = cloned_user_manager.upgrade() { if let Err(err) = user_manager - .init(user_status_callback, collab_interact_impl) + .init_with_callback(user_status_callback, collab_interact_impl) .await { error!("Init user failed: {}", err) diff --git a/frontend/rust-lib/flowy-error/src/code.rs b/frontend/rust-lib/flowy-error/src/code.rs index 1b02f368b436c..8cc78a4ca0634 100644 --- a/frontend/rust-lib/flowy-error/src/code.rs +++ b/frontend/rust-lib/flowy-error/src/code.rs @@ -24,23 +24,14 @@ pub enum ErrorCode { #[error("Workspace name can not be empty or whitespace")] WorkspaceNameInvalid = 5, - #[error("Workspace id can not be empty or whitespace")] - WorkspaceIdInvalid = 6, - - #[error("Color style of the App is invalid")] - AppColorStyleInvalid = 7, - #[error("Workspace desc is invalid")] WorkspaceDescTooLong = 8, #[error("Workspace description too long")] WorkspaceNameTooLong = 9, - #[error("App id can not be empty or whitespace")] - AppIdInvalid = 10, - - #[error("App name can not be empty or whitespace")] - AppNameInvalid = 11, + #[error("Can't load the workspace data")] + WorkspaceInitializeError = 6, #[error("View name can not be empty or whitespace")] ViewNameInvalid = 12, @@ -51,9 +42,6 @@ pub enum ErrorCode { #[error("View id can not be empty or whitespace")] ViewIdIsInvalid = 14, - #[error("View desc too long")] - ViewDescTooLong = 15, - #[error("View data is invalid")] ViewDataInvalid = 16, diff --git a/frontend/rust-lib/flowy-error/src/errors.rs b/frontend/rust-lib/flowy-error/src/errors.rs index e532714cf6832..96a735c2e4689 100644 --- a/frontend/rust-lib/flowy-error/src/errors.rs +++ b/frontend/rust-lib/flowy-error/src/errors.rs @@ -70,16 +70,10 @@ impl FlowyError { static_flowy_error!(internal, ErrorCode::Internal); static_flowy_error!(record_not_found, ErrorCode::RecordNotFound); - static_flowy_error!(workspace_name, ErrorCode::WorkspaceNameInvalid); - static_flowy_error!(workspace_id, ErrorCode::WorkspaceIdInvalid); - static_flowy_error!(color_style, ErrorCode::AppColorStyleInvalid); - static_flowy_error!(workspace_desc, ErrorCode::WorkspaceDescTooLong); - static_flowy_error!(app_name, ErrorCode::AppNameInvalid); - static_flowy_error!(invalid_app_id, ErrorCode::AppIdInvalid); + static_flowy_error!(workspace_initialize, ErrorCode::WorkspaceInitializeError); static_flowy_error!(view_name, ErrorCode::ViewNameInvalid); static_flowy_error!(view_thumbnail, ErrorCode::ViewThumbnailInvalid); static_flowy_error!(invalid_view_id, ErrorCode::ViewIdIsInvalid); - static_flowy_error!(view_desc, ErrorCode::ViewDescTooLong); static_flowy_error!(view_data, ErrorCode::ViewDataInvalid); static_flowy_error!(unauthorized, ErrorCode::UserUnauthorized); static_flowy_error!(email_empty, ErrorCode::EmailIsEmpty); diff --git a/frontend/rust-lib/flowy-folder/src/entities/parser/view/mod.rs b/frontend/rust-lib/flowy-folder/src/entities/parser/view/mod.rs index 399cc44c451a3..6d0faf330d044 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/parser/view/mod.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/parser/view/mod.rs @@ -1,9 +1,7 @@ -mod view_desc; mod view_id; mod view_name; mod view_thumbnail; -pub use view_desc::*; pub use view_id::*; pub use view_name::*; pub use view_thumbnail::*; diff --git a/frontend/rust-lib/flowy-folder/src/entities/parser/view/view_desc.rs b/frontend/rust-lib/flowy-folder/src/entities/parser/view/view_desc.rs deleted file mode 100644 index f88eb6ebff9b6..0000000000000 --- a/frontend/rust-lib/flowy-folder/src/entities/parser/view/view_desc.rs +++ /dev/null @@ -1,21 +0,0 @@ -use flowy_error::ErrorCode; -use unicode_segmentation::UnicodeSegmentation; - -#[derive(Debug)] -pub struct ViewDesc(pub String); - -impl ViewDesc { - pub fn parse(s: String) -> Result { - if s.graphemes(true).count() > 1000 { - return Err(ErrorCode::ViewDescTooLong); - } - - Ok(Self(s)) - } -} - -impl AsRef for ViewDesc { - fn as_ref(&self) -> &str { - &self.0 - } -} diff --git a/frontend/rust-lib/flowy-folder/src/entities/parser/workspace/workspace_id.rs b/frontend/rust-lib/flowy-folder/src/entities/parser/workspace/workspace_id.rs index b3ce24c6c944a..3bb30fa3dc7b2 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/parser/workspace/workspace_id.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/parser/workspace/workspace_id.rs @@ -6,7 +6,7 @@ pub struct WorkspaceIdentify(pub String); impl WorkspaceIdentify { pub fn parse(s: String) -> Result { if s.trim().is_empty() { - return Err(ErrorCode::WorkspaceIdInvalid); + return Err(ErrorCode::WorkspaceInitializeError); } Ok(Self(s)) diff --git a/frontend/rust-lib/flowy-folder/src/entities/view.rs b/frontend/rust-lib/flowy-folder/src/entities/view.rs index 88d8187d396a0..7b756163a655f 100644 --- a/frontend/rust-lib/flowy-folder/src/entities/view.rs +++ b/frontend/rust-lib/flowy-folder/src/entities/view.rs @@ -10,7 +10,7 @@ use flowy_error::ErrorCode; use flowy_folder_pub::cloud::gen_view_id; use crate::entities::icon::ViewIconPB; -use crate::entities::parser::view::{ViewDesc, ViewIdentify, ViewName, ViewThumbnail}; +use crate::entities::parser::view::{ViewIdentify, ViewName, ViewThumbnail}; #[derive(Eq, PartialEq, ProtoBuf, Debug, Default, Clone)] pub struct ChildViewUpdatePB { @@ -336,11 +336,6 @@ impl TryInto for UpdateViewPayloadPB { Some(name) => Some(ViewName::parse(name)?.0), }; - let desc = match self.desc { - None => None, - Some(desc) => Some(ViewDesc::parse(desc)?.0), - }; - let thumbnail = match self.thumbnail { None => None, Some(thumbnail) => Some(ViewThumbnail::parse(thumbnail)?.0), @@ -351,7 +346,7 @@ impl TryInto for UpdateViewPayloadPB { Ok(UpdateViewParams { view_id, name, - desc, + desc: self.desc, thumbnail, is_favorite, layout: self.layout.map(|ty| ty.into()), diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 2d42edc212c58..7eee016727973 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -329,3 +329,11 @@ pub(crate) async fn get_folder_snapshots_handler( let snapshots = folder.get_folder_snapshots(&data.value, 10).await?; data_result_ok(RepeatedFolderSnapshotPB { items: snapshots }) } + +pub(crate) async fn reload_workspace_handler( + folder: AFPluginState>, +) -> Result<(), FlowyError> { + let folder = upgrade_folder(folder)?; + folder.reload_workspace().await?; + Ok(()) +} diff --git a/frontend/rust-lib/flowy-folder/src/event_map.rs b/frontend/rust-lib/flowy-folder/src/event_map.rs index 2e6c8855d7d83..6a4d1faa20758 100644 --- a/frontend/rust-lib/flowy-folder/src/event_map.rs +++ b/frontend/rust-lib/flowy-folder/src/event_map.rs @@ -37,6 +37,7 @@ pub fn init(folder: Weak) -> AFPlugin { .event(FolderEvent::ReadRecentViews, read_recent_views_handler) .event(FolderEvent::ToggleFavorite, toggle_favorites_handler) .event(FolderEvent::UpdateRecentViews, update_recent_views_handler) + .event(FolderEvent::ReloadWorkspace, reload_workspace_handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)] @@ -152,4 +153,7 @@ pub enum FolderEvent { // used for add or remove recent views, like history #[event(input = "UpdateRecentViewPayloadPB")] UpdateRecentViews = 37, + + #[event()] + ReloadWorkspace = 38, } diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index 72fbd27154f3d..a2e8d951e64e2 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -72,15 +72,39 @@ impl FolderManager { Ok(manager) } + pub async fn reload_workspace(&self) -> FlowyResult<()> { + let workspace_id = self + .workspace_id + .read() + .as_ref() + .ok_or_else(|| { + FlowyError::internal().with_context("workspace id is empty when trying to reload workspace") + })? + .clone(); + + let uid = self.user.user_id()?; + let doc_state = self + .cloud_service + .get_folder_doc_state(&workspace_id, uid, CollabType::Folder, &workspace_id) + .await?; + + self + .initialize(uid, &workspace_id, FolderInitDataSource::Cloud(doc_state)) + .await?; + Ok(()) + } + #[instrument(level = "debug", skip(self), err)] pub async fn get_current_workspace(&self) -> FlowyResult { self.with_folder( || { let uid = self.user.user_id()?; - let workspace_id = self.workspace_id.read().as_ref().cloned().ok_or( - FlowyError::from(ErrorCode::WorkspaceIdInvalid) - .with_context("Unexpected empty workspace id"), - )?; + let workspace_id = self + .workspace_id + .read() + .as_ref() + .cloned() + .ok_or_else(|| FlowyError::from(ErrorCode::WorkspaceInitializeError))?; Err(workspace_data_not_sync_error(uid, &workspace_id)) }, |folder| { diff --git a/frontend/rust-lib/flowy-folder/src/manager_init.rs b/frontend/rust-lib/flowy-folder/src/manager_init.rs index b83ef2fc5a9a7..5d3475a10d0f9 100644 --- a/frontend/rust-lib/flowy-folder/src/manager_init.rs +++ b/frontend/rust-lib/flowy-folder/src/manager_init.rs @@ -1,10 +1,9 @@ -use std::sync::{Arc, Weak}; - +use collab_entity::CollabType; use collab_folder::{Folder, FolderNotify, UserId}; -use tracing::{event, Level}; - use collab_integrate::CollabKVDB; -use flowy_error::{ErrorCode, FlowyError, FlowyResult}; +use flowy_error::{FlowyError, FlowyResult}; +use std::sync::{Arc, Weak}; +use tracing::{event, Level}; use crate::manager::{FolderInitDataSource, FolderManager}; use crate::manager_observer::{ @@ -47,25 +46,34 @@ impl FolderManager { create_if_not_exist, } => { let is_exist = self.is_workspace_exist_in_local(uid, &workspace_id).await; + // 1. if the folder exists, open it from local disk if is_exist { self .open_local_folder(uid, &workspace_id, collab_db, 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 // Currently, this branch is only used when the server type is supabase. For appflowy cloud, // the default workspace is already created when the user sign up. self .create_default_folder(uid, &workspace_id, collab_db, folder_notifier) .await? } else { - return Err(FlowyError::new( - ErrorCode::RecordNotFound, - "Can't find any workspace data", - )); + // 3. If the folder doesn't exist and create_if_not_exist is false, try to fetch the folder data from cloud/ + // This will happen user can't fetch the folder data when the user sign in. + let doc_state = self + .cloud_service + .get_folder_doc_state(&workspace_id, uid, CollabType::Folder, &workspace_id) + .await?; + + let collab = self + .collab_for_folder(uid, &workspace_id, collab_db.clone(), doc_state) + .await?; + Folder::open(UserId::from(uid), collab, Some(folder_notifier.clone()))? } }, - FolderInitDataSource::Cloud(raw_data) => { - if raw_data.is_empty() { + FolderInitDataSource::Cloud(doc_state) => { + if doc_state.is_empty() { event!(Level::ERROR, "remote folder data is empty, open from local"); self .open_local_folder(uid, &workspace_id, collab_db, folder_notifier) @@ -73,7 +81,7 @@ impl FolderManager { } else { event!(Level::INFO, "Restore folder with remote data"); let collab = self - .collab_for_folder(uid, &workspace_id, collab_db.clone(), raw_data) + .collab_for_folder(uid, &workspace_id, collab_db.clone(), doc_state) .await?; Folder::open(UserId::from(uid), collab, Some(folder_notifier.clone()))? } 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 57de02de3174a..73717798f02ba 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 @@ -331,7 +331,9 @@ async fn get_admin_client(client: &Arc) -> FlowyResult { client.base_url(), client.ws_addr(), client.gotrue_url(), + &client.device_id, ClientConfiguration::default(), + &client.client_id, ); admin_client .sign_in_password(&admin_email, &admin_password) 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 9a9a8cd57b4a9..f82297356c549 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -12,7 +12,8 @@ use client_api::{Client, ClientConfiguration}; use flowy_storage::ObjectStorageService; use tokio::sync::watch; use tokio_stream::wrappers::WatchStream; -use tracing::{error, event, info}; +use tracing::{error, event, info, warn}; +use uuid::Uuid; use flowy_database_pub::cloud::DatabaseCloudService; use flowy_document_pub::cloud::DocumentCloudService; @@ -38,20 +39,32 @@ pub struct AppFlowyCloudServer { pub(crate) client: Arc, enable_sync: Arc, network_reachable: Arc, - #[allow(dead_code)] - device_id: String, + pub device_id: String, ws_client: Arc, } impl AppFlowyCloudServer { - pub fn new(config: AFCloudConfiguration, enable_sync: bool, device_id: String) -> Self { + pub fn new( + config: AFCloudConfiguration, + enable_sync: bool, + mut device_id: String, + app_version: &str, + ) -> Self { + // The device id can't be empty, so we generate a new one if it is. + if device_id.is_empty() { + warn!("Device ID is empty, generating a new one"); + device_id = Uuid::new_v4().to_string(); + } + let api_client = AFCloudClient::new( &config.base_url, &config.ws_base_url, &config.gotrue_url, + &device_id, ClientConfiguration::default() .with_compression_buffer_size(10240) .with_compression_quality(8), + app_version, ); let token_state_rx = api_client.subscribe_token_state(); let enable_sync = Arc::new(AtomicBool::new(enable_sync)); diff --git a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs index 96957f1656b79..8377eb3dd943c 100644 --- a/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs +++ b/frontend/rust-lib/flowy-server/tests/af_cloud_test/util.rs @@ -26,7 +26,12 @@ pub fn get_af_cloud_config() -> Option { pub fn af_cloud_server(config: AFCloudConfiguration) -> Arc { let fake_device_id = uuid::Uuid::new_v4().to_string(); - Arc::new(AppFlowyCloudServer::new(config, true, fake_device_id)) + Arc::new(AppFlowyCloudServer::new( + config, + true, + fake_device_id, + "flowy-server-test", + )) } pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguration) -> String { @@ -34,7 +39,9 @@ pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguratio &config.base_url, &config.ws_base_url, &config.gotrue_url, + "fake_device_id", ClientConfiguration::default(), + "test", ); let admin_email = std::env::var("GOTRUE_ADMIN_EMAIL").unwrap(); let admin_password = std::env::var("GOTRUE_ADMIN_PASSWORD").unwrap(); @@ -42,7 +49,9 @@ pub async fn generate_sign_in_url(user_email: &str, config: &AFCloudConfiguratio client.base_url(), client.ws_addr(), client.gotrue_url(), + "fake_device_id", ClientConfiguration::default(), + &client.client_id, ); admin_client .sign_in_password(&admin_email, &admin_password) diff --git a/frontend/rust-lib/flowy-user-pub/src/cloud.rs b/frontend/rust-lib/flowy-user-pub/src/cloud.rs index 41d71f1efb98f..27cc2233f09af 100644 --- a/frontend/rust-lib/flowy-user-pub/src/cloud.rs +++ b/frontend/rust-lib/flowy-user-pub/src/cloud.rs @@ -1,21 +1,18 @@ -use std::collections::HashMap; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; -use std::sync::Arc; - use anyhow::Error; use collab::core::collab::CollabDocState; use collab_entity::{CollabObject, CollabType}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio_stream::wrappers::WatchStream; -use uuid::Uuid; - use flowy_error::{ErrorCode, FlowyError}; - use lib_infra::box_any::BoxAny; use lib_infra::conditional_send_sync_trait; use lib_infra::future::FutureResult; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::fmt::{Display, Formatter}; +use std::str::FromStr; +use std::sync::Arc; +use tokio_stream::wrappers::WatchStream; +use uuid::Uuid; use crate::entities::{ AuthResponse, Authenticator, Role, UpdateUserProfileParams, UserCredentials, UserProfile, diff --git a/frontend/rust-lib/flowy-user/src/event_handler.rs b/frontend/rust-lib/flowy-user/src/event_handler.rs index c86d810a0a159..40cba9c28216d 100644 --- a/frontend/rust-lib/flowy-user/src/event_handler.rs +++ b/frontend/rust-lib/flowy-user/src/event_handler.rs @@ -122,8 +122,17 @@ pub async fn get_user_profile_handler( #[tracing::instrument(level = "debug", skip(manager))] pub async fn sign_out_handler(manager: AFPluginState>) -> Result<(), FlowyError> { - let manager = upgrade_manager(manager)?; - manager.sign_out().await?; + let (tx, rx) = tokio::sync::oneshot::channel(); + tokio::spawn(async move { + let result = async { + let manager = upgrade_manager(manager)?; + manager.sign_out().await?; + Ok::<(), FlowyError>(()) + } + .await; + let _ = tx.send(result); + }); + rx.await??; Ok(()) } @@ -565,7 +574,7 @@ pub async fn reset_workspace_handler( let reset_pb = data.into_inner(); if reset_pb.workspace_id.is_empty() { return Err(FlowyError::new( - ErrorCode::WorkspaceIdInvalid, + ErrorCode::WorkspaceInitializeError, "The workspace id is empty", )); } 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 d22b6ab04b65a..a046237943bd5 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -118,7 +118,7 @@ impl UserManager { /// the function will set up the collaboration configuration and initialize the user's awareness. Upon successful /// completion, a user status callback is invoked to signify that the initialization process is complete. #[instrument(level = "debug", skip_all, err)] - pub async fn init( + pub async fn init_with_callback( &self, user_status_callback: C, collab_interact: I, @@ -159,27 +159,43 @@ impl UserManager { } // Subscribe the token state + let weak_cloud_services = Arc::downgrade(&self.cloud_services); + let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); + let cloned_session = session.clone(); if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { event!(tracing::Level::DEBUG, "Listen token state change"); let user_uid = user.uid; - let user_token = user.token.clone(); + let local_token = user.token.clone(); af_spawn(async move { while let Some(token_state) = token_state_rx.next().await { debug!("Token state changed: {:?}", token_state); match token_state { - UserTokenState::Refresh { token } => { + UserTokenState::Refresh { token: new_token } => { // Only save the token if the token is different from the current token - if token != user_token { + if new_token != local_token { if let Some(conn) = weak_pool.upgrade().and_then(|pool| pool.get().ok()) { // Save the new token - if let Err(err) = save_user_token(user_uid, conn, token) { + if let Err(err) = save_user_token(user_uid, conn, new_token) { error!("Save user token failed: {}", err); } } } }, - UserTokenState::Invalid => {}, + UserTokenState::Invalid => { + // Force user to sign out when the token is invalid + if let (Some(cloud_services), Some(authenticate_user), Some(conn)) = ( + weak_cloud_services.upgrade(), + weak_authenticate_user.upgrade(), + weak_pool.upgrade().and_then(|pool| pool.get().ok()), + ) { + if let Err(err) = + sign_out(&cloud_services, &cloned_session, &authenticate_user, conn).await + { + error!("Sign out when token invalid failed: {:?}", err); + } + } + }, } } }); @@ -203,7 +219,12 @@ impl UserManager { } self.authenticate_user.vacuum_database_if_need(); let cloud_config = get_cloud_config(session.user_id, &self.store_preferences); - if let Err(e) = user_status_callback + // Init the user awareness + self + .initialize_user_awareness(&session, UserAwarenessDataSource::Local) + .await; + + user_status_callback .did_init( user.uid, &user.authenticator, @@ -211,14 +232,7 @@ impl UserManager { &session.user_workspace, &self.authenticate_user.user_config.device_id, ) - .await - { - error!("Failed to call did_init callback: {:?}", e); - } - // Init the user awareness - self - .initialize_user_awareness(&session, UserAwarenessDataSource::Local) - .await; + .await?; } Ok(()) } @@ -282,7 +296,7 @@ impl UserManager { .initialize_user_awareness(&session, UserAwarenessDataSource::Remote) .await; - if let Err(e) = self + self .user_status_callback .read() .await @@ -291,10 +305,7 @@ impl UserManager { &latest_workspace, &self.authenticate_user.user_config.device_id, ) - .await - { - error!("Failed to call did_sign_in callback: {:?}", e); - } + .await?; send_auth_state_notification(AuthStateChangedPB { state: AuthStatePB::AuthStateSignIn, message: "Sign in success".to_string(), @@ -423,14 +434,13 @@ impl UserManager { #[tracing::instrument(level = "info", skip(self))] pub async fn sign_out(&self) -> Result<(), FlowyError> { if let Ok(session) = self.get_session() { - let _ = remove_user_token(session.user_id, self.db_connection(session.user_id)?); - self.authenticate_user.database.close(session.user_id)?; - self.authenticate_user.set_session(None)?; - - let server = self.cloud_services.get_user_service()?; - if let Err(err) = server.sign_out(None).await { - event!(tracing::Level::ERROR, "{:?}", err); - } + sign_out( + &self.cloud_services, + &session, + &self.authenticate_user, + self.db_connection(session.user_id)?, + ) + .await?; } Ok(()) } @@ -821,3 +831,21 @@ pub(crate) fn run_collab_data_migration( Err(e) => error!("User data migration failed: {:?}", e), } } + +pub async fn sign_out( + cloud_services: &Arc, + session: &Session, + authenticate_user: &AuthenticateUser, + conn: DBConnection, +) -> Result<(), FlowyError> { + let _ = remove_user_token(session.user_id, conn); + authenticate_user.database.close(session.user_id)?; + authenticate_user.set_session(None)?; + + let server = cloud_services.get_user_service()?; + if let Err(err) = server.sign_out(None).await { + event!(tracing::Level::ERROR, "{:?}", err); + } + + Ok(()) +} From 45054e76398d2808862f6b73a70319c4a0ee65c6 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Fri, 9 Feb 2024 00:18:51 +0800 Subject: [PATCH 26/50] chore: bump version 047 (#4635) --- CHANGELOG.md | 4 ++++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a742c869221..889c811c4c1c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Release Notes +## Version 0.4.7 - 02/08/2024 +### Bug Fixes +- Fixed a possible error when loading workspaces + ## Version 0.4.6 - 02/03/2024 ### Bug Fixes - Fixed refresh token bug diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index a5bac2f43b002..1b338f95b98f6 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.4.6" +APPFLOWY_VERSION = "0.4.7" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 567fa46e6181d..5a38b20f22dac 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ 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.4.6 +version: 0.4.7 environment: flutter: ">=3.18.0-0.2.pre" From f29f1bae32f81182e4410618822f706d97c86600 Mon Sep 17 00:00:00 2001 From: nathan Date: Fri, 9 Feb 2024 10:18:52 +0800 Subject: [PATCH 27/50] chore: update error message --- frontend/rust-lib/flowy-folder/src/event_handler.rs | 1 + frontend/rust-lib/flowy-folder/src/manager.rs | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/frontend/rust-lib/flowy-folder/src/event_handler.rs b/frontend/rust-lib/flowy-folder/src/event_handler.rs index 7eee016727973..a5412decc6abd 100644 --- a/frontend/rust-lib/flowy-folder/src/event_handler.rs +++ b/frontend/rust-lib/flowy-folder/src/event_handler.rs @@ -102,6 +102,7 @@ pub(crate) async fn create_orphan_view_handler( data_result_ok(view_pb_without_child_views(Arc::new(view))) } +#[tracing::instrument(level = "debug", skip(data, folder), err)] pub(crate) async fn read_view_handler( data: AFPluginData, folder: AFPluginState>, diff --git a/frontend/rust-lib/flowy-folder/src/manager.rs b/frontend/rust-lib/flowy-folder/src/manager.rs index a2e8d951e64e2..cf782c7586990 100644 --- a/frontend/rust-lib/flowy-folder/src/manager.rs +++ b/frontend/rust-lib/flowy-folder/src/manager.rs @@ -455,7 +455,7 @@ impl FolderManager { /// Returns the view with the given view id. /// The child views of the view will only access the first. So if you want to get the child view's /// child view, you need to call this method again. - #[tracing::instrument(level = "debug", skip(self, view_id), err)] + #[tracing::instrument(level = "debug", skip(self))] pub async fn get_view_pb(&self, view_id: &str) -> FlowyResult { let view_id = view_id.to_string(); let folder = self.mutex_folder.lock(); @@ -467,11 +467,17 @@ impl FolderManager { .collect::>(); if trash_ids.contains(&view_id) { - return Err(FlowyError::record_not_found()); + return Err(FlowyError::new( + ErrorCode::RecordNotFound, + format!("View:{} is in trash", view_id), + )); } match folder.views.get_view(&view_id) { - None => Err(FlowyError::record_not_found()), + None => { + error!("Can't find the view with id: {}", view_id); + Err(FlowyError::record_not_found()) + }, Some(view) => { let child_views = folder .views From 1894409dc3c215bd1962b6d44d3e955a127fd20d Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Fri, 9 Feb 2024 14:40:06 +0100 Subject: [PATCH 28/50] ci: resolve mobile build failing due to missing space (#4640) * chore: remove additionally unused tools * chore: test * chore: bunch cleanup commands --- .github/workflows/mobile_ci.yaml | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml index b6892f9c83d5b..0e64811c3e258 100644 --- a/.github/workflows/mobile_ci.yaml +++ b/.github/workflows/mobile_ci.yaml @@ -19,7 +19,6 @@ on: env: FLUTTER_VERSION: "3.18.0-0.2.pre" - RUST_TOOLCHAIN: "1.75" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -46,19 +45,14 @@ jobs: 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: Checkout source code uses: actions/checkout@v2 - - name: Install Rust toolchain - id: rust_toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ env.RUST_TOOLCHAIN }} - target: ${{ matrix.target }} - override: true - profile: minimal - - name: Install flutter id: flutter uses: subosito/flutter-action@v2 @@ -70,8 +64,8 @@ jobs: - uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: "r24" - add-to-path: true + ndk-version: "r24" + add-to-path: true - uses: gradle/gradle-build-action@v2 with: From b356927e60ca1bfb049de20f1058e7eb00c1a26c Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Fri, 9 Feb 2024 15:54:05 +0100 Subject: [PATCH 29/50] feat: improve more action in document (#4639) --- .../presentation/more/font_size_slider.dart | 51 ++++++++++ .../presentation/more/font_size_switcher.dart | 92 ------------------- .../presentation/more/more_button.dart | 61 +++++++----- frontend/resources/translations/en.json | 5 +- 4 files changed, 91 insertions(+), 118 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_slider.dart delete mode 100644 frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_slider.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_slider.dart new file mode 100644 index 0000000000000..513833b82bf39 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_slider.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; +import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flowy_infra_ui/widget/spacing.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class FontSizeStepper extends StatelessWidget { + const FontSizeStepper({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + children: [ + const FlowyText('A', fontSize: 14), + const HSpace(6), + Expanded( + child: SliderTheme( + data: Theme.of(context).sliderTheme.copyWith( + showValueIndicator: ShowValueIndicator.never, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 16, + ), + ), + child: Slider( + value: state.fontSize, + min: 10, + max: 24, + divisions: 8, + onChanged: (fontSize) => context + .read() + .syncFontSize(fontSize), + ), + ), + ), + const HSpace(6), + const FlowyText('A', fontSize: 20), + ], + ), + ); + }, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart deleted file mode 100644 index 5242db91e01e9..0000000000000 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/font_size_switcher.dart +++ /dev/null @@ -1,92 +0,0 @@ -import 'package:appflowy/generated/locale_keys.g.dart'; -import 'package:appflowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart'; -import 'package:collection/collection.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flowy_infra/theme_extension.dart'; -import 'package:flowy_infra_ui/style_widget/text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -typedef _FontSizeSelection = (String, double, bool); - -class FontSizeSwitcher extends StatefulWidget { - const FontSizeSwitcher({ - super.key, - }); - - @override - State createState() => _FontSizeSwitcherState(); -} - -class _FontSizeSwitcherState extends State { - final List<_FontSizeSelection> _fontSizes = [ - (LocaleKeys.moreAction_small.tr(), 14.0, false), - (LocaleKeys.moreAction_medium.tr(), 18.0, true), - (LocaleKeys.moreAction_large.tr(), 22.0, false), - ]; - - _FontSizeSelection? _selection; - - @override - Widget build(BuildContext context) { - final selectedBgColor = AFThemeExtension.of(context).toggleButtonBGColor; - final foregroundColor = Theme.of(context).colorScheme.onBackground; - return BlocBuilder( - builder: (context, state) { - _selection = _fontSizes.firstWhereOrNull( - (element) => element.$2 == state.fontSize, - ); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - FlowyText.semibold( - LocaleKeys.moreAction_fontSize.tr(), - fontSize: 12, - color: Theme.of(context).colorScheme.tertiary, - ), - const SizedBox( - height: 5, - ), - SegmentedButton<_FontSizeSelection>( - showSelectedIcon: false, - style: TextButton.styleFrom( - foregroundColor: foregroundColor, - shadowColor: selectedBgColor.withOpacity(0.3), - padding: const EdgeInsets.all(16), - side: BorderSide( - width: 0.5, - color: foregroundColor, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), - ), - ), - segments: _fontSizes - .map( - (e) => ButtonSegment( - value: e, - label: FlowyText( - e.$1, - fontSize: e.$2, - ), - ), - ) - .toList(), - selected: { - _selection ?? _fontSizes.first, - }, - onSelectionChanged: (Set<(String, double, bool)> newSelection) { - _selection = newSelection.firstOrNull; - _updateSelectedFontSize(newSelection.first.$2); - }, - ), - ], - ); - }, - ); - } - - void _updateSelectedFontSize(double fontSize) { - context.read().syncFontSize(fontSize); - } -} diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart index 016129e615afc..338e1bf429ef9 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/more/more_button.dart @@ -1,24 +1,55 @@ +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/presentation/more/cubit/document_appearance_cubit.dart'; -import 'package:appflowy/plugins/document/presentation/more/font_size_switcher.dart'; +import 'package:appflowy/plugins/document/presentation/more/font_size_slider.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:flowy_infra_ui/widget/flowy_tooltip.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; class DocumentMoreButton extends StatelessWidget { - const DocumentMoreButton({ - super.key, - }); + const DocumentMoreButton({super.key}); @override Widget build(BuildContext context) { return AppFlowyPopover( - constraints: const BoxConstraints(maxWidth: 400, maxHeight: 320), + constraints: BoxConstraints.loose(const Size(200, 400)), offset: const Offset(0, 30), + popupBuilder: (_) { + final actions = [ + AppFlowyPopover( + direction: PopoverDirection.leftWithCenterAligned, + constraints: const BoxConstraints(maxHeight: 40, maxWidth: 240), + offset: const Offset(-10, 0), + popupBuilder: (context) => const FontSizeStepper(), + child: FlowyButton( + text: FlowyText.regular( + LocaleKeys.moreAction_fontSize.tr(), + color: AFThemeExtension.of(context).textColor, + ), + leftIcon: Icon( + Icons.format_size_sharp, + color: Theme.of(context).iconTheme.color, + size: 18, + ), + leftIconSize: const Size(18, 18), + hoverColor: AFThemeExtension.of(context).lightGreyHover, + ), + ), + ]; + + return ListView.separated( + shrinkWrap: true, + padding: EdgeInsets.zero, + itemCount: actions.length, + separatorBuilder: (_, __) => const VSpace(4), + physics: StyledScrollPhysics(), + itemBuilder: (_, index) => actions[index], + ); + }, child: FlowyTooltip( message: LocaleKeys.moreAction_moreOptions.tr(), child: FlowyHover( @@ -32,20 +63,6 @@ class DocumentMoreButton extends StatelessWidget { ), ), ), - popupBuilder: (context) { - return Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - BlocProvider.value( - value: context.read(), - child: const FontSizeSwitcher(), - ), - ], - ), - ); - }, ); } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 93842e59c2534..83afac352b886 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -73,10 +73,7 @@ "copyLink": "Copy Link" }, "moreAction": { - "small": "small", - "medium": "medium", - "large": "large", - "fontSize": "Font Size", + "fontSize": "Font size", "import": "Import", "moreOptions": "More options" }, From e81a2ff5776612dd16fc5f6a4eb800a5f06687de Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Wed, 14 Feb 2024 09:38:05 +0800 Subject: [PATCH 30/50] fix: token refresh on local (#4650) * fix: refresh user token on local * chore: add test --- CHANGELOG.md | 2 +- frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- .../local_test/import_af_data_local_test.rs | 50 ++++++++ .../tests/user/local_test/mod.rs | 1 + .../flowy-user/src/user_manager/manager.rs | 112 +++++++++++------- 6 files changed, 126 insertions(+), 43 deletions(-) create mode 100644 frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 889c811c4c1c6..57ad262e1ccf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Release Notes -## Version 0.4.7 - 02/08/2024 +## Version 0.4.8 - 02/13/2024 ### Bug Fixes - Fixed a possible error when loading workspaces diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 1b338f95b98f6..7f05064bedc72 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.4.7" +APPFLOWY_VERSION = "0.4.8" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 5a38b20f22dac..85edb513ff683 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ 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.4.7 +version: 0.4.8 environment: flutter: ">=3.18.0-0.2.pre" diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs b/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs new file mode 100644 index 0000000000000..0c801c77be496 --- /dev/null +++ b/frontend/rust-lib/event-integration/tests/user/local_test/import_af_data_local_test.rs @@ -0,0 +1,50 @@ +use crate::util::unzip_history_user_db; +use event_integration::user_event::user_localhost_af_cloud; +use event_integration::EventIntegrationTest; +use flowy_core::DEFAULT_NAME; +use std::time::Duration; + +#[tokio::test] +async fn import_appflowy_data_folder_into_new_view_test() { + let import_container_name = "040_local".to_string(); + let (cleaner, user_db_path) = + unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + let (imported_af_folder_cleaner, imported_af_data_path) = + unzip_history_user_db("./tests/asset", &import_container_name).unwrap(); + + user_localhost_af_cloud().await; + let test = + EventIntegrationTest::new_with_user_data_path(user_db_path.clone(), DEFAULT_NAME.to_string()) + .await; + // In the 040_local, the structure is: + // workspace: + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 + // Sleep for 2 seconds to wait for the initial workspace to be created + tokio::time::sleep(Duration::from_secs(5)).await; + + test + .import_appflowy_data( + imported_af_data_path.to_str().unwrap().to_string(), + Some(import_container_name.clone()), + ) + .await + .unwrap(); + + // after import, the structure is: + // workspace: + // view: Getting Started + // view: 040_local + // view: Document1 + // view: Document2 + // view: Grid1 + // view: Grid2 + let views = test.get_all_workspace_views().await; + assert_eq!(views.len(), 2); + assert_eq!(views[1].name, import_container_name); + + drop(cleaner); + drop(imported_af_folder_cleaner); +} diff --git a/frontend/rust-lib/event-integration/tests/user/local_test/mod.rs b/frontend/rust-lib/event-integration/tests/user/local_test/mod.rs index 78d6ca1eca34a..0f1e2e47d5716 100644 --- a/frontend/rust-lib/event-integration/tests/user/local_test/mod.rs +++ b/frontend/rust-lib/event-integration/tests/user/local_test/mod.rs @@ -1,4 +1,5 @@ mod auth_test; mod helper; +mod import_af_data_local_test; mod user_awareness_test; mod user_profile_test; 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 a046237943bd5..1e44a19b0b998 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -17,7 +17,7 @@ use std::sync::atomic::{AtomicI64, Ordering}; use std::sync::{Arc, Weak}; use tokio::sync::{Mutex, RwLock}; use tokio_stream::StreamExt; -use tracing::{debug, error, event, info, instrument}; +use tracing::{debug, error, event, info, instrument, warn}; use lib_dispatch::prelude::af_spawn; use lib_infra::box_any::BoxAny; @@ -152,56 +152,88 @@ impl UserManager { user.email ); + self.prepare_user(&session).await; + self.prepare_backup(&session).await; + // Set the token if the current cloud service using token to authenticate // Currently, only the AppFlowy cloud using token to init the client api. - if let Err(err) = self.cloud_services.set_token(&user.token) { - error!("Set token failed: {}", err); - } + // TODO(nathan): using trait to separate the init process for different cloud service + if user.authenticator.is_appflowy_cloud() { + if let Err(err) = self.cloud_services.set_token(&user.token) { + error!("Set token failed: {}", err); + } - // Subscribe the token state - let weak_cloud_services = Arc::downgrade(&self.cloud_services); - let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); - let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); - let cloned_session = session.clone(); - if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { - event!(tracing::Level::DEBUG, "Listen token state change"); - let user_uid = user.uid; - let local_token = user.token.clone(); - af_spawn(async move { - while let Some(token_state) = token_state_rx.next().await { - debug!("Token state changed: {:?}", token_state); - match token_state { - UserTokenState::Refresh { token: new_token } => { - // Only save the token if the token is different from the current token - if new_token != local_token { - if let Some(conn) = weak_pool.upgrade().and_then(|pool| pool.get().ok()) { - // Save the new token - if let Err(err) = save_user_token(user_uid, conn, new_token) { - error!("Save user token failed: {}", err); + // Subscribe the token state + let weak_cloud_services = Arc::downgrade(&self.cloud_services); + let weak_authenticate_user = Arc::downgrade(&self.authenticate_user); + let weak_pool = Arc::downgrade(&self.db_pool(user.uid)?); + let cloned_session = session.clone(); + if let Some(mut token_state_rx) = self.cloud_services.subscribe_token_state() { + event!(tracing::Level::DEBUG, "Listen token state change"); + let user_uid = user.uid; + let local_token = user.token.clone(); + af_spawn(async move { + while let Some(token_state) = token_state_rx.next().await { + debug!("Token state changed: {:?}", token_state); + match token_state { + UserTokenState::Refresh { token: new_token } => { + // Only save the token if the token is different from the current token + if new_token != local_token { + if let Some(conn) = weak_pool.upgrade().and_then(|pool| pool.get().ok()) { + // Save the new token + if let Err(err) = save_user_token(user_uid, conn, new_token) { + error!("Save user token failed: {}", err); + } } } - } - }, - UserTokenState::Invalid => { - // Force user to sign out when the token is invalid - if let (Some(cloud_services), Some(authenticate_user), Some(conn)) = ( - weak_cloud_services.upgrade(), - weak_authenticate_user.upgrade(), - weak_pool.upgrade().and_then(|pool| pool.get().ok()), - ) { + }, + UserTokenState::Invalid => { + // Attempt to upgrade the weak reference for cloud_services + let cloud_services = match weak_cloud_services.upgrade() { + Some(cloud_services) => cloud_services, + None => { + error!("Failed to upgrade weak reference for cloud_services"); + return; // Exit early if the upgrade fails + }, + }; + + // Attempt to upgrade the weak reference for authenticate_user + let authenticate_user = match weak_authenticate_user.upgrade() { + Some(authenticate_user) => authenticate_user, + None => { + warn!("Failed to upgrade weak reference for authenticate_user"); + return; // Exit early if the upgrade fails + }, + }; + + // Attempt to upgrade the weak reference for pool and then get a connection + let conn = match weak_pool.upgrade() { + Some(pool) => match pool.get() { + Ok(conn) => conn, + Err(_) => { + warn!("Failed to get connection from pool"); + return; // Exit early if getting connection fails + }, + }, + None => { + warn!("Failed to upgrade weak reference for pool"); + return; // Exit early if the upgrade fails + }, + }; + + // If all upgrades succeed, proceed with the sign_out operation if let Err(err) = sign_out(&cloud_services, &cloned_session, &authenticate_user, conn).await { error!("Sign out when token invalid failed: {:?}", err); } - } - }, + // Force user to sign out when the token is invalid + }, + } } - } - }); + }); + } } - self.prepare_user(&session).await; - self.prepare_backup(&session).await; // Do the user data migration if needed event!(tracing::Level::INFO, "Prepare user data migration"); @@ -270,7 +302,7 @@ impl UserManager { /// /// A sign-in notification is also sent after a successful sign-in. /// - #[tracing::instrument(level = "debug", skip(self, params))] + #[tracing::instrument(level = "info", skip(self, params))] pub async fn sign_in( &self, params: SignInParams, From 3e0a003872b07621cb731a47205e837c9764fcba Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Fri, 16 Feb 2024 13:39:49 +0800 Subject: [PATCH 31/50] fix: deleting a sort doesn't allow creating a new sort on that field (#4660) --- .../application/field/field_controller.dart | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) 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 60d1037b100ba..832c21440fde0 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 @@ -1,3 +1,5 @@ +import 'dart:collection'; + import 'package:appflowy/plugins/database/application/database_view_service.dart'; import 'package:appflowy/plugins/database/application/field_settings/field_settings_listener.dart'; import 'package:appflowy/plugins/database/application/field_settings/field_settings_service.dart'; @@ -333,6 +335,32 @@ class FieldController { } } + void updateFieldInfos( + List newSortInfos, + SortChangesetNotificationPB changeset, + ) { + final changedFieldIds = HashSet.from([ + ...changeset.insertSorts.map((sort) => sort.sort.fieldId), + ...changeset.updateSorts.map((sort) => sort.fieldId), + ...changeset.deleteSorts.map((sort) => sort.fieldId), + ]); + + final newFieldInfos = [...fieldInfos]; + + for (final fieldId in changedFieldIds) { + final index = + newFieldInfos.indexWhere((fieldInfo) => fieldInfo.id == fieldId); + if (index == -1) { + continue; + } + newFieldInfos[index] = newFieldInfos[index].copyWith( + hasSort: newSortInfos.any((sort) => sort.fieldId == fieldId), + ); + } + + _fieldNotifier.fieldInfos = newFieldInfos; + } + _sortsListener.start( onSortChanged: (result) { if (_isDisposed) { @@ -346,6 +374,7 @@ class FieldController { updateSortFromChangeset(newSortInfos, changeset); _sortNotifier?.sorts = newSortInfos; + updateFieldInfos(newSortInfos, changeset); }, (err) => Log.error(err), ); From d690ca2751278e6a2ac499446c9e02059f497dff Mon Sep 17 00:00:00 2001 From: Danilo Barion Nogueira Date: Fri, 16 Feb 2024 14:44:05 -0300 Subject: [PATCH 32/50] chore: update translations (#4270) Co-authored-by: nathan --- frontend/resources/translations/pt-BR.json | 10 +++++----- project.inlang/project_id | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 5ba443feca4c8..7f2176ee8ac4d 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -152,7 +152,7 @@ "defaultNewPageName": "Sem título", "renameDialog": "Renomear" }, - "noPagesInside": "Nenhuma página dentro", + "noPagesInside": "Sem páginas internas", "toolbar": { "undo": "Desfazer", "redo": "Refazer", @@ -192,7 +192,7 @@ "clickToHidePersonal": "Clique para ocultar a seção pessoal", "clickToHideFavorites": "Clique para ocultar a seção favorita", "addAPage": "Adicionar uma página", - "recent": "Recente" + "recent": "Recentes" }, "notifications": { "export": { @@ -221,7 +221,7 @@ "discard": "Descartar", "replace": "substituir", "insertBelow": "Inserir Abaixo", - "insertAbove": "Inserir acima", + "insertAbove": "Insira acima", "upload": "Carregar", "edit": "Editar", "delete": "Excluir", @@ -298,7 +298,7 @@ "enableEncryptPrompt": "Ative a criptografia para proteger seus dados com este segredo. Armazene-o com segurança; uma vez ativado, não pode ser desativado. Se perdidos, seus dados se tornarão irrecuperáveis. Clique para copiar", "inputEncryptPrompt": "Por favor, insira seu segredo de criptografia para", "clickToCopySecret": "Clique para copiar o segredo", - "configServerSetting": "Defina as configurações do seu servidor", + "configServerSetting": "Configure seu servidor", "configServerGuide": "Após selecionar `Quick Start`, navegue até `Settings` e depois \"Cloud Settings\" para configurar seu servidor auto-hospedado.", "inputTextFieldHint": "Seu segredo", "historicalUserList": "Histórico de login do usuário", @@ -315,7 +315,7 @@ "notifications": { "enableNotifications": { "label": "Habilitar notificações", - "hint": "Desligue para impedir que notificações locais apareçam." + "hint": "Desligue para impedir notificações locais de aparecer" } }, "appearance": { diff --git a/project.inlang/project_id b/project.inlang/project_id index 64ebffbe6b9a3..2f6b9c75b450b 100644 --- a/project.inlang/project_id +++ b/project.inlang/project_id @@ -1 +1 @@ -1d68c3f4acb5d44d0d7dcd44277e656bab7f3d5fd2fd386d8d1095a5c98ee886 \ No newline at end of file +1d68c3f4acb5d44d0d7dcd44277e656bab7f3d5fd2fd386d8d1095a5c98ee886 From e6aa57275a5d48661a6f381dc472f5aef77cac83 Mon Sep 17 00:00:00 2001 From: Hassam Sulehria <35934427+H12324@users.noreply.github.com> Date: Fri, 16 Feb 2024 12:47:23 -0500 Subject: [PATCH 33/50] feat: open emoji selection menu with keyboard short (#4133) * feat: Implement emoji shortcut event (#2038) * test: emoji shortcut integration test (#2038) * test: Fixed testcase for emoji_shortcut_test (#2038) * fix: remove _showEmojiPickerMenu (#4133) * fix: change local variables with underscore --------- Co-authored-by: jazima --- .../integration_test/emoji_shortcut_test.dart | 41 +++++++++ .../integration_test/runner.dart | 2 + .../document/presentation/editor_page.dart | 2 + .../widgets/emoji_picker/emoji_menu_item.dart | 11 ++- .../widgets/emoji_picker/emoji_picker.dart | 1 + .../emoji_picker/emoji_shortcut_event.dart | 86 +++++++++++++++++++ 6 files changed, 137 insertions(+), 6 deletions(-) create mode 100644 frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart create mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart diff --git a/frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart b/frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart new file mode 100644 index 0000000000000..4bc15dd2140ed --- /dev/null +++ b/frontend/appflowy_flutter/integration_test/emoji_shortcut_test.dart @@ -0,0 +1,41 @@ +import 'dart:io'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:appflowy_editor/src/editor/editor_component/service/editor.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'util/keyboard.dart'; +import 'util/util.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + // May be better to move this to an existing test but unsure what it fits with + group('Keyboard shortcuts related to emojis', () { + testWidgets('cmd/ctrl+alt+e shortcut opens the emoji picker', + (tester) async { + await tester.initializeAppFlowy(); + await tester.tapGoButton(); + + final Finder editor = find.byType(AppFlowyEditor); + await tester.tap(editor); + await tester.pumpAndSettle(); + + expect(find.byType(EmojiSelectionMenu), findsNothing); + + await FlowyTestKeyboard.simulateKeyDownEvent( + [ + Platform.isMacOS + ? LogicalKeyboardKey.meta + : LogicalKeyboardKey.control, + LogicalKeyboardKey.alt, + LogicalKeyboardKey.keyE, + ], + tester: tester, + ); + + expect(find.byType(EmojiSelectionMenu), findsOneWidget); + }); + }); +} diff --git a/frontend/appflowy_flutter/integration_test/runner.dart b/frontend/appflowy_flutter/integration_test/runner.dart index c744a5fa66382..51abc6de7be1c 100644 --- a/frontend/appflowy_flutter/integration_test/runner.dart +++ b/frontend/appflowy_flutter/integration_test/runner.dart @@ -23,6 +23,7 @@ import 'share_markdown_test.dart' as share_markdown_test; import 'sidebar/sidebar_test_runner.dart' as sidebar_test_runner; import 'switch_folder_test.dart' as switch_folder_test; import 'tabs_test.dart' as tabs_test; +import 'emoji_shortcut_test.dart' as emoji_shortcut_test; // import 'auth/supabase_auth_test.dart' as supabase_auth_test_runner; /// The main task runner for all integration tests in AppFlowy. @@ -69,6 +70,7 @@ Future main() async { // Others hotkeys_test.main(); + emoji_shortcut_test.main(); // Appearance integration test appearance_test_runner.main(); 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 eb8ad38ef2a8a..8b959daae4efe 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_page.dart @@ -32,6 +32,7 @@ final List commandShortcutEvents = [ customCutCommand, ...customTextAlignCommands, ...standardCommandShortcutEvents, + emojiShortcutEvent, ]; final List defaultCommandShortcutEvents = [ @@ -93,6 +94,7 @@ class _AppFlowyEditorPageState extends State { customCutCommand, ...customTextAlignCommands, ...standardCommandShortcutEvents, + emojiShortcutEvent, ..._buildFindAndReplaceCommands(), ]; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart index 0ea296a7d4523..1c2dc3650f832 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_menu_item.dart @@ -17,10 +17,12 @@ SelectionMenuItem emojiMenuItem = SelectionMenuItem( keywords: ['emoji'], handler: (editorState, menuService, context) { final container = Overlay.of(context); + menuService.dismiss(); showEmojiPickerMenu( container, editorState, - menuService, + menuService.alignment, + menuService.offset, ); }, ); @@ -28,12 +30,9 @@ SelectionMenuItem emojiMenuItem = SelectionMenuItem( void showEmojiPickerMenu( OverlayState container, EditorState editorState, - SelectionMenuService menuService, + Alignment alignment, + Offset offset, ) { - menuService.dismiss(); - - final alignment = menuService.alignment; - final offset = menuService.offset; final top = alignment == Alignment.topLeft ? offset.dy : null; final bottom = alignment == Alignment.bottomLeft ? offset.dy : null; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart index 573710742c36e..a369cc6b87be4 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart @@ -1,4 +1,5 @@ export 'emoji_menu_item.dart'; +export 'emoji_shortcut_event.dart'; export 'src/emji_picker_config.dart'; export 'src/emoji_picker.dart'; export 'src/emoji_picker_builder.dart'; diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart new file mode 100644 index 0000000000000..805a72105b49c --- /dev/null +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -0,0 +1,86 @@ +import 'package:appflowy_editor/appflowy_editor.dart'; +import 'package:flutter/material.dart'; +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; + +final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( + key: 'show emoji picker', + command: 'ctrl+alt+e', + macOSCommand: 'cmd+alt+e', + handler: _emojiShortcutHandler, +); + +CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { + final selection = editorState.selection; + if (selection == null) { + return KeyEventResult.ignored; + } + final context = editorState.getNodeAtPath(selection.start.path)?.context; + if (context == null) { + return KeyEventResult.ignored; + } + + final container = Overlay.of(context); + + Alignment alignment = Alignment.topLeft; + Offset offset = Offset.zero; + + final selectionService = editorState.service.selectionService; + final selectionRects = selectionService.selectionRects; + if (selectionRects.isEmpty) { + return KeyEventResult.ignored; + } + final rect = selectionRects.first; + + // Calculate the offset and alignment + // Don't like these values being hardcoded but unsure how to grab the + // values dynamically to match the /emoji command. + const menuHeight = 200.0; + const menuOffset = Offset(10, 10); // Tried (0, 10) but that looked off + + final editorOffset = + editorState.renderBox?.localToGlobal(Offset.zero) ?? Offset.zero; + final editorHeight = editorState.renderBox!.size.height; + final editorWidth = editorState.renderBox!.size.width; + + // show below default + alignment = Alignment.topLeft; + final bottomRight = rect.bottomRight; + final topRight = rect.topRight; + var newOffset = bottomRight + menuOffset; + offset = Offset( + newOffset.dx, + newOffset.dy, + ); + + // show above + if (newOffset.dy + menuHeight >= editorOffset.dy + editorHeight) { + offset = topRight - menuOffset; + alignment = Alignment.bottomLeft; + + offset = Offset( + newOffset.dx, + MediaQuery.of(context).size.height - newOffset.dy, + ); + } + + // show on left + if (offset.dx - editorOffset.dx > editorWidth / 2) { + alignment = _alignment == Alignment.topLeft + ? Alignment.topRight + : Alignment.bottomRight; + + offset = Offset( + editorWidth - offset.dx + editorOffset.dx, + offset.dy, + ); + } + + showEmojiPickerMenu( + container, + editorState, + alignment, + offset, + ); + + return KeyEventResult.handled; +}; From 1311e2d379eef5de76b9e584e1129c2adcc8c2da Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:42:01 +0800 Subject: [PATCH 34/50] fix: build regression (#4667) --- .../widgets/emoji_picker/emoji_shortcut_event.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart index 805a72105b49c..e55d0325f477c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( - key: 'show emoji picker', + key: 'Ctrl + Alt + E to show emoji picker', command: 'ctrl+alt+e', macOSCommand: 'cmd+alt+e', + getDescription: () => 'Show an emoji picker', handler: _emojiShortcutHandler, ); @@ -46,7 +47,7 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { alignment = Alignment.topLeft; final bottomRight = rect.bottomRight; final topRight = rect.topRight; - var newOffset = bottomRight + menuOffset; + final newOffset = bottomRight + menuOffset; offset = Offset( newOffset.dx, newOffset.dy, @@ -65,7 +66,7 @@ CommandShortcutEventHandler _emojiShortcutHandler = (editorState) { // show on left if (offset.dx - editorOffset.dx > editorWidth / 2) { - alignment = _alignment == Alignment.topLeft + alignment = alignment == Alignment.topLeft ? Alignment.topRight : Alignment.bottomRight; From c159a5e42b5ca76c9b7984942d86696907f0c8ee Mon Sep 17 00:00:00 2001 From: Richard Shiue <71320345+richardshiue@users.noreply.github.com> Date: Sat, 17 Feb 2024 15:28:49 +0800 Subject: [PATCH 35/50] chore: enable number filter (#4653) * chore: enable filtering by number field type * chore: code cleanup * fix: integration test * chore: remove unnecessary async from event handler --- .../database/database_filter_test.dart | 2 +- .../application/field/field_info.dart | 9 +- .../filter/number_filter_editor_bloc.dart | 117 ++++++++++ .../widgets/filter/choicechip/number.dart | 218 +++++++++++++++++- .../choicechip/select_option/option_list.dart | 62 ++--- .../widgets/filter/filter_info.dart | 57 ++--- .../widgets/filter/filter_menu_item.dart | 39 ++-- frontend/resources/translations/en.json | 12 +- 8 files changed, 411 insertions(+), 105 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart diff --git a/frontend/appflowy_flutter/integration_test/database/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/database/database_filter_test.dart index c1c52bb23d7fa..a7f5726842b17 100644 --- a/frontend/appflowy_flutter/integration_test/database/database_filter_test.dart +++ b/frontend/appflowy_flutter/integration_test/database/database_filter_test.dart @@ -9,7 +9,7 @@ import '../util/database_test_op.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('database filter', () { + group('grid filter:', () { testWidgets('add text filter', (tester) async { await tester.openV020database(); 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 717776f075b53..0bcb645b4afae 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,9 +47,12 @@ class FieldInfo with _$FieldInfo { } bool get canCreateFilter { - if (hasFilter) return false; + if (hasFilter) { + return false; + } switch (field.fieldType) { + case FieldType.Number: case FieldType.Checkbox: case FieldType.MultiSelect: case FieldType.RichText: @@ -62,7 +65,9 @@ class FieldInfo with _$FieldInfo { } bool get canCreateSort { - if (hasSort) return false; + if (hasSort) { + return false; + } switch (field.fieldType) { case FieldType.RichText: 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 new file mode 100644 index 0000000000000..832bfa905ae91 --- /dev/null +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart @@ -0,0 +1,117 @@ +import 'dart:async'; + +import 'package:appflowy/plugins/database/application/filter/filter_listener.dart'; +import 'package:appflowy/plugins/database/application/filter/filter_service.dart'; +import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart'; +import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'number_filter_editor_bloc.freezed.dart'; + +class NumberFilterEditorBloc + extends Bloc { + NumberFilterEditorBloc({required this.filterInfo}) + : _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId), + _listener = FilterListener( + viewId: filterInfo.viewId, + filterId: filterInfo.filter.id, + ), + super(NumberFilterEditorState.initial(filterInfo)) { + _dispatch(); + _startListening(); + } + + final FilterInfo filterInfo; + final FilterBackendService _filterBackendSvc; + final FilterListener _listener; + + void _dispatch() { + on( + (event, emit) async { + event.when( + didReceiveFilter: (filter) { + final filterInfo = state.filterInfo.copyWith(filter: filter); + emit( + state.copyWith( + filterInfo: filterInfo, + filter: filterInfo.numberFilter()!, + ), + ); + }, + updateCondition: (NumberFilterConditionPB condition) { + _filterBackendSvc.insertNumberFilter( + filterId: filterInfo.filter.id, + fieldId: filterInfo.fieldInfo.id, + condition: condition, + content: state.filter.content, + ); + }, + updateContent: (content) { + _filterBackendSvc.insertNumberFilter( + 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, + ); + }, + ); + }, + ); + } + + void _startListening() { + _listener.start( + onDeleted: () { + if (!isClosed) { + add(const NumberFilterEditorEvent.delete()); + } + }, + onUpdated: (filter) { + if (!isClosed) { + add(NumberFilterEditorEvent.didReceiveFilter(filter)); + } + }, + ); + } + + @override + Future close() async { + await _listener.stop(); + return super.close(); + } +} + +@freezed +class NumberFilterEditorEvent with _$NumberFilterEditorEvent { + const factory NumberFilterEditorEvent.didReceiveFilter(FilterPB filter) = + _DidReceiveFilter; + const factory NumberFilterEditorEvent.updateCondition( + NumberFilterConditionPB condition, + ) = _UpdateCondition; + const factory NumberFilterEditorEvent.updateContent(String content) = + _UpdateContent; + const factory NumberFilterEditorEvent.delete() = _Delete; +} + +@freezed +class NumberFilterEditorState with _$NumberFilterEditorState { + const factory NumberFilterEditorState({ + required FilterInfo filterInfo, + required NumberFilterPB filter, + }) = _NumberFilterEditorState; + + factory NumberFilterEditorState.initial(FilterInfo filterInfo) { + return NumberFilterEditorState( + filterInfo: filterInfo, + filter: filterInfo.numberFilter()!, + ); + } +} diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart index f94f72b59c191..0947239273d48 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/number.dart @@ -1,15 +1,227 @@ +import 'package:appflowy/generated/flowy_svgs.g.dart'; +import 'package:appflowy/generated/locale_keys.g.dart'; +import 'package:appflowy/plugins/database/grid/application/filter/number_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:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../condition_button.dart'; +import '../disclosure_button.dart'; import '../filter_info.dart'; import 'choicechip.dart'; -class NumberFilterChoicechip extends StatelessWidget { - const NumberFilterChoicechip({required this.filterInfo, super.key}); +class NumberFilterChoiceChip extends StatefulWidget { + const NumberFilterChoiceChip({ + super.key, + required this.filterInfo, + }); final FilterInfo filterInfo; + @override + State createState() => _NumberFilterChoiceChipState(); +} + +class _NumberFilterChoiceChipState extends State { + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => NumberFilterEditorBloc( + filterInfo: widget.filterInfo, + ), + child: BlocBuilder( + builder: (context, state) { + return AppFlowyPopover( + constraints: BoxConstraints.loose(const Size(200, 100)), + direction: PopoverDirection.bottomWithCenterAligned, + popupBuilder: (_) { + return BlocProvider.value( + value: context.read(), + child: const NumberFilterEditor(), + ); + }, + child: ChoiceChipButton( + filterInfo: state.filterInfo, + ), + ); + }, + ), + ); + } +} + +class NumberFilterEditor extends StatefulWidget { + const NumberFilterEditor({super.key}); + + @override + State createState() => _NumberFilterEditorState(); +} + +class _NumberFilterEditorState extends State { + final popoverMutex = PopoverMutex(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final List children = [ + _buildFilterPanel(context, state), + if (state.filter.condition != NumberFilterConditionPB.NumberIsEmpty && + state.filter.condition != + NumberFilterConditionPB.NumberIsNotEmpty) ...[ + const VSpace(4), + _buildFilterNumberField(context, state), + ], + ]; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1), + child: IntrinsicHeight(child: Column(children: children)), + ); + }, + ); + } + + Widget _buildFilterPanel( + BuildContext context, + NumberFilterEditorState state, + ) { + return SizedBox( + height: 20, + child: Row( + children: [ + Expanded( + child: FlowyText( + state.filterInfo.fieldInfo.name, + overflow: TextOverflow.ellipsis, + ), + ), + const HSpace(4), + Expanded( + child: NumberFilterConditionPBList( + filterInfo: state.filterInfo, + popoverMutex: popoverMutex, + onCondition: (condition) { + context + .read() + .add(NumberFilterEditorEvent.updateCondition(condition)); + }, + ), + ), + const HSpace(4), + DisclosureButton( + popoverMutex: popoverMutex, + onAction: (action) { + switch (action) { + case FilterDisclosureAction.delete: + context + .read() + .add(const NumberFilterEditorEvent.delete()); + break; + } + }, + ), + ], + ), + ); + } + + Widget _buildFilterNumberField( + BuildContext context, + NumberFilterEditorState state, + ) { + return FlowyTextField( + text: state.filter.content, + hintText: LocaleKeys.grid_settings_typeAValue.tr(), + debounceDuration: const Duration(milliseconds: 300), + autoFocus: false, + onChanged: (text) { + context + .read() + .add(NumberFilterEditorEvent.updateContent(text)); + }, + ); + } +} + +class NumberFilterConditionPBList extends StatelessWidget { + const NumberFilterConditionPBList({ + super.key, + required this.filterInfo, + required this.popoverMutex, + required this.onCondition, + }); + + final FilterInfo filterInfo; + final PopoverMutex popoverMutex; + final Function(NumberFilterConditionPB) onCondition; + @override Widget build(BuildContext context) { - return ChoiceChipButton(filterInfo: filterInfo); + final numberFilter = filterInfo.numberFilter()!; + return PopoverActionList( + asBarrier: true, + mutex: popoverMutex, + direction: PopoverDirection.bottomWithCenterAligned, + actions: NumberFilterConditionPB.values + .map( + (action) => ConditionWrapper( + action, + numberFilter.condition == action, + ), + ) + .toList(), + buildChild: (controller) { + return ConditionButton( + conditionName: numberFilter.condition.filterName, + onTap: () => controller.show(), + ); + }, + onSelected: (action, controller) { + onCondition(action.inner); + controller.close(); + }, + ); + } +} + +class ConditionWrapper extends ActionCell { + ConditionWrapper(this.inner, this.isSelected); + + final NumberFilterConditionPB inner; + final bool isSelected; + + @override + Widget? rightIcon(Color iconColor) => + isSelected ? const FlowySvg(FlowySvgs.check_s) : null; + + @override + String get name => inner.filterName; +} + +extension NumberFilterConditionPBExtension on NumberFilterConditionPB { + String get filterName { + return switch (this) { + NumberFilterConditionPB.Equal => LocaleKeys.grid_numberFilter_equal.tr(), + NumberFilterConditionPB.NotEqual => + LocaleKeys.grid_numberFilter_notEqual.tr(), + NumberFilterConditionPB.LessThan => + LocaleKeys.grid_numberFilter_lessThan.tr(), + NumberFilterConditionPB.LessThanOrEqualTo => + LocaleKeys.grid_numberFilter_lessThanOrEqualTo.tr(), + NumberFilterConditionPB.GreaterThan => + LocaleKeys.grid_numberFilter_greaterThan.tr(), + NumberFilterConditionPB.GreaterThanOrEqualTo => + LocaleKeys.grid_numberFilter_greaterThanOrEqualTo.tr(), + NumberFilterConditionPB.NumberIsEmpty => + LocaleKeys.grid_numberFilter_isEmpty.tr(), + NumberFilterConditionPB.NumberIsNotEmpty => + LocaleKeys.grid_numberFilter_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 54fa63c2c0333..5d2406094a040 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 @@ -27,51 +27,37 @@ class SelectOptionFilterList extends StatelessWidget { Widget build(BuildContext context) { return BlocProvider( create: (context) { - late SelectOptionFilterListBloc bloc; - if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) { - bloc = SelectOptionFilterListBloc( - selectedOptionIds: selectedOptionIds, - delegate: - SingleSelectOptionFilterDelegateImpl(filterInfo: filterInfo), - ); - } else { - bloc = SelectOptionFilterListBloc( - selectedOptionIds: selectedOptionIds, - delegate: - MultiSelectOptionFilterDelegateImpl(filterInfo: filterInfo), - ); - } - - bloc.add(const SelectOptionFilterListEvent.initial()); - return bloc; + return SelectOptionFilterListBloc( + selectedOptionIds: selectedOptionIds, + delegate: filterInfo.fieldInfo.fieldType == FieldType.SingleSelect + ? SingleSelectOptionFilterDelegateImpl(filterInfo: filterInfo) + : MultiSelectOptionFilterDelegateImpl(filterInfo: filterInfo), + )..add(const SelectOptionFilterListEvent.initial()); }, child: - BlocListener( + BlocConsumer( listenWhen: (previous, current) => previous.selectedOptionIds != current.selectedOptionIds, listener: (context, state) { onSelectedOptions(state.selectedOptionIds.toList()); }, - child: BlocBuilder( - builder: (context, state) { - return ListView.separated( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: state.visibleOptions.length, - separatorBuilder: (context, index) { - return VSpace(GridSize.typeOptionSeparatorHeight); - }, - itemBuilder: (BuildContext context, int index) { - final option = state.visibleOptions[index]; - return SelectOptionFilterCell( - option: option.optionPB, - isSelected: option.isSelected, - ); - }, - ); - }, - ), + builder: (context, state) { + return ListView.separated( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: state.visibleOptions.length, + separatorBuilder: (context, index) { + return VSpace(GridSize.typeOptionSeparatorHeight); + }, + itemBuilder: (BuildContext context, int index) { + final option = state.visibleOptions[index]; + return SelectOptionFilterCell( + option: option.optionPB, + isSelected: option.isSelected, + ); + }, + ); + }, ), ); } 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 36c6ebf030367..97fc5907482d8 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 @@ -1,11 +1,5 @@ import 'package:appflowy/plugins/database/application/field/field_info.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart'; -import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.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'; class FilterInfo { FilterInfo(this.viewId, this.filter, this.fieldInfo); @@ -27,44 +21,39 @@ class FilterInfo { String get fieldId => filter.fieldId; DateFilterPB? dateFilter() { - if (![ - FieldType.DateTime, - FieldType.LastEditedTime, - FieldType.CreatedTime, - ].contains(filter.fieldType)) { - return null; - } - return DateFilterPB.fromBuffer(filter.data); + return filter.fieldType == FieldType.DateTime + ? DateFilterPB.fromBuffer(filter.data) + : null; } TextFilterPB? textFilter() { - if (filter.fieldType != FieldType.RichText) { - return null; - } - return TextFilterPB.fromBuffer(filter.data); + return filter.fieldType == FieldType.RichText + ? TextFilterPB.fromBuffer(filter.data) + : null; } CheckboxFilterPB? checkboxFilter() { - if (filter.fieldType != FieldType.Checkbox) { - return null; - } - return CheckboxFilterPB.fromBuffer(filter.data); + return filter.fieldType == FieldType.Checkbox + ? CheckboxFilterPB.fromBuffer(filter.data) + : null; } SelectOptionFilterPB? selectOptionFilter() { - if (filter.fieldType == FieldType.SingleSelect || - filter.fieldType == FieldType.MultiSelect) { - return SelectOptionFilterPB.fromBuffer(filter.data); - } else { - return null; - } + return filter.fieldType == FieldType.SingleSelect || + filter.fieldType == FieldType.MultiSelect + ? SelectOptionFilterPB.fromBuffer(filter.data) + : null; } ChecklistFilterPB? checklistFilter() { - if (filter.fieldType == FieldType.Checklist) { - return ChecklistFilterPB.fromBuffer(filter.data); - } else { - return null; - } + return filter.fieldType == FieldType.Checklist + ? ChecklistFilterPB.fromBuffer(filter.data) + : null; + } + + NumberFilterPB? numberFilter() { + return filter.fieldType == FieldType.Number + ? NumberFilterPB.fromBuffer(filter.data) + : null; } } 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 524a17cee49ec..f661ea57de2e9 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 @@ -17,31 +17,18 @@ class FilterMenuItem extends StatelessWidget { @override Widget build(BuildContext context) { - return buildFilterChoicechip(filterInfo); - } -} - -Widget buildFilterChoicechip(FilterInfo filterInfo) { - switch (filterInfo.fieldInfo.fieldType) { - case FieldType.Checkbox: - return CheckboxFilterChoicechip(filterInfo: filterInfo); - case FieldType.DateTime: - case FieldType.LastEditedTime: - case FieldType.CreatedTime: - return DateFilterChoicechip(filterInfo: filterInfo); - case FieldType.MultiSelect: - return SelectOptionFilterChoicechip(filterInfo: filterInfo); - case FieldType.Number: - return NumberFilterChoicechip(filterInfo: filterInfo); - case FieldType.RichText: - return TextFilterChoicechip(filterInfo: filterInfo); - case FieldType.SingleSelect: - return SelectOptionFilterChoicechip(filterInfo: filterInfo); - case FieldType.URL: - return URLFilterChoicechip(filterInfo: filterInfo); - case FieldType.Checklist: - return ChecklistFilterChoicechip(filterInfo: filterInfo); - default: - return const SizedBox(); + return switch (filterInfo.fieldInfo.fieldType) { + FieldType.Checkbox => CheckboxFilterChoicechip(filterInfo: filterInfo), + FieldType.DateTime => DateFilterChoicechip(filterInfo: filterInfo), + FieldType.MultiSelect => + SelectOptionFilterChoicechip(filterInfo: filterInfo), + FieldType.Number => NumberFilterChoiceChip(filterInfo: filterInfo), + FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo), + FieldType.SingleSelect => + SelectOptionFilterChoicechip(filterInfo: filterInfo), + FieldType.URL => URLFilterChoicechip(filterInfo: filterInfo), + FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo), + _ => const SizedBox(), + }; } } diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 83afac352b886..967e511bced55 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -540,6 +540,16 @@ "empty": "Is empty", "notEmpty": "Is not empty" }, + "numberFilter": { + "equal": "Equals", + "notEqual": "Does not equal", + "lessThan": "Is less than", + "greaterThan": "Is greater than", + "lessThanOrEqualTo": "Is less than or equal to", + "greaterThanOrEqualTo": "Is greater than or equal to", + "isEmpty": "Is empty", + "isNotEmpty": "Is not empty" + }, "field": { "hide": "Hide", "show": "Show", @@ -1262,4 +1272,4 @@ "userIcon": "User icon" }, "noLogFiles": "There're no log files" -} +} \ No newline at end of file From 54bfc6566497346ff3eb762f2acb9ffd22ce8679 Mon Sep 17 00:00:00 2001 From: PW Date: Sat, 17 Feb 2024 16:28:10 +0800 Subject: [PATCH 36/50] feat: translate to traditional Chinese (#4505) * inlang: update translations * inlang: update translations * inlang: update translations * inlang: update translations * inlang: update translations --------- Co-authored-by: nathan From ee61ef3f1295f863e939a29fef2e9072cb596b99 Mon Sep 17 00:00:00 2001 From: TeddyFlink <159998207+TeddyFlink@users.noreply.github.com> Date: Sat, 17 Feb 2024 11:31:25 +0300 Subject: [PATCH 37/50] feat: update translations (#4654) --- frontend/resources/translations/ar-SA.json | 10 ++++---- frontend/resources/translations/ca-ES.json | 10 ++++---- frontend/resources/translations/de-DE.json | 10 ++++---- frontend/resources/translations/es-VE.json | 10 ++++---- frontend/resources/translations/eu-ES.json | 10 ++++---- frontend/resources/translations/fa.json | 10 ++++---- frontend/resources/translations/fr-CA.json | 10 ++++---- frontend/resources/translations/fr-FR.json | 30 ++++++++++++++-------- frontend/resources/translations/hu-HU.json | 10 ++++---- frontend/resources/translations/id-ID.json | 10 ++++---- frontend/resources/translations/it-IT.json | 10 ++++---- frontend/resources/translations/ja-JP.json | 10 ++++---- frontend/resources/translations/ko-KR.json | 10 ++++---- frontend/resources/translations/pl-PL.json | 10 ++++---- frontend/resources/translations/pt-BR.json | 10 ++++---- frontend/resources/translations/pt-PT.json | 10 ++++---- frontend/resources/translations/ru-RU.json | 10 ++++---- frontend/resources/translations/tr-TR.json | 10 ++++---- frontend/resources/translations/vi-VN.json | 10 ++++---- frontend/resources/translations/zh-CN.json | 8 +++--- frontend/resources/translations/zh-TW.json | 10 ++++---- 21 files changed, 119 insertions(+), 109 deletions(-) diff --git a/frontend/resources/translations/ar-SA.json b/frontend/resources/translations/ar-SA.json index 2172d161b6c00..d6872679586e9 100644 --- a/frontend/resources/translations/ar-SA.json +++ b/frontend/resources/translations/ar-SA.json @@ -70,12 +70,12 @@ "copyLink": "نسخ الرابط" }, "moreAction": { - "small": "صغير", - "medium": "متوسط", - "large": "كبير", "fontSize": "حجم الخط", "import": "استيراد", - "moreOptions": "المزيد من الخيارات" + "moreOptions": "المزيد من الخيارات", + "small": "صغير", + "medium": "متوسط", + "large": "كبير" }, "importPanel": { "textAndMarkdown": "نص و Markdown", @@ -1173,4 +1173,4 @@ "addField": "إضافة حقل", "userIcon": "رمز المستخدم" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ca-ES.json b/frontend/resources/translations/ca-ES.json index 20f4dd3cc379a..1379cc4450665 100644 --- a/frontend/resources/translations/ca-ES.json +++ b/frontend/resources/translations/ca-ES.json @@ -69,12 +69,12 @@ "copyLink": "Copiar l'enllaç" }, "moreAction": { - "small": "petit", - "medium": "mitjà", - "large": "gran", "fontSize": "Mida de la font", "import": "Importar", - "moreOptions": "Més opcions" + "moreOptions": "Més opcions", + "small": "petit", + "medium": "mitjà", + "large": "gran" }, "importPanel": { "textAndMarkdown": "Text i rebaixa", @@ -812,4 +812,4 @@ "deleteContentTitle": "Esteu segur que voleu suprimir {pageType}?", "deleteContentCaption": "si suprimiu aquest {pageType}, podeu restaurar-lo des de la paperera." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/de-DE.json b/frontend/resources/translations/de-DE.json index cd0c4a3a83df5..d8147c62fc37a 100644 --- a/frontend/resources/translations/de-DE.json +++ b/frontend/resources/translations/de-DE.json @@ -69,12 +69,12 @@ "copyLink": "Link kopieren" }, "moreAction": { - "small": "klein", - "medium": "mittel", - "large": "groß", "fontSize": "Schriftgröße", "import": "Importieren", - "moreOptions": "Mehr Optionen" + "moreOptions": "Mehr Optionen", + "small": "klein", + "medium": "mittel", + "large": "groß" }, "importPanel": { "textAndMarkdown": "Text & Markdown", @@ -1202,4 +1202,4 @@ "addField": "Ein Feld hinzufügen", "userIcon": "Nutzerbild" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/es-VE.json b/frontend/resources/translations/es-VE.json index 94a2a215391d8..8bbb3aac2f318 100644 --- a/frontend/resources/translations/es-VE.json +++ b/frontend/resources/translations/es-VE.json @@ -72,12 +72,12 @@ "copyLink": "Copiar enlace" }, "moreAction": { - "small": "pequeño", - "medium": "medio", - "large": "grande", "fontSize": "Tamaño de fuente", "import": "Importar", - "moreOptions": "Mas opciones" + "moreOptions": "Mas opciones", + "small": "pequeño", + "medium": "medio", + "large": "grande" }, "importPanel": { "textAndMarkdown": "Texto y descuento", @@ -1064,4 +1064,4 @@ "backgroundColorPink": "fondo rosa", "backgroundColorRed": "fondo rojo" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/eu-ES.json b/frontend/resources/translations/eu-ES.json index ca7c8e8b89ed2..e5573ad680e3a 100644 --- a/frontend/resources/translations/eu-ES.json +++ b/frontend/resources/translations/eu-ES.json @@ -50,12 +50,12 @@ "copyLink": "Esteka kopiatu" }, "moreAction": { - "small": "txikia", - "medium": "ertaina", - "large": "handia", "fontSize": "Letra tamaina", "import": "Inportatu", - "moreOptions": "Aukera gehiago" + "moreOptions": "Aukera gehiago", + "small": "txikia", + "medium": "ertaina", + "large": "handia" }, "importPanel": { "textAndMarkdown": "Testua eta Markdown", @@ -601,4 +601,4 @@ "deleteContentTitle": "Ziur {pageType} ezabatu nahi duzula?", "deleteContentCaption": "{pageType} hau ezabatzen baduzu, zaborrontzitik leheneratu dezakezu." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/fa.json b/frontend/resources/translations/fa.json index 802b7ebbfceee..d7cdf6e92e406 100644 --- a/frontend/resources/translations/fa.json +++ b/frontend/resources/translations/fa.json @@ -55,12 +55,12 @@ "copyLink": "کپی کردن لینک" }, "moreAction": { - "small": "کوچک", - "medium": "متوسط", - "large": "بزرگ", "fontSize": "اندازه قلم", "import": "اضافه کردن", - "moreOptions": "گزینه های بیشتر" + "moreOptions": "گزینه های بیشتر", + "small": "کوچک", + "medium": "متوسط", + "large": "بزرگ" }, "importPanel": { "textAndMarkdown": "Text & Markdown", @@ -674,4 +674,4 @@ "frequentlyUsed": "استفاده‌شده" } } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 7cc07d9deb48d..6f4d740ba024f 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -50,12 +50,12 @@ "copyLink": "Copier le lien" }, "moreAction": { - "small": "petit", - "medium": "moyen", - "large": "grand", "fontSize": "Taille de police", "import": "Importer", - "moreOptions": "Plus d'options" + "moreOptions": "Plus d'options", + "small": "petit", + "medium": "moyen", + "large": "grand" }, "importPanel": { "textAndMarkdown": "Texte et Markdown", @@ -595,4 +595,4 @@ "deleteContentTitle": "Voulez-vous vraiment supprimer le {pageType} ?", "deleteContentCaption": "si vous supprimez ce {pageType}, vous pouvez le restaurer à partir de la corbeille." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index a0c20ed2d6b72..b66874df9cd67 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -74,12 +74,12 @@ "copyLink": "Copier le lien" }, "moreAction": { - "small": "petit", - "medium": "moyen", - "large": "grand", "fontSize": "Taille de police", "import": "Importer", - "moreOptions": "Plus d'options" + "moreOptions": "Plus d'options", + "small": "petit", + "medium": "moyen", + "large": "grand" }, "importPanel": { "textAndMarkdown": "Texte et Markdown", @@ -659,7 +659,15 @@ "showComplete": "Afficher toutes les tâches" }, "menuName": "Grille", - "referencedGridPrefix": "Vue" + "referencedGridPrefix": "Vue", + "calculationTypeLabel": { + "none": "Vide", + "average": "Moyenne", + "max": "Maximum", + "median": "Médiane", + "min": "Minimum", + "sum": "Somme" + } }, "document": { "menuName": "Document", @@ -872,7 +880,8 @@ "page": { "label": "Lien vers la page", "tooltip": "Cliquez pour ouvrir la page" - } + }, + "deleted": "Supprimer" }, "toolbar": { "resetToDefaultFont": "Réinitialiser aux valeurs par défaut" @@ -944,10 +953,10 @@ "layoutDateField": "Calendrier de mise en page par", "changeLayoutDateField": "Modifier le champ de mise en page", "noDateTitle": "Pas de date", - "noDateHint": "Les événements non planifiés s'afficheront ici", "unscheduledEventsTitle": "Événements imprévus", "clickToAdd": "Cliquez pour ajouter au calendrier", - "name": "Disposition du calendrier" + "name": "Disposition du calendrier", + "noDateHint": "Les événements non planifiés s'afficheront ici" }, "referencedCalendarPrefix": "Vue", "quickJumpYear": "Sauter à" @@ -1052,7 +1061,8 @@ "onDayOfEvent": "Le jour de l'événement", "oneDayBefore": "1 jour avant", "twoDaysBefore": "2 jours avant", - "oneWeekBefore": "1 semaine avant" + "oneWeekBefore": "1 semaine avant", + "custom": "Personnaliser" } }, "relativeDates": { @@ -1249,4 +1259,4 @@ "userIcon": "Icône utilisateur" }, "noLogFiles": "Il n'y a pas de log" -} \ No newline at end of file +} diff --git a/frontend/resources/translations/hu-HU.json b/frontend/resources/translations/hu-HU.json index f2255af19e9e8..93d13413103e5 100644 --- a/frontend/resources/translations/hu-HU.json +++ b/frontend/resources/translations/hu-HU.json @@ -50,12 +50,12 @@ "copyLink": "Link másolása" }, "moreAction": { - "small": "kicsi", - "medium": "közepes", - "large": "nagy", "fontSize": "Betűméret", "import": "Importálás", - "moreOptions": "Több lehetőség" + "moreOptions": "Több lehetőség", + "small": "kicsi", + "medium": "közepes", + "large": "nagy" }, "importPanel": { "textAndMarkdown": "Szöveg & Markdown", @@ -599,4 +599,4 @@ "deleteContentTitle": "Biztosan törli a következőt: {pageType}?", "deleteContentCaption": "ha törli ezt a {pageType} oldalt, visszaállíthatja a kukából." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/id-ID.json b/frontend/resources/translations/id-ID.json index 471716c36a811..6959403759d24 100644 --- a/frontend/resources/translations/id-ID.json +++ b/frontend/resources/translations/id-ID.json @@ -72,12 +72,12 @@ "copyLink": "Salin tautan" }, "moreAction": { - "small": "kecil", - "medium": "sedang", - "large": "besar", "fontSize": "Ukuran huruf", "import": "Impor", - "moreOptions": "Lebih banyak pilihan" + "moreOptions": "Lebih banyak pilihan", + "small": "kecil", + "medium": "sedang", + "large": "besar" }, "importPanel": { "textAndMarkdown": "Teks & Markdown", @@ -1024,4 +1024,4 @@ "noFavorite": "Tidak ada halaman favorit", "noFavoriteHintText": "Geser halaman ke kiri untuk menambahkannya ke favorit Anda" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/it-IT.json b/frontend/resources/translations/it-IT.json index cc9b8cbc042ba..34c31b686126d 100644 --- a/frontend/resources/translations/it-IT.json +++ b/frontend/resources/translations/it-IT.json @@ -70,12 +70,12 @@ "copyLink": "Copia Link" }, "moreAction": { - "small": "piccolo", - "medium": "medio", - "large": "grande", "fontSize": "Dimensione del font", "import": "Importare", - "moreOptions": "Più opzioni" + "moreOptions": "Più opzioni", + "small": "piccolo", + "medium": "medio", + "large": "grande" }, "importPanel": { "textAndMarkdown": "Testo e markdown", @@ -1186,4 +1186,4 @@ "addField": "Aggiungi campo", "userIcon": "Icona utente" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index ab43685a73418..b74c71f2759a0 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -50,12 +50,12 @@ "copyLink": "Copy Link" }, "moreAction": { - "small": "小さい", - "medium": "中くらい", - "large": "大きい", "fontSize": "フォントサイズ", "import": "取り込む", - "moreOptions": "より多くのオプション" + "moreOptions": "より多くのオプション", + "small": "小さい", + "medium": "中くらい", + "large": "大きい" }, "importPanel": { "textAndMarkdown": "テキストとマークダウン", @@ -596,4 +596,4 @@ "deleteContentTitle": "{pageType} を削除してもよろしいですか?", "deleteContentCaption": "この {pageType} を削除しても、ゴミ箱から復元できます。" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ko-KR.json b/frontend/resources/translations/ko-KR.json index dcccd2673bcdc..5fad732b78178 100644 --- a/frontend/resources/translations/ko-KR.json +++ b/frontend/resources/translations/ko-KR.json @@ -50,12 +50,12 @@ "copyLink": "링크 복사" }, "moreAction": { - "small": "작은", - "medium": "중간", - "large": "크기가 큰", "fontSize": "글꼴 크기", "import": "수입", - "moreOptions": "추가 옵션" + "moreOptions": "추가 옵션", + "small": "작은", + "medium": "중간", + "large": "크기가 큰" }, "importPanel": { "textAndMarkdown": "텍스트 및 마크다운", @@ -598,4 +598,4 @@ "deleteContentTitle": "{pageType}을(를) 삭제하시겠습니까?", "deleteContentCaption": "이 {pageType}을(를) 삭제하면 휴지통에서 복원할 수 있습니다." } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/pl-PL.json b/frontend/resources/translations/pl-PL.json index 68e7c4a6e687f..06aa12352c186 100644 --- a/frontend/resources/translations/pl-PL.json +++ b/frontend/resources/translations/pl-PL.json @@ -70,12 +70,12 @@ "copyLink": "Skopiuj link" }, "moreAction": { - "small": "mały", - "medium": "średni", - "large": "duży", "fontSize": "Rozmiar czcionki", "import": "Import", - "moreOptions": "Więcej opcji" + "moreOptions": "Więcej opcji", + "small": "mały", + "medium": "średni", + "large": "duży" }, "importPanel": { "textAndMarkdown": "Tekst i Markdown", @@ -1078,4 +1078,4 @@ "language": "Język", "font": "Czcionka" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/pt-BR.json b/frontend/resources/translations/pt-BR.json index 7f2176ee8ac4d..b7ef0bcd50bab 100644 --- a/frontend/resources/translations/pt-BR.json +++ b/frontend/resources/translations/pt-BR.json @@ -72,12 +72,12 @@ "copyLink": "Copiar link" }, "moreAction": { - "small": "pequeno", - "medium": "médio", - "large": "grande", "fontSize": "Tamanho da fonte", "import": "Importar", - "moreOptions": "Mais opções" + "moreOptions": "Mais opções", + "small": "pequeno", + "medium": "médio", + "large": "grande" }, "importPanel": { "textAndMarkdown": "Texto e Remarcação", @@ -1212,4 +1212,4 @@ "addField": "Adicionar campo", "userIcon": "Ícone do usuário" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/pt-PT.json b/frontend/resources/translations/pt-PT.json index 36818b4a9fd43..dbbfc69f50b02 100644 --- a/frontend/resources/translations/pt-PT.json +++ b/frontend/resources/translations/pt-PT.json @@ -70,12 +70,12 @@ "copyLink": "Copiar o link" }, "moreAction": { - "small": "pequeno", - "medium": "médio", - "large": "grande", "fontSize": "Tamanho da fonte", "import": "Importar", - "moreOptions": "Mais opções" + "moreOptions": "Mais opções", + "small": "pequeno", + "medium": "médio", + "large": "grande" }, "importPanel": { "textAndMarkdown": "Texto e Remarcação", @@ -857,4 +857,4 @@ "noResult": "Nenhum resultado", "caseSensitive": "Maiúsculas e minúsculas" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/ru-RU.json b/frontend/resources/translations/ru-RU.json index 1a5776a380afa..9658b769e1e07 100644 --- a/frontend/resources/translations/ru-RU.json +++ b/frontend/resources/translations/ru-RU.json @@ -70,12 +70,12 @@ "copyLink": "Скопировать ссылку" }, "moreAction": { - "small": "маленький", - "medium": "средний", - "large": "большой", "fontSize": "Размер шрифта", "import": "Импорт", - "moreOptions": "Дополнительные опции" + "moreOptions": "Дополнительные опции", + "small": "маленький", + "medium": "средний", + "large": "большой" }, "importPanel": { "textAndMarkdown": "Текст и Markdown", @@ -1204,4 +1204,4 @@ "addField": "Добавить поле", "userIcon": "Пользовательская иконка" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/tr-TR.json b/frontend/resources/translations/tr-TR.json index eb018802485d7..dfc3cc29ac08f 100644 --- a/frontend/resources/translations/tr-TR.json +++ b/frontend/resources/translations/tr-TR.json @@ -69,12 +69,12 @@ "copyLink": "Bağlantıyı kopyala" }, "moreAction": { - "small": "Küçük", - "medium": "Orta", - "large": "Büyük", "fontSize": "Yazı boyutu", "import": "İçe aktar", - "moreOptions": "Diğer seçenekler" + "moreOptions": "Diğer seçenekler", + "small": "Küçük", + "medium": "Orta", + "large": "Büyük" }, "importPanel": { "textAndMarkdown": "Metin ve Markdown", @@ -1093,4 +1093,4 @@ "language": "Dil", "font": "Yazı tipi" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/vi-VN.json b/frontend/resources/translations/vi-VN.json index 27923f68a54eb..c0c8c279d8acc 100644 --- a/frontend/resources/translations/vi-VN.json +++ b/frontend/resources/translations/vi-VN.json @@ -72,12 +72,12 @@ "copyLink": "Sao chép đường dẫn" }, "moreAction": { - "small": "nhỏ", - "medium": "trung bình", - "large": "lớn", "fontSize": "Cỡ chữ", "import": "Import", - "moreOptions": "Lựa chọn khác" + "moreOptions": "Lựa chọn khác", + "small": "nhỏ", + "medium": "trung bình", + "large": "lớn" }, "importPanel": { "textAndMarkdown": "Văn bản & Markdown", @@ -811,4 +811,4 @@ "font": "Phông chữ", "date": "Ngày" } -} \ No newline at end of file +} diff --git a/frontend/resources/translations/zh-CN.json b/frontend/resources/translations/zh-CN.json index e0e0dae358358..47f8f629af1c6 100644 --- a/frontend/resources/translations/zh-CN.json +++ b/frontend/resources/translations/zh-CN.json @@ -74,12 +74,12 @@ "copyLink": "复制链接" }, "moreAction": { - "small": "小", - "medium": "中", - "large": "大", "fontSize": "字体大小", "import": "导入", - "moreOptions": "更多选项" + "moreOptions": "更多选项", + "small": "小", + "medium": "中", + "large": "大" }, "importPanel": { "textAndMarkdown": "文本和Markdown", diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 2a6cfa2411712..673ddc56b7d72 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -71,12 +71,12 @@ "copyLink": "複製連結" }, "moreAction": { - "small": "小", - "medium": "中", - "large": "大", "fontSize": "字型大小", "import": "匯入", - "moreOptions": "更多選項" + "moreOptions": "更多選項", + "small": "小", + "medium": "中", + "large": "大" }, "importPanel": { "textAndMarkdown": "文字 & Markdown", @@ -1244,4 +1244,4 @@ "addField": "新增欄位", "userIcon": "使用者圖示" } -} \ No newline at end of file +} From ddbe0fa698fe10fa468338cdd775b352e9d2e00d Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:38:12 +0100 Subject: [PATCH 38/50] fix: tauri build (#4669) * ci: update workflows * chore: add back translations used on web --- .github/workflows/build_bot.yaml | 3 +-- .github/workflows/commit_lint.yml | 3 +-- .github/workflows/docker_ci.yml | 2 +- .github/workflows/flutter_ci.yaml | 29 +++++++++++++------------ .github/workflows/mobile_ci.yaml | 4 ++-- .github/workflows/release.yml | 18 +++++++-------- .github/workflows/rust_ci.yaml | 8 +++---- .github/workflows/rust_coverage.yml | 2 +- .github/workflows/tauri_ci.yaml | 4 ++-- .github/workflows/web_ci.yaml | 10 +++++---- frontend/resources/translations/en.json | 5 ++++- 11 files changed, 45 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build_bot.yaml b/.github/workflows/build_bot.yaml index 7a67f0a669072..65854b94d123a 100644 --- a/.github/workflows/build_bot.yaml +++ b/.github/workflows/build_bot.yaml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 # get build name from pubspec.yaml - name: Get build version @@ -19,7 +19,6 @@ jobs: echo "fetching version from pubspec.yaml..." echo "build_name=$(grep 'version: ' pubspec.yaml | awk '{print $2}')" >> $GITHUB_OUTPUT - - uses: peter-evans/slash-command-dispatch@v4 with: token: ${{ secrets.PAT }} diff --git a/.github/workflows/commit_lint.yml b/.github/workflows/commit_lint.yml index 4c9a5a54732a1..eb55922af23d8 100644 --- a/.github/workflows/commit_lint.yml +++ b/.github/workflows/commit_lint.yml @@ -5,8 +5,7 @@ jobs: commitlint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: wagoid/commitlint-github-action@v4 - diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml index c3b8e7c20e2aa..5755fb72766ed 100644 --- a/.github/workflows/docker_ci.yml +++ b/.github/workflows/docker_ci.yml @@ -31,7 +31,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Build the app shell: bash diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index c4e3497aec864..439df3143af68 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -9,6 +9,7 @@ on: - ".github/workflows/flutter_ci.yaml" - "frontend/rust-lib/**" - "frontend/appflowy_flutter/**" + - "frontend/resources/**" pull_request: branches: @@ -18,6 +19,7 @@ on: - ".github/workflows/flutter_ci.yaml" - "frontend/rust-lib/**" - "frontend/appflowy_flutter/**" + - "frontend/resources/**" env: CARGO_TERM_COLOR: always @@ -59,7 +61,7 @@ jobs: sudo rm -rf "$AGENT_TOOLSDIRECTORY" - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain id: rust_toolchain @@ -120,7 +122,7 @@ jobs: run: | tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: ${{ github.run_id }}-${{ matrix.os }} path: appflowy_flutter.tar.gz @@ -140,7 +142,7 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain id: rust_toolchain @@ -192,7 +194,7 @@ jobs: fi shell: bash - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ${{ github.run_id }}-${{ matrix.os }} @@ -216,11 +218,11 @@ jobs: shell: bash cloud_integration_test: - needs: [ prepare ] + needs: [prepare] strategy: fail-fast: false matrix: - os: [ ubuntu-latest ] + os: [ubuntu-latest] include: - os: ubuntu-latest flutter_profile: development-linux-x86_64 @@ -229,11 +231,10 @@ jobs: steps: - name: Checkout appflowy cloud code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: AppFlowy-IO/AppFlowy-Cloud path: AppFlowy-Cloud - depth: 1 - name: Prepare appflowy cloud env working-directory: AppFlowy-Cloud @@ -253,7 +254,7 @@ jobs: sleep 10 - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install flutter id: flutter @@ -280,7 +281,7 @@ jobs: flutter config --enable-linux-desktop shell: bash - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ${{ github.run_id }}-${{ matrix.os }} @@ -318,7 +319,7 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain id: rust_toolchain @@ -363,7 +364,7 @@ jobs: fi shell: bash - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ${{ github.run_id }}-${{ matrix.os }} @@ -409,7 +410,7 @@ jobs: steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain id: rust_toolchain @@ -454,7 +455,7 @@ jobs: fi shell: bash - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: name: ${{ github.run_id }}-${{ matrix.os }} diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml index 0e64811c3e258..1fa136052ccc1 100644 --- a/.github/workflows/mobile_ci.yaml +++ b/.github/workflows/mobile_ci.yaml @@ -51,7 +51,7 @@ jobs: sudo rm -rf $ANDROID_HOME/ndk - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install flutter id: flutter @@ -67,7 +67,7 @@ jobs: ndk-version: "r24" add-to-path: true - - uses: gradle/gradle-build-action@v2 + - uses: gradle/gradle-build-action@v3 with: gradle-version: 7.6.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c82b9f23ac206..cb767f2094191 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Build release notes run: | @@ -52,7 +52,7 @@ jobs: - { target: x86_64-pc-windows-msvc, os: windows-2019 } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter uses: subosito/flutter-action@v2 @@ -138,7 +138,7 @@ jobs: - { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter uses: subosito/flutter-action@v2 @@ -238,7 +238,7 @@ jobs: } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter uses: subosito/flutter-action@v2 @@ -343,7 +343,7 @@ jobs: } steps: - name: Checkout source code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install flutter uses: subosito/flutter-action@v2 @@ -455,19 +455,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Login to Docker Hub - uses: docker/login-action@v1 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + uses: docker/setup-buildx-action@v3 - name: Build and push - uses: docker/build-push-action@v2 + uses: docker/build-push-action@v5 with: context: . file: ./frontend/scripts/docker-buildfiles/Dockerfile diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml index 3abfa67ad03d4..b4062980b5ea3 100644 --- a/.github/workflows/rust_ci.yaml +++ b/.github/workflows/rust_ci.yaml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain id: rust_toolchain @@ -45,16 +45,15 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - prefix-key: 'ubuntu-latest' + prefix-key: "ubuntu-latest" workspaces: | frontend/rust-lib - name: Checkout appflowy cloud code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: AppFlowy-IO/AppFlowy-Cloud path: AppFlowy-Cloud - depth: 1 - name: Prepare appflowy cloud env working-directory: AppFlowy-Cloud @@ -80,7 +79,6 @@ jobs: af_cloud_test_gotrue_url: http://localhost/gotrue run: cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture - - name: rustfmt rust-lib run: cargo fmt --all -- --check working-directory: frontend/rust-lib/ diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index dbb2794bc3f70..61cdb7882296e 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout source code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install Rust toolchain id: rust_toolchain diff --git a/.github/workflows/tauri_ci.yaml b/.github/workflows/tauri_ci.yaml index 66024c75f275d..7045e58aa1a8e 100644 --- a/.github/workflows/tauri_ci.yaml +++ b/.github/workflows/tauri_ci.yaml @@ -5,6 +5,7 @@ on: - ".github/workflows/tauri_ci.yaml" - "frontend/rust-lib/**" - "frontend/appflowy_tauri/**" + - "frontend/resources/**" env: NODE_VERSION: "18.16.0" @@ -21,8 +22,7 @@ jobs: strategy: fail-fast: false matrix: -# platform: [macos-latest, ubuntu-latest, windows-latest] - platform: [ubuntu-latest] + platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/web_ci.yaml b/.github/workflows/web_ci.yaml index 943a778b12d70..079a176772d99 100644 --- a/.github/workflows/web_ci.yaml +++ b/.github/workflows/web_ci.yaml @@ -26,9 +26,9 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: setup node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} @@ -40,14 +40,16 @@ jobs: frontend/rust-lib frontend/appflowy_web/appflowy_wasm + # TODO: Can combine caching deps and node_modules in one + # See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob - name: Cache Node.js dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ runner.os }} - name: Cache node_modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: frontend/appflowy_web/node_modules key: node-modules-${{ runner.os }} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 967e511bced55..e94485a64e823 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -73,6 +73,9 @@ "copyLink": "Copy Link" }, "moreAction": { + "small": "small", + "medium": "medium", + "large": "large", "fontSize": "Font size", "import": "Import", "moreOptions": "More options" @@ -1272,4 +1275,4 @@ "userIcon": "User icon" }, "noLogFiles": "There're no log files" -} \ No newline at end of file +} From a637517413ec4b68d0f0f8f1bd20ee7e0599b518 Mon Sep 17 00:00:00 2001 From: kosei <4537402+koseimori@users.noreply.github.com> Date: Sun, 18 Feb 2024 01:25:52 +0900 Subject: [PATCH 39/50] feat: update ja-JP translations (#4627) --- frontend/resources/translations/fr-FR.json | 4 +- frontend/resources/translations/ja-JP.json | 108 +++++++++++++++++++-- 2 files changed, 102 insertions(+), 10 deletions(-) diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index b66874df9cd67..8455de0e8dcd9 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -484,7 +484,6 @@ "typeAValue": "Tapez une valeur...", "layout": "Mise en page", "databaseLayout": "Mise en page", - "viewList": "Vues de base de données", "editView": "Modifier vue", "boardSettings": "Paramètres du tableau", "calendarSettings": "Paramètres du calendrier", @@ -492,7 +491,8 @@ "duplicateView": "Dupliquer vue", "deleteView": "Supprimer vue", "numberOfVisibleFields": "{} affiché", - "Properties": "Propriétés" + "Properties": "Propriétés", + "viewList": "Vues de base de données" }, "textFilter": { "contains": "Contient", diff --git a/frontend/resources/translations/ja-JP.json b/frontend/resources/translations/ja-JP.json index b74c71f2759a0..56c2ba8b8ef23 100644 --- a/frontend/resources/translations/ja-JP.json +++ b/frontend/resources/translations/ja-JP.json @@ -12,7 +12,9 @@ "addBelowTooltip": "クリックして下に追加してください", "addAboveCmd": "Alt+クリック", "addAboveMacCmd": "Option+クリック", - "addAboveTooltip": "上に追加する" + "addAboveTooltip": "上に追加する", + "dragTooltip": "ドラッグして移動", + "openMenuTooltip": "クリックしてメニューを開く" }, "signUp": { "buttonText": "新規登録", @@ -30,24 +32,35 @@ "loginTitle": "@:appName にログイン", "loginButtonText": "ログイン", "buttonText": "サインイン", + "signingInText": "サインイン中...", "forgotPassword": "パスワードをお忘れですか?", "emailHint": "メールアドレス", "passwordHint": "パスワード", "dontHaveAnAccount": "まだアカウントをお持ちではないですか?", "repeatPasswordEmptyError": "パスワード(確認用)を空にはできません", "unmatchedPasswordError": "パスワード(確認用)が一致しません", + "LogInWithGoogle": "Googleでログイン", + "LogInWithGithub": "GitHubでログイン", + "LogInWithDiscord": "Discordでログイン", "loginAsGuestButtonText": "始めましょう" }, "workspace": { + "chooseWorkspace": "ワークスベースを選択", "create": "ワークスペースを作成する", "hint": "ワークスペース", - "notFoundError": "ワークスペースがみつかりません" + "notFoundError": "ワークスペースがみつかりません", + "errorActions": { + "reportIssue": "問題を報告", + "reportIssueOnGithub": "GitHubで問題を報告", + "exportLogFiles": "ログファイルを出力" + } }, "shareAction": { "buttonText": "共有する", "workInProgress": "Coming soon", "markdown": "Markdown", - "copyLink": "Copy Link" + "csv": "CSV", + "copyLink": "リンクをコピー" }, "moreAction": { "fontSize": "フォントサイズ", @@ -68,10 +81,17 @@ "rename": "名前を変更", "delete": "削除", "duplicate": "コピーを作成", - "openNewTab": "新しいタブで開く" + "unfavorite": "お気に入りから削除", + "favorite": "お気に入りに追加", + "openNewTab": "新しいタブで開く", + "addToFavorites": "お気に入りに追加", + "copyLink": "リンクをコピー" }, "blankPageTitle": "空のページ", "newPageText": "新しいページ", + "newDocumentText": "新しいドキュメント", + "newCalendarText": "新しいカレンダー", + "newBoardText": "新しいボード", "trash": { "text": "ごみ箱", "restoreAll": "全て復元", @@ -88,6 +108,11 @@ "confirmRestoreAll": { "title": "ゴミ箱内のすべてのページを復元してもよろしいですか?", "caption": "この操作は元に戻すことができません。" + }, + "mobile": { + "empty": "ゴミ箱を殻にする", + "emptyDescription": "削除されたファイルはありません", + "isDeleted": "削除済み" } }, "deletePagePrompt": { @@ -144,7 +169,11 @@ }, "sideBar": { "closeSidebar": "Close sidebar", - "openSidebar": "Open sidebar" + "openSidebar": "Open sidebar", + "favorites": "お気に入り", + "clickToHideFavorites": "クリックしてお気に入りを隠す", + "addAPage": "ページを追加", + "recent": "最近" }, "notifications": { "export": { @@ -159,7 +188,9 @@ "editContact": "連絡先を編集する" }, "button": { + "ok": "OK", "done": "終わり", + "cancel": "キャンセル", "signIn": "サインイン", "signOut": "サインアウト", "complete": "完了", @@ -171,11 +202,19 @@ "discard": "破棄", "replace": "交換", "insertBelow": "下に挿入", + "insertAbove": "上に挿入", "upload": "アップロード", "edit": "編集", "delete": "消去", "duplicate": "複製", "putback": "戻す", + "update": "更新", + "share": "共有", + "removeFromFavorites": "お気に入りから削除", + "addToFavorites": "お気に入りへ追加", + "rename": "名前を変更", + "helpCenter": "ヘルプセンター", + "add": "追加", "Done": "終わり", "Cancel": "キャンセル", "OK": "OK" @@ -193,8 +232,8 @@ "failedMsg": "サインインが完了したことをブラウザーで確認してください" }, "google": { - "title": "GOOGLEでサインイン", - "instruction1": "GOOGLEでのサインインを有効にするためには、Webブラウザーを使ってこのアプリケーションを認証する必要があります。", + "title": "Googleでサインイン", + "instruction1": "Googleでのサインインを有効にするためには、Webブラウザーを使ってこのアプリケーションを認証する必要があります。", "instruction2": "アイコンをクリックするか、以下のテキストを選択して、このコードをクリップボードにコピーします。", "instruction3": "以下のリンク先をブラウザーで開いて、次のコードを入力します。", "instruction4": "登録が完了したら以下のボタンを押してください。" @@ -207,7 +246,31 @@ "language": "言語", "user": "ユーザー", "files": "ファイル", + "notifications": "通知", "open": "設定", + "logout": "ログアウト", + "logoutPrompt": "本当にログアウトしますか?", + "syncSetting": "設定を同期", + "enableSync": "同期を有効可", + "enableEncrypt": "暗号化されたデータ", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "Supabase URL", + "cloudSupabaseUrlCanNotBeEmpty": "Supabase URLは空白にはできません", + "cloudSupabaseAnonKey": "Supabase anon key", + "cloudSupabaseAnonKeyCanNotBeEmpty": "Supabase anon keyは空白にはできません", + "cloudAppFlowy": "AppFlowy Cloud Beta", + "cloudAppFlowySelfHost": "AppFlowy Cloud セルフホスト", + "appFlowyCloudUrlCanNotBeEmpty": "クラウドURLは空白にはできません", + "clickToCopy": "クリックしてコピー", + "selfHostStart": "サーバーが準備できていない場合、", + "selfHostContent": "ドキュメント", + "selfHostEnd": "を参照してセルフホストサーバーのセットアップ手順を確認してください", + "cloudURLHint": "サーバーのURLを入力", + "cloudWSURL": "Websocket URL", + "cloudWSURLHint": "Websocketサーバーのアドレスを入力", + "restartApp": "再起動", + "clickToCopySecret": "クリックしてシークレットをコピー", + "historicalUserList": "ログイン履歴", "supabaseSetting": "Supabaseの設定" }, "appearance": { @@ -223,6 +286,7 @@ }, "themeUpload": { "button": "アップロード", + "uploadTheme": "テーマをアップロード", "description": "下のボタンを使用して、独自のAppFlowyテーマをアップロードします。", "loading": "テーマを検証してアップロードするまでお待ちください...", "uploadSuccess": "テーマは正常にアップロードされました", @@ -233,7 +297,15 @@ }, "theme": "テーマ", "builtInsLabel": "組み込みテーマ", - "pluginsLabel": "プラグイン" + "pluginsLabel": "プラグイン", + "dateFormat": { + "label": "日付フォーマット" + }, + "timeFormat": { + "label": "時刻フォーマット", + "twelveHour": "12時間表記", + "twentyFourHour": "24時間表記" + } }, "files": { "copy": "コピー", @@ -275,11 +347,31 @@ "name": "名前", "selectAnIcon": "アイコンを選択してください", "pleaseInputYourOpenAIKey": "OpenAI キーを入力してください" + }, + "shortcuts": { + "shortcutsLabel": "ショートカット", + "command": "コマンド", + "keyBinding": "キーバインディング", + "addNewCommand": "新しいコマンドを追加", + "resetToDefault": "キーバインディングをデフォルトに戻す" + }, + "mobile": { + "username": "ユーザー名", + "usernameEmptyError": "ユーザー名は空白にはできません", + "pushNotifications": "プッシュ通知", + "support": "サポート", + "privacyPolicy": "プライバシーポリシー", + "userAgreement": "ユーザー同意", + "termsAndConditions": "利用規約", + "version": "バージョン" } }, "grid": { "deleteView": "このビューを削除してもよろしいですか?", "createView": "新しい", + "title": { + "placeholder": "Untitled" + }, "settings": { "filter": "絞り込み", "sort": "選別", From 211559ed6e02a724349756b40d43e90fbd655072 Mon Sep 17 00:00:00 2001 From: HY <17506755+rhybs@users.noreply.github.com> Date: Sun, 18 Feb 2024 00:40:22 +0800 Subject: [PATCH 40/50] chore: Update translations for Traditional Chinese (#4662) * chore: update zh-TW translations * inlang: update translations * inlang: update translations * chore: update zh tw * chore: update zh tw --------- Co-authored-by: nathan --- frontend/resources/translations/zh-TW.json | 91 +++++++++++++--------- 1 file changed, 54 insertions(+), 37 deletions(-) diff --git a/frontend/resources/translations/zh-TW.json b/frontend/resources/translations/zh-TW.json index 673ddc56b7d72..d554a4641304a 100644 --- a/frontend/resources/translations/zh-TW.json +++ b/frontend/resources/translations/zh-TW.json @@ -42,11 +42,11 @@ "passwordHint": "密碼", "dontHaveAnAccount": "還沒有帳號?", "repeatPasswordEmptyError": "確認密碼不能為空", - "unmatchedPasswordError": "確認密碼與密碼不符", + "unmatchedPasswordError": "密碼重複輸入不一致", "syncPromptMessage": "同步資料可能需要一些時間。請不要關閉此頁面", "or": "或", "LogInWithGoogle": "使用 Google 登入", - "LogInWithGithub": "使用 GitHub 登入", + "LogInWithGithub": "使用 Github 登入", "LogInWithDiscord": "使用 Discord 登入", "signInWith": "透過以下方式登入:" }, @@ -60,6 +60,8 @@ "failedToLoad": "出了些問題!無法載入工作區。請嘗試關閉 AppFlowy 的任何開啟執行個體,然後再試一次。", "errorActions": { "reportIssue": "回報問題", + "reportIssueOnGithub": "在 Github 提交 issue", + "exportLogFiles": "匯出日誌記錄檔案", "reachOut": "在 Discord 上聯絡我們" } }, @@ -97,18 +99,18 @@ "copyLink": "複製連結" }, "blankPageTitle": "空白頁面", - "newPageText": "新頁面", - "newDocumentText": "新文件", - "newGridText": "新網格", - "newCalendarText": "新日曆", - "newBoardText": "新看板", + "newPageText": "新增頁面", + "newDocumentText": "新增文件", + "newGridText": "新增網格", + "newCalendarText": "新增日曆", + "newBoardText": "新增看板", "trash": { "text": "垃圾桶", "restoreAll": "全部還原", "deleteAll": "全部刪除", "pageHeader": { "fileName": "檔案名稱", - "lastModified": "最後修改時間", + "lastModified": "最近修改時間", "created": "建立時間" }, "confirmDeleteAll": { @@ -250,9 +252,9 @@ "failedMsg": "請確認您已在瀏覽器中完成登入程序。" }, "google": { - "title": "GOOGLE 登入", + "title": "Google 帳號登入", "instruction1": "若要匯入您的 Google 聯絡人,您必須透過瀏覽器授權此應用程式。", - "instruction2": "點選圖示或選取文字以複製程式碼:", + "instruction2": "點選圖示或選取文字以複製程式碼到剪貼簿:", "instruction3": "前往下列網址,並輸入上述程式碼:", "instruction4": "完成註冊後,請點選下方按鈕:" } @@ -284,12 +286,12 @@ "cloudSupabaseAnonKey": "Supabase 匿名金鑰", "cloudSupabaseAnonKeyCanNotBeEmpty": "如果 Supabase 網址不為空,則匿名金鑰不得為空", "cloudAppFlowy": "AppFlowy 雲端測試版 (Beta)", - "cloudAppFlowySelfHost": "AppFlowy 雲端自建", + "cloudAppFlowySelfHost": "自架 AppFlowy 雲端伺服器", "appFlowyCloudUrlCanNotBeEmpty": "雲端網址不能為空", "clickToCopy": "點選以複製", "selfHostStart": "若您尚未設定伺服器,請參閱", "selfHostContent": "文件", - "selfHostEnd": "以瞭解如何自建您的伺服器", + "selfHostEnd": "有關如何自架個人伺服器的指南", "cloudURLHint": "請輸入您伺服器的基礎網址", "cloudWSURL": "Websocket 網址", "cloudWSURLHint": "請輸入您伺服器的 Websocket 位址", @@ -300,7 +302,7 @@ "inputEncryptPrompt": "請為 {username} 輸入您的加密金鑰", "clickToCopySecret": "點選以複製金鑰", "configServerSetting": "設定您的伺服器選項", - "configServerGuide": "在選擇「快速開始」之後,請前往「設定」並選擇「雲端設定」,以進行自託管伺服器的設定。", + "configServerGuide": "在選擇「快速開始」之後,請前往「設定」並選擇「雲端設定」,以進行自架伺服器的設定。", "inputTextFieldHint": "您的金鑰", "historicalUserList": "使用者登入歷史", "historicalUserListTooltip": "此列表顯示您的匿名帳號。您可以點選帳號以檢視其詳細資訊。透過點選「開始使用」按鈕來建立匿名帳號", @@ -328,15 +330,15 @@ "themeMode": { "label": "主題模式", "light": "亮色模式", - "dark": "暗色模式", + "dark": "深色模式", "system": "依照系統設定" }, "documentSettings": { "cursorColor": "文件游標顏色", "selectionColor": "文件選取顏色", - "hexEmptyError": "Hex 顏色不能為空", - "hexLengthError": "Hex 值必須為 6 位數", - "hexInvalidError": "無效的 Hex 值", + "hexEmptyError": "十六進位色碼不能為空", + "hexLengthError": "十六進位值必須為 6 位數", + "hexInvalidError": "無效的十六進位值", "opacityEmptyError": "不透明度不能為空", "opacityRangeError": "不透明度必須在 1 到 100 之間", "app": "App", @@ -346,15 +348,15 @@ "layoutDirection": { "label": "版面配置方向", "hint": "控制螢幕上內容的流向,從左到右或從右到左。", - "ltr": "LTR", - "rtl": "RTL" + "ltr": "從左到右", + "rtl": "從右到左" }, "textDirection": { "label": "預設文字方向", - "hint": "指定文字應從左邊或右邊開始為預設。", - "ltr": "LTR", - "rtl": "RTL", - "auto": "AUTO", + "hint": "預設指定文字應從左邊或右邊開始。", + "ltr": "從左到右", + "rtl": "從右到左", + "auto": "自動調整", "fallback": "與版面配置方向相同" }, "themeUpload": { @@ -476,8 +478,8 @@ "deleteFilter": "刪除篩選器", "filterBy": "以……篩選", "typeAValue": "輸入一個值……", - "layout": "配置", - "databaseLayout": "配置", + "layout": "版面佈局", + "databaseLayout": "版面佈局", "editView": "編輯檢視", "boardSettings": "看板設定", "calendarSettings": "日曆設定", @@ -614,7 +616,7 @@ "textPlaceholder": "空白", "copyProperty": "已將屬性複製到剪貼簿", "count": "計數", - "newRow": "新行", + "newRow": "新增行", "action": "操作", "add": "點選以在下方新增", "drag": "拖曳以移動", @@ -651,7 +653,16 @@ "showComplete": "顯示所有任務" }, "menuName": "網格", - "referencedGridPrefix": "檢視" + "referencedGridPrefix": "檢視", + "calculate": "計算", + "calculationTypeLabel": { + "none": "無", + "average": "平均", + "max": "最大值", + "median": "中間值", + "min": "最小值", + "sum": "總和" + } }, "document": { "menuName": "文件", @@ -688,7 +699,7 @@ "autoGeneratorMenuItemName": "OpenAI 寫手", "autoGeneratorTitleName": "OpenAI:讓 AI 撰寫任何內容……", "autoGeneratorLearnMore": "瞭解更多", - "autoGeneratorGenerate": "生成", + "autoGeneratorGenerate": "產生", "autoGeneratorHintText": "問 OpenAI……", "autoGeneratorCantGetOpenAIKey": "無法取得 OpenAI 金鑰", "autoGeneratorRewrite": "改寫", @@ -697,7 +708,7 @@ "smartEditFixSpelling": "修正拼寫", "warning": "⚠️ AI 的回覆可能不準確或具有誤導性。", "smartEditSummarize": "總結", - "smartEditImproveWriting": "提高寫作水平", + "smartEditImproveWriting": "提高寫作水準", "smartEditMakeLonger": "做得更長", "smartEditCouldNotFetchResult": "無法取得 OpenAI 的結果", "smartEditCouldNotFetchKey": "無法取得 OpenAI 金鑰", @@ -864,7 +875,9 @@ "page": { "label": "連結到頁面", "tooltip": "點選以開啟頁面" - } + }, + "deleted": "已刪除", + "deletedContent": "該內容不存在或已經刪除" }, "toolbar": { "resetToDefaultFont": "重設為預設字型" @@ -1035,6 +1048,7 @@ "timeFormat": "時間格式", "clearDate": "清除日期", "reminderLabel": "提醒", + "selectReminder": "選擇提醒", "reminderOptions": { "none": "無", "atTimeOfEvent": "事件發生時", @@ -1044,8 +1058,10 @@ "thirtyMinsBefore": "提前 30 分鐘", "oneHourBefore": "提前 1 小時", "twoHoursBefore": "提前 2 小時", + "onDayOfEvent": "活動當天", "oneDayBefore": "提前 1 天", "twoDaysBefore": "提前 2 天", + "oneWeekBefore": "一週前", "custom": "自訂" } }, @@ -1098,7 +1114,7 @@ }, "error": { "weAreSorry": "我們很抱歉", - "loadingViewError": "我們在載入此視圖時遇到問題。請檢查您的網路連線,重新整理應用程式,並且如果問題持續,請隨時聯絡我們的團隊。" + "loadingViewError": "我們在載入此檢視時遇到問題。請檢查您的網路連線,重新整理應用程式,並且如果問題持續,請隨時聯絡我們的團隊。" }, "editor": { "bold": "粗體", @@ -1209,11 +1225,11 @@ "colAddAfter": "在後面新增", "rowAddAfter": "在後面新增", "colRemove": "移除", - "rowRemove": "移除", - "colDuplicate": "複製", - "rowDuplicate": "複製", - "colClear": "清除內容", - "rowClear": "清除內容", + "rowRemove": "移除行", + "colDuplicate": "複製列", + "rowDuplicate": "複製行", + "colClear": "清除本列內容", + "rowClear": "清除本行內容", "slashPlaceHolder": "輸入 '/' 以插入區塊,或開始輸入", "typeSomething": "輸入一些東西...", "toggleListShortForm": "切換", @@ -1243,5 +1259,6 @@ "date": "日期", "addField": "新增欄位", "userIcon": "使用者圖示" - } + }, + "noLogFiles": "這裡沒有日誌記錄檔案" } From 39588a4731012377d17329a0d85cc15db5094555 Mon Sep 17 00:00:00 2001 From: Arcesilas Date: Sat, 17 Feb 2024 17:44:39 +0100 Subject: [PATCH 41/50] chore: update translations (#4664) Co-authored-by: nathan --- frontend/resources/translations/en.json | 2 +- frontend/resources/translations/fr-CA.json | 836 +++++++++++++++++++-- frontend/resources/translations/fr-FR.json | 226 +++--- 3 files changed, 871 insertions(+), 193 deletions(-) diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index e94485a64e823..70fb0fffc1b5b 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -654,7 +654,7 @@ "panelTitle": "Select an option or create one", "searchOption": "Search for an option", "searchOrCreateOption": "Search or create an option...", - "createNew": "Create an new", + "createNew": "Create a new", "orSelectOne": "Or select an option", "typeANewOption": "Type a new option", "tagName": "Tag name" diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 6f4d740ba024f..68e9068061c16 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -2,6 +2,7 @@ "appName": "AppFlowy", "defaultUsername": "Moi", "welcomeText": "Bienvenue sur @:appName", + "welcomeTo": "Bienvenue à", "githubStarText": "Étoiler sur GitHub", "subscribeNewsletterText": "Abonnez-vous à notre courriel", "letsGoButtonText": "Allons-y", @@ -12,41 +13,64 @@ "addBelowTooltip": "Cliquez pour ajouter ci-dessous", "addAboveCmd": "Alt+clic", "addAboveMacCmd": "Option+clic", - "addAboveTooltip": "à ajouter au dessus" + "addAboveTooltip": "à ajouter au dessus", + "dragTooltip": "Glisser pour déplacer", + "openMenuTooltip": "Cliquez pour ouvrir le menu" }, "signUp": { "buttonText": "S'inscrire", "title": "S'inscrire à @:appName", "getStartedText": "Commencer", "emptyPasswordError": "Vous n'avez pas saisi votre mot de passe", - "repeatPasswordEmptyError": "Mot de passe ne doit pas être vide", + "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", "alreadyHaveAnAccount": "Avez-vous déjà un compte?", - "emailHint": "courriel", + "emailHint": "Courriel", "passwordHint": "Mot de passe", - "repeatPasswordHint": "Ressaisir votre mot de passe" + "repeatPasswordHint": "Ressaisir votre mot de passe", + "signUpWith": "Se connecter avec:" }, "signIn": { "loginTitle": "Connexion à @:appName", "loginButtonText": "Connexion", + "loginStartWithAnonymous": "Lancer avec une session anonyme", + "continueAnonymousUser": "Continuer avec une session anonyme", "buttonText": "Se connecter", + "signingInText": "Connexion en cours...", "forgotPassword": "Mot de passe oublié?", - "emailHint": "courriel", + "emailHint": "Courriel", "passwordHint": "Mot de passe", - "dontHaveAnAccount": "Vous n'avez pas de compte?", - "repeatPasswordEmptyError": "Mot de passe ne doit pas être vide", + "dontHaveAnAccount": "Vous n'avez pas encore de compte?", + "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", + "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.", + "or": "OU", + "LogInWithGoogle": "Se connecter avec Google", + "LogInWithGithub": "Se connecter avec Github", + "LogInWithDiscord": "Se connecter avec Discord", + "signInWith": "Se connecter avec:", "loginAsGuestButtonText": "Commencer" }, "workspace": { + "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", "hint": "Espace de travail", - "notFoundError": "Espace de travail introuvable" + "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.", + "errorActions": { + "reportIssue": "Signaler un problème", + "reportIssueOnGithub": "Signaler un bug sur Github", + "exportLogFiles": "Exporter les logs", + "reachOut": "Contactez-nous sur Discord" + } }, "shareAction": { "buttonText": "Partager", "workInProgress": "Bientôt disponible", "markdown": "Markdown", + "csv": "CSV", "copyLink": "Copier le lien" }, "moreAction": { @@ -68,10 +92,19 @@ "rename": "Renommer", "delete": "Supprimer", "duplicate": "Dupliquer", - "openNewTab": "Ouvrir dans un nouvel onglet" + "unfavorite": "Retirer des favoris", + "favorite": "Ajouter aux favoris", + "openNewTab": "Ouvrir dans un nouvel onglet", + "moveTo": "Déplacer vers", + "addToFavorites": "Ajouter aux Favoris", + "copyLink": "Copier le lien" }, "blankPageTitle": "Page vierge", "newPageText": "Nouvelle page", + "newDocumentText": "Nouveau document", + "newGridText": "Nouvelle grille", + "newCalendarText": "Nouveau calendrier", + "newBoardText": "Nouveau tableau", "trash": { "text": "Corbeille", "restoreAll": "Tout récupérer", @@ -88,10 +121,17 @@ "confirmRestoreAll": { "title": "Êtes-vous sûr de vouloir restaurer toutes les pages dans la corbeille ?", "caption": "Cette action ne peut pas être annulée." + }, + "mobile": { + "actions": "Actions de la corbeille", + "empty": "La corbeille est vide", + "emptyDescription": "Vous n'avez aucun fichier supprimé", + "isDeleted": "a été supprimé", + "isRestored": "a été restauré" } }, "deletePagePrompt": { - "text": "Cette page est dans la corbeille", + "text": "Cette page se trouve dans la corbeille", "restore": "Récupérer la page", "deletePermanent": "Supprimer définitivement" }, @@ -103,16 +143,18 @@ "markdown": "Réduction", "debug": { "name": "Infos du système", - "success": "Info copié!", - "fail": "Impossible de copier l'info" + "success": "Informations de débogage copiées dans le presse-papiers !", + "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, "feedback": "Retour" }, "menuAppHeader": { - "addPageTooltip": "Ajouter une page", + "moreButtonToolTip": "Supprimer, renommer et plus...", + "addPageTooltip": "Ajouter rapidement une page à l'intérieur", "defaultNewPageName": "Sans titre", "renameDialog": "Renommer" }, + "noPagesInside": "Aucune page à l'intérieur", "toolbar": { "undo": "Annuler", "redo": "Rétablir", @@ -135,50 +177,70 @@ "lightMode": "Passer en mode clair", "darkMode": "Passer en mode sombre", "openAsPage": "Ouvrir en tant que page", - "addNewRow": "Ajouter une nouvelle ligne", + "addNewRow": "Ajouter une ligne", "openMenu": "Cliquez pour ouvrir le menu", "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" + "addBlockBelow": "Ajouter un bloc ci-dessous", + "urlLaunchAccessory": "Ouvrir dans le navigateur", + "urlCopyAccessory": "Copier l'URL" }, "sideBar": { - "closeSidebar": "Close sidebar", - "openSidebar": "Open sidebar" + "closeSidebar": "Fermer le menu latéral", + "openSidebar": "Ouvrir le menu latéral", + "personal": "Personnel", + "favorites": "Favoris", + "clickToHidePersonal": "Cliquez pour cacher la section personnelle", + "clickToHideFavorites": "Cliquez pour cacher la section favorite", + "addAPage": "Ajouter une page", + "recent": "Récent" }, "notifications": { "export": { - "markdown": "Note exportée vers Markdown", + "markdown": "Note exportée en Markdown", "path": "Documents/fluide" } }, "contactsPage": { "title": "Contacts", - "whatsHappening": "Quoi de neuf?", + "whatsHappening": "Que se passe-t-il cette semaine ?", "addContact": "Ajouter un contact", "editContact": "Modifier le contact" }, "button": { + "ok": "OK", "done": "Fait", + "cancel": "Annuler", "signIn": "Se connecter", "signOut": "Se déconnecter", "complete": "Achevé", "save": "Sauvegarder", "generate": "Générer", "esc": "ESC", - "keep": "Donjon", + "keep": "Garder", "tryAgain": "Essayer à nouveau", "discard": "Jeter", "replace": "Remplacer", "insertBelow": "Insérer ci-dessous", + "insertAbove": "Insérer ci-dessus", "upload": "Télécharger", "edit": "Modifier", "delete": "Supprimer", "duplicate": "Dupliquer", "putback": "Remettre", + "update": "Mettre à jour", + "share": "Partager", + "removeFromFavorites": "Retirer des favoris", + "addToFavorites": "Ajouter aux favoris", + "rename": "Renommer", + "helpCenter": "Centre d'aide", + "add": "Ajouter", + "yes": "Oui", "Done": "Fait", "Cancel": "Annuler", - "OK": "OK" + "OK": "OK", + "tryAGain": "Réessayer" }, "label": { "welcome": "Bienvenue!", @@ -190,7 +252,7 @@ "oAuth": { "err": { "failedTitle": "Incapable de se connecter à votre compte.", - "failedMsg": "SVP vous assurrez d'avoir complèté le processus d'enregistrement dans votre fureteur." + "failedMsg": "SVP assurez-vous d'avoir complèté le processus d'enregistrement dans votre fureteur." }, "google": { "title": "S'identifier avec Google", @@ -206,26 +268,107 @@ "appearance": "Apparence", "language": "Langue", "user": "Utilisateur", - "files": "Des dossiers", + "files": "Dossiers", + "notifications": "Notifications", "open": "Ouvrir les paramètres", + "logout": "Se 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", + "enableSync": "Activer la synchronisation", + "enableEncrypt": "Chiffrer les données", + "cloudURL": "URL de base", + "invalidCloudURLScheme": "Schéma invalide", + "cloudServerType": "Serveur cloud", + "cloudServerTypeTip": "Veuillez noter qu'il est possible que votre compte actuel soit déconnecté après avoir changé de serveur cloud.", + "cloudLocal": "Local", + "cloudSupabase": "Supabase", + "cloudSupabaseUrl": "URL de Supabase", + "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", + "cloudSupabaseAnonKey": "Clé anonyme Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", + "cloudAppFlowy": "AppFlowy Cloud Bêta", + "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", + "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", + "clickToCopy": "Cliquez pour copier", + "selfHostStart": "Si vous n'avez pas de serveur, veuillez vous référer au", + "selfHostContent": "document", + "selfHostEnd": "pour obtenir des conseils sur la façon d'auto-héberger votre propre serveur", + "cloudURLHint": "Saisissez l'URL de base de votre serveur", + "cloudWSURL": "URL du websocket", + "cloudWSURLHint": "Saisissez l'adresse websocket de votre serveur", + "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", + "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 »", + "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", + "importingAppFlowyDataTip": "L'importation des données est en cours. Veuillez ne pas fermer l'application", + "importAppFlowyDataDescription": "Copiez les données d'un dossier de données AppFlowy externe et importez-les dans le dossier de données AppFlowy actuel", + "importSuccess": "Importation réussie du dossier de données AppFlowy", + "importFailed": "L'importation du dossier de données AppFlowy a échoué", + "importGuide": "Pour plus de détails, veuillez consulter le document référencé", "supabaseSetting": "Paramètre Supbase" }, + "notifications": { + "enableNotifications": { + "label": "Activer les notifications", + "hint": "Désactivez-la pour empêcher l'affichage des notifications locales." + } + }, "appearance": { + "resetSetting": "Réinitialiser ce paramètre", "fontFamily": { "label": "Famille de polices", "search": "Recherche" }, "themeMode": { - "label": "Theme Mode", + "label": " Mode du Thème", "light": "Mode clair", "dark": "Mode sombre", - "system": "Adapt to System" + "system": "S'adapter au système" + }, + "documentSettings": { + "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", + "hexInvalidError": "Valeur hexadécimale invalide", + "opacityEmptyError": "L'opacité ne peut pas être vide", + "opacityRangeError": "L'opacité doit être comprise entre 1 et 100", + "app": "Application", + "flowy": "Flowy", + "apply": "Appliquer" + }, + "layoutDirection": { + "label": "Orientation de la mise en page", + "hint": "Contrôlez l'orientation du contenu sur votre écran, de gauche à droite ou de droite à gauche.", + "ltr": "LTR", + "rtl": "RTL" + }, + "textDirection": { + "label": "Direction du texte par défaut", + "hint": "Spécifiez si le texte doit commencer à gauche ou à droite par défaut.", + "ltr": "LTR", + "rtl": "RTL", + "auto": "AUTO", + "fallback": "Identique au sens de mise en page" }, "themeUpload": { - "button": "Télécharger", - "description": "Téléchargez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", + "button": "Téléverser", + "uploadTheme": "Téléverser le thème", + "description": "Téléversez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", "loading": "Veuillez patienter pendant que nous validons et téléchargeons votre thème...", - "uploadSuccess": "Votre thème a été téléchargé avec succès", + "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 : {}", @@ -233,7 +376,21 @@ }, "theme": "Thème", "builtInsLabel": "Thèmes intégrés", - "pluginsLabel": "Plugins" + "pluginsLabel": "Plugins", + "dateFormat": { + "label": "Format de la date", + "local": "Local", + "us": "US", + "iso": "ISO", + "friendly": "Convivial", + "dmy": "J/M/A" + }, + "timeFormat": { + "label": "Format de l'heure", + "twelveHour": "Douze heures", + "twentyFourHour": "Vingt-quatre heures" + }, + "showNamingDialogWhenCreatingPage": "Afficher la boîte de dialogue de nommage lors de la création d'une page" }, "files": { "copy": "Copie", @@ -253,7 +410,7 @@ "open": "Ouvrir", "openFolder": "Ouvrir un dossier existant", "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier AppFlowy existant", - "folderHintText": "nom de dossier", + "folderHintText": "Nom de dossier", "location": "Création d'un nouveau dossier", "locationDesc": "Choisissez un nom pour votre dossier de données AppFlowy", "browser": "Parcourir", @@ -263,25 +420,60 @@ "locationCannotBeEmpty": "Le chemin ne peut pas être vide", "pathCopiedSnackbar": "Chemin de stockage des fichiers copié dans le presse-papier !", "changeLocationTooltips": "Changer le répertoire de données", - "change": "Changement", + "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'exportation du fichier !", + "exportFileSuccess": "Exporter le fichier avec succès !", + "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter" }, "user": { "name": "Nom", + "email": "Courriel", + "tooltipSelectIcon": "Sélectionner l'icône", "selectAnIcon": "Sélectionnez une icône", - "pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI" + "pleaseInputYourOpenAIKey": "Veuillez entrer votre clé OpenAI", + "pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI", + "clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel" + }, + "shortcuts": { + "shortcutsLabel": "Raccourcis", + "command": "Commande", + "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}", + "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" + }, + "mobile": { + "personalInfo": "Informations personnelles", + "username": "Nom d'utilisateur", + "usernameEmptyError": "Le nom d'utilisateur ne peut pas être vide", + "about": "À propos", + "pushNotifications": "Notifications push", + "support": "Support", + "joinDiscord": "Rejoignez-nous sur Discord", + "privacyPolicy": "Politique de Confidentialité", + "userAgreement": "Accord de l'utilisateur", + "termsAndConditions": "Termes et conditions", + "userprofileError": "Échec du chargement du profil utilisateur", + "userprofileErrorDescription": "Veuillez essayer de vous déconnecter et de vous reconnecter pour vérifier si le problème persiste.", + "selectLayout": "Sélectionner la mise en page", + "selectStartingDay": "Sélectionnez le jour de début", + "version": "Version" } }, "grid": { "deleteView": "Voulez-vous vraiment supprimer cette vue ?", "createView": "Nouveau", + "title": { + "placeholder": "Sans titre" + }, "settings": { - "filter": "Filtre", + "filter": "Filtrer", "sort": "Trier", "sortBy": "Trier par", "properties": "Propriétés", @@ -292,35 +484,47 @@ "filterBy": "Filtrer par...", "typeAValue": "Tapez une valeur...", "layout": "Mise en page", - "databaseLayout": "Mise en page" + "databaseLayout": "Mise en page", + "viewList": { + "zero": "0 vue", + "one": "{count} vue", + "other": "{count} vues" + }, + "editView": "Modifier vue", + "boardSettings": "Paramètres du tableau", + "calendarSettings": "Paramètres du calendrier", + "createView": "Nouvelle vue", + "duplicateView": "Dupliquer la vue", + "deleteView": "Supprimer la vue", + "numberOfVisibleFields": "{} affiché(s)" }, "textFilter": { "contains": "Contient", "doesNotContain": "Ne contient pas", "endsWith": "Se termine par", - "startWith": "Commence avec", + "startWith": "Commence par", "is": "Est", "isNot": "N'est pas", "isEmpty": "Est vide", "isNotEmpty": "N'est pas vide", "choicechipPrefix": { "isNot": "Pas", - "startWith": "Commence avec", + "startWith": "Commence par", "endWith": "Se termine par", "isEmpty": "est vide", "isNotEmpty": "n'est pas vide" } }, "checkboxFilter": { - "isChecked": "Vérifié", + "isChecked": "Coché", "isUnchecked": "Décoché", "choicechipPrefix": { "is": "est" } }, "checklistFilter": { - "isComplete": "est complet", - "isIncomplted": "est incomplet" + "isComplete": "fait", + "isIncomplted": "pas fait" }, "singleSelectOptionFilter": { "is": "Est", @@ -334,8 +538,19 @@ "isEmpty": "Est vide", "isNotEmpty": "N'est pas vide" }, + "dateFilter": { + "is": "Est", + "before": "Est avant", + "after": "Est après", + "onOrBefore": "Est le ou avant", + "onOrAfter": "Est le ou après", + "between": "Est entre", + "empty": "Est vide", + "notEmpty": "N'est pas vide" + }, "field": { "hide": "Cacher", + "show": "Afficher", "insertLeft": "Insérer à gauche", "insertRight": "Insérer à droite", "duplicate": "Dupliquer", @@ -343,46 +558,81 @@ "textFieldName": "Texte", "checkboxFieldName": "Case à cocher", "dateFieldName": "Date", - "updatedAtFieldName": "Heure de la dernière modification", - "createdAtFieldName": "Temps créé", - "numberFieldName": "Nombres", + "updatedAtFieldName": "Dernière modification", + "createdAtFieldName": "Créé le", + "numberFieldName": "Nombre", "singleSelectFieldName": "Sélectionner", "multiSelectFieldName": "Sélection multiple", "urlFieldName": "URL", "checklistFieldName": "Liste de contrôle", "numberFormat": "Format de nombre", - "dateFormat": "Format de date", - "includeTime": "Inclure le temps", - "dateFormatFriendly": "Année mois jour", - "dateFormatISO": "Année mois jour", - "dateFormatLocal": "Année mois jour", - "dateFormatUS": "Année mois jour", - "dateFormatDayMonthYear": "Jour mois année", + "dateFormat": "Format de la date", + "includeTime": "Inclure l'heure", + "isRange": "Date de fin", + "dateFormatFriendly": "Mois Jour, Année", + "dateFormatISO": "Année-Mois-Jour", + "dateFormatLocal": "Mois/Jour/Année", + "dateFormatUS": "Année/Mois/Jour", + "dateFormatDayMonthYear": "Jour/Mois/Année", "timeFormat": "Format de l'heure", "invalidTimeFormat": "Format invalide", "timeFormatTwelveHour": "12 heures", "timeFormatTwentyFourHour": "24 heures", + "clearDate": "Effacer la date", + "dateTime": "Date et heure", + "startDateTime": "Date et heure de début", + "endDateTime": "Date et heure de fin", + "failedToLoadDate": "Échec du chargement de la valeur de la date", + "selectTime": "Sélectionnez l'heure", + "selectDate": "Sélectionner une date", + "visibility": "Visibilité", + "propertyType": "Type de propriété", "addSelectOption": "Ajouter une option", + "typeANewOption": "Saisissez une nouvelle option", "optionTitle": "Choix", - "addOption": "Ajouter une option", + "addOption": "Ajouter un choix", "editProperty": "Modifier la propriété", "newProperty": "Nouvelle propriété", - "deleteFieldPromptMessage": "Es-tu sûr? Cette propriété sera supprimée" + "deleteFieldPromptMessage": "Vous voulez supprimer cette propriété ?", + "newColumn": "Nouvelle colonne", + "format": "Format", + "reminderOnDateTooltip": "Cette cellule a un rappel programmé" + }, + "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" + }, + "hideHiddenFields": { + "one": "Cacher {count} champ caché", + "many": "Cacher {count} champs masqués", + "other": "Cacher {count} champs masqués" + } }, "sort": { "ascending": "Ascendant", "descending": "Descendant", + "deleteAllSorts": "Supprimer tous les tris", "addSort": "Ajouter un tri", "deleteSort": "Supprimer le tri" }, "row": { "duplicate": "Dupliquer", "delete": "Supprimer", + "titlePlaceholder": "Sans titre", "textPlaceholder": "Vide", "copyProperty": "Propriété copiée dans le presse-papiers", - "count": "Compter", + "count": "Compte", "newRow": "Nouvelle ligne", - "action": "Action" + "action": "Action", + "add": "Cliquez sur ajouter ci-dessous", + "drag": "Glisser pour déplacer", + "dragAndClick": "Faites glisser pour déplacer, cliquez pour ouvrir le menu", + "insertRecordAbove": "Insérer l'enregistrement ci-dessus", + "insertRecordBelow": "Insérer l'enregistrement ci-dessous" }, "selectOption": { "create": "Créer", @@ -391,25 +641,43 @@ "lightPinkColor": "Rose clair", "orangeColor": "Orange", "yellowColor": "Jaune", - "limeColor": "Chaux", + "limeColor": "Lime", "greenColor": "Vert", - "aquaColor": "Aqua", + "aquaColor": "Turquoise", "blueColor": "Bleu", - "deleteTag": "Supprimer la balise", + "deleteTag": "Supprimer l'étiquette", "colorPanelTitle": "Couleurs", "panelTitle": "Sélectionnez une option ou créez-en une", - "searchOption": "Rechercher une option" + "searchOption": "Rechercher une option", + "searchOrCreateOption": "Rechercher ou créer une option...", + "createNew": "Créer une nouvelle", + "orSelectOne": "Ou sélectionnez une option", + "typeANewOption": "Saisissez une nouvelle option", + "tagName": "Nom de l'étiquette" }, "checklist": { - "addNew": "Ajouter un article" + "taskHint": "Description de la tâche", + "addNew": "Ajouter un élément", + "submitNewTask": "Créer", + "hideComplete": "Cacher les tâches terminées", + "showComplete": "Afficher toutes les tâches" }, "menuName": "Grille", - "referencedGridPrefix": "Vue" + "referencedGridPrefix": "Vue", + "calculate": "Calculer", + "calculationTypeLabel": { + "none": "Aucun", + "average": "Moyenne", + "max": "Maximum", + "median": "Médiane", + "min": "Minimum", + "sum": "Somme" + } }, "document": { "menuName": "Document", "date": { - "timeHintTextInTwelveHour": "13h00", + "timeHintTextInTwelveHour": "01:00 PM", "timeHintTextInTwentyFourHour": "13:00" }, "slashMenu": { @@ -424,22 +692,27 @@ "calendar": { "selectACalendarToLinkTo": "Sélectionnez un calendrier à lier", "createANewCalendar": "Créer un nouveau calendrier" + }, + "document": { + "selectADocumentToLinkTo": "Sélectionnez un Document vers lequel créer un lien" } }, "selectionMenu": { - "outline": "Contour" + "outline": "Contour", + "codeBlock": "Bloc de code" }, "plugins": { - "referencedBoard": "Conseil référencé", + "referencedBoard": "Tableau référencé", "referencedGrid": "Grille référencée", "referencedCalendar": "Calendrier référencé", + "referencedDocument": "Document référencé", "autoGeneratorMenuItemName": "Rédacteur OpenAI", - "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire n'importe quoi...", + "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...", "autoGeneratorLearnMore": "Apprendre encore plus", "autoGeneratorGenerate": "Générer", "autoGeneratorHintText": "Demandez à OpenAI...", "autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé OpenAI", - "autoGeneratorRewrite": "Récrire", + "autoGeneratorRewrite": "Réécrire", "smartEdit": "Assistants IA", "openAI": "OpenAI", "smartEditFixSpelling": "Corriger l'orthographe", @@ -452,9 +725,15 @@ "smartEditDisabled": "Connectez OpenAI dans les paramètres", "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", "createInlineMathEquation": "Créer une équation", + "fonts": "Polices", "toggleList": "Basculer la liste", + "quoteList": "Liste de citations", + "numberedList": "Liste numérotée", + "bulletedList": "Liste à puces", + "todoList": "Liste de tâches", + "callout": "Encadré", "cover": { - "changeCover": "Changer de couverture", + "changeCover": "Changer la couverture", "colors": "Couleurs", "images": "Images", "clearAll": "Tout effacer", @@ -474,10 +753,12 @@ "couldNotFetchImage": "Impossible de récupérer l'image", "imageSavingFailed": "Échec de l'enregistrement de l'image", "addIcon": "Ajouter une icône", + "changeIcon": "Changer l'icône", "coverRemoveAlert": "Il sera retiré de la couverture après sa suppression.", - "alertDialogConfirmation": "Es-tu sur de vouloir continuer?" + "alertDialogConfirmation": "Voulez-vous vraiment continuer?" }, "mathEquation": { + "name": "Équation mathématique", "addMathEquation": "Ajouter une équation mathématique", "editMathEquation": "Modifier l'équation mathématique" }, @@ -497,11 +778,39 @@ "defaultColor": "Défaut" }, "image": { - "copiedToPasteBoard": "Le lien de l'image a été copié dans le presse-papiers" + "copiedToPasteBoard": "Le lien de l'image a été copié dans le presse-papiers", + "addAnImage": "Ajouter une image" + }, + "urlPreview": { + "copiedToPasteBoard": "Le lien a été copié dans le presse-papier" }, "outline": { "addHeadingToCreateOutline": "Ajoutez des titres pour créer une table des matières." - } + }, + "table": { + "addAfter": "Ajouter après", + "addBefore": "Ajouter avant", + "delete": "Supprimer", + "clear": "Éffacer contenu", + "duplicate": "Dupliquer", + "bgColor": "Couleur de fond" + }, + "contextMenu": { + "copy": "Copier", + "cut": "Couper", + "paste": "Coller" + }, + "action": "Actions", + "database": { + "selectDataSource": "Sélectionnez la source de données", + "noDataSource": "Aucune source de données", + "selectADataSource": "Sélectionnez une source de données", + "toContinue": "pour continuer", + "newDatabase": "Nouvelle Base de données", + "linkToDatabase": "Lien vers la Base de données" + }, + "date": "Date", + "emoji": "Emoji" }, "textBlock": { "placeholder": "Tapez '/' pour les commandes" @@ -512,20 +821,44 @@ "imageBlock": { "placeholder": "Cliquez pour ajouter une image", "upload": { - "label": "Télécharger", - "placeholder": "Cliquez pour télécharger l'image" + "label": "Téléverser", + "placeholder": "Cliquez pour téléverser l'image" }, "url": { "label": "URL de l'image", "placeholder": "Entrez l'URL de l'image" }, + "ai": { + "label": "Générer une image à partir d'OpenAI", + "placeholder": "Veuillez saisir l'invite pour qu'OpenAI génère l'image" + }, + "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." + }, "support": "La limite de taille d'image est de 5 Mo. Formats pris en charge : JPEG, PNG, GIF, SVG", "error": { "invalidImage": "Image invalide", "invalidImageSize": "La taille de l'image doit être inférieure à 5 Mo", "invalidImageFormat": "Le format d'image n'est pas pris en charge. Formats pris en charge : JPEG, PNG, GIF, SVG", "invalidImageUrl": "URL d'image non valide" - } + }, + "embedLink": { + "label": "Lien intégré", + "placeholder": "Collez ou saisissez un lien d'image" + }, + "unsplash": { + "label": "Unsplash" + }, + "searchForAnImage": "Rechercher une image", + "pleaseInputYourOpenAIKey": "veuillez saisir votre clé OpenAI dans la page Paramètres", + "pleaseInputYourStabilityAIKey": "veuillez saisir votre clé Stability AI dans la page Paramètres", + "saveImageToGallery": "Enregistrer l'image", + "failedToAddImageToGallery": "Échec de l'ajout d'une image à la galerie", + "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" }, "codeBlock": { "language": { @@ -535,6 +868,9 @@ }, "inlineLink": { "placeholder": "Coller ou saisir un lien", + "openInNewTab": "Ouvrir dans un nouvel onglet", + "copyLink": "Copier le lien", + "removeLink": "Supprimer le lien", "url": { "label": "URL du lien", "placeholder": "Entrez l'URL du lien" @@ -543,35 +879,93 @@ "label": "Titre du lien", "placeholder": "Entrez le titre du lien" } + }, + "mention": { + "placeholder": "Mentionner une personne ou une page ou une date...", + "page": { + "label": "Lien vers la page", + "tooltip": "Cliquez pour ouvrir la page" + }, + "deleted": "Supprimé", + "deletedContent": "Ce document n'existe pas ou a été supprimé" + }, + "toolbar": { + "resetToDefaultFont": "Réinitialiser aux valeurs par défaut" + }, + "errorBlock": { + "theBlockIsNotSupported": "La version actuelle ne prend pas en charge ce bloc.", + "blockContentHasBeenCopied": "Le contenu du bloc a été copié." } }, "board": { "column": { - "createNewCard": "Nouveau" + "createNewCard": "Nouveau", + "renameGroupTooltip": "Appuyez pour renommer le groupe", + "createNewColumn": "Ajouter un nouveau groupe", + "addToColumnTopTooltip": "Ajouter une nouvelle carte en haut", + "addToColumnBottomTooltip": "Ajouter une nouvelle carte en bas", + "renameColumn": "Renommer", + "hideColumn": "Cacher", + "groupActions": "Actions de groupe", + "newGroup": "Nouveau groupe", + "deleteColumn": "Supprimer", + "deleteColumnConfirmation": "Cela supprimera ce groupe et toutes les cartes qu'il contient. \nÊtes-vous sûr de vouloir continuer?" + }, + "hiddenGroupSection": { + "sectionTitle": "Groupes cachés", + "collapseTooltip": "Cacher les groupes cachés", + "expandTooltip": "Afficher les groupes cachés" }, - "menuName": "Conseil", - "referencedBoardPrefix": "Vue" + "cardDetail": "Détail de la Carte", + "cardActions": "Actions des Cartes", + "cardDuplicated": "La carte a été dupliquée", + "cardDeleted": "La carte a été supprimée", + "showOnCard": "Afficher les détails de la carte", + "setting": "Paramètre", + "propertyName": "Nom de la propriété", + "menuName": "Tableau", + "showUngrouped": "Afficher les éléments non regroupés", + "ungroupedButtonText": "Non groupé", + "ungroupedButtonTooltip": "Contient des cartes qui n'appartiennent à aucun groupe", + "ungroupedItemsTitle": "Cliquez pour ajouter au tableau", + "groupBy": "Regrouper par", + "referencedBoardPrefix": "Vue", + "notesTooltip": "Notes à l'intérieur", + "mobile": { + "editURL": "Modifier l'URL", + "unhideGroup": "Afficher le groupe", + "unhideGroupContent": "Êtes-vous sûr de vouloir afficher ce groupe sur le tableau ?", + "faildToLoad": "Échec du chargement de la vue du tableau" + } }, "calendar": { "menuName": "Calendrier", "defaultNewCalendarTitle": "Sans titre", + "newEventButtonTooltip": "Ajouter un nouvel événement", "navigation": { "today": "Aujourd'hui", "jumpToday": "Aller à Aujourd'hui", - "previousMonth": "Le mois précédent", - "nextMonth": "Le mois prochain" + "previousMonth": "Mois précédent", + "nextMonth": "Mois prochain" + }, + "mobileEventScreen": { + "emptyTitle": "Pas d'événements", + "emptyBody": "Cliquez sur le bouton plus pour créer un événement à cette date." }, "settings": { "showWeekNumbers": "Afficher les numéros de semaine", "showWeekends": "Afficher les week-ends", "firstDayOfWeek": "Commencer la semaine le", "layoutDateField": "Calendrier de mise en page par", + "changeLayoutDateField": "Modifier le champ de mise en page", "noDateTitle": "Pas de date", + "noDateHint": "Les événements non planifiés s'afficheront ici", + "unscheduledEventsTitle": "Événements non planifiés", "clickToAdd": "Cliquez pour ajouter au calendrier", - "name": "Disposition du calendrier", - "noDateHint": "Les événements non planifiés s'afficheront ici" + "name": "Disposition du calendrier" }, - "referencedCalendarPrefix": "Vue" + "referencedCalendarPrefix": "Vue", + "quickJumpYear": "Sauter à" }, "errorDialog": { "title": "Erreur AppFlowy", @@ -594,5 +988,283 @@ "views": { "deleteContentTitle": "Voulez-vous vraiment supprimer le {pageType} ?", "deleteContentCaption": "si vous supprimez ce {pageType}, vous pouvez le restaurer à partir de la corbeille." - } + }, + "colors": { + "custom": "Personnalisé", + "default": "Défaut", + "red": "Rouge", + "orange": "Orange", + "yellow": "Jaune", + "green": "Vert", + "blue": "Bleu", + "purple": "Violet", + "pink": "Rose", + "brown": "Marron", + "gray": "Gris" + }, + "emoji": { + "emojiTab": "Émoji", + "search": "Chercher un émoji", + "noRecent": "Aucun émoji récent", + "noEmojiFound": "Aucun émoji trouvé", + "filter": "Filtrer", + "random": "Aléatoire", + "selectSkinTone": "Choisir le teint de la peau", + "remove": "Supprimer l'émoji", + "categories": { + "smileys": "Smileys & émoticônes", + "people": "Personnes & corps", + "animals": "Animaux & Nature", + "food": "Nourriture & Boisson", + "activities": "Activités", + "places": "Voyages & Lieux", + "objects": "Objets", + "symbols": "Symboles", + "flags": "Drapeaux", + "nature": "Nature", + "frequentlyUsed": "Fréquemment utilisés" + }, + "skinTone": { + "default": "Défaut", + "light": "Claire", + "mediumLight": "Moyennement claire", + "medium": "Moyen", + "mediumDark": "Moyennement foncé", + "dark": "Foncé" + } + }, + "inlineActions": { + "noResults": "Aucun résultat", + "pageReference": "Référence de page", + "docReference": "Référence de document", + "boardReference": "Référence du tableau", + "calReference": "Référence du calendrier", + "gridReference": "Référence de grille", + "date": "Date", + "reminder": { + "groupTitle": "Rappel", + "shortKeyword": "rappeler" + } + }, + "datePicker": { + "dateTimeFormatTooltip": "Modifier le format de la date et de l'heure dans les paramètres", + "dateFormat": "Format de date", + "includeTime": "Inclure l'heure", + "isRange": "Date de fin", + "timeFormat": "Format de l'heure", + "clearDate": "Effacer la date", + "reminderLabel": "Rappel", + "selectReminder": "Sélectionnez un rappel", + "reminderOptions": { + "none": "Aucun", + "atTimeOfEvent": "Heure de l'événement", + "fiveMinsBefore": "5 minutes avant", + "tenMinsBefore": "10 minutes avant", + "fifteenMinsBefore": "15 minutes avant", + "thirtyMinsBefore": "30 minutes avant", + "oneHourBefore": "1 heure avant", + "twoHoursBefore": "2 heures avant", + "onDayOfEvent": "Le jour de l'événement", + "oneDayBefore": "1 jour avant", + "twoDaysBefore": "2 jours avant", + "oneWeekBefore": "1 semaine avant", + "custom": "Personnalisé" + } + }, + "relativeDates": { + "yesterday": "Hier", + "today": "Aujourd'hui", + "tomorrow": "Demain", + "oneWeek": "1 semaine" + }, + "notificationHub": { + "title": "Notifications", + "mobile": { + "title": "Mises à jour" + }, + "emptyTitle": "Vous êtes à jour !", + "emptyBody": "Aucune notification ou action en attente. Profitez du calme.", + "tabs": { + "inbox": "Boîte de réception", + "upcoming": "A venir" + }, + "actions": { + "markAllRead": "Tout marquer comme lu", + "showAll": "Tous", + "showUnreads": "Non lu" + }, + "filters": { + "ascending": "Ascendant", + "descending": "Descendant", + "groupByDate": "Regrouper par date", + "showUnreadsOnly": "Afficher uniquement les éléments non lus", + "resetToDefault": "Réinitialiser aux valeurs par défaut" + } + }, + "reminderNotification": { + "title": "Rappel", + "message": "Pensez à vérifier cela avant d'oublier !", + "tooltipDelete": "Supprimer", + "tooltipMarkRead": "Marquer comme lu", + "tooltipMarkUnread": "Marquer comme non lu" + }, + "findAndReplace": { + "find": "Chercher", + "previousMatch": "Occurence précedente", + "nextMatch": "Prochaine occurence", + "close": "Fermer", + "replace": "Remplacer", + "replaceAll": "Tout remplacer", + "noResult": "Aucun résultat", + "caseSensitive": "Sensible à la casse" + }, + "error": { + "weAreSorry": "Nous sommes désolés", + "loadingViewError": "Nous rencontrons des difficultés pour charger cette vue. Veuillez vérifier votre connexion Internet, actualiser l'application et n'hésitez pas à contacter l'équipe si le problème persiste." + }, + "editor": { + "bold": "Gras", + "bulletedList": "Liste à puces", + "bulletedListShortForm": "Puces", + "checkbox": "Case à cocher", + "embedCode": "Code intégré", + "heading1": "H1", + "heading2": "H2", + "heading3": "H3", + "highlight": "Surligner", + "color": "Couleur", + "image": "Image", + "date": "Date", + "italic": "Italique", + "link": "Lien", + "numberedList": "Liste numérotée", + "numberedListShortForm": "Numéroté", + "quote": "Citation", + "strikethrough": "Barré", + "text": "Texte", + "underline": "Souligner", + "fontColorDefault": "Défaut", + "fontColorGray": "Gris", + "fontColorBrown": "Marron", + "fontColorOrange": "Orange", + "fontColorYellow": "Jaune", + "fontColorGreen": "Vert", + "fontColorBlue": "Bleu", + "fontColorPurple": "Violet", + "fontColorPink": "Rose", + "fontColorRed": "Rouge", + "backgroundColorDefault": "Fond par défaut", + "backgroundColorGray": "Fond gris", + "backgroundColorBrown": "Fond marron", + "backgroundColorOrange": "Fond orange", + "backgroundColorYellow": "Fond jaune", + "backgroundColorGreen": "Fond vert", + "backgroundColorBlue": "Fond bleu", + "backgroundColorPurple": "Fond violet", + "backgroundColorPink": "Fond rose", + "backgroundColorRed": "Fond rouge", + "done": "Fait", + "cancel": "Annuler", + "tint1": "Teinte 1", + "tint2": "Teinte 2", + "tint3": "Teinte 3", + "tint4": "Teinte 4", + "tint5": "Teinte 5", + "tint6": "Teinte 6", + "tint7": "Teinte 7", + "tint8": "Teinte 8", + "tint9": "Teinte 9", + "lightLightTint1": "Violet", + "lightLightTint2": "Rose", + "lightLightTint3": "Rose clair", + "lightLightTint4": "Orange", + "lightLightTint5": "Jaune", + "lightLightTint6": "Lime", + "lightLightTint7": "Vert", + "lightLightTint8": "Turquoise", + "lightLightTint9": "Bleu", + "urlHint": "URL", + "mobileHeading1": "Titre 1", + "mobileHeading2": "Titre 2", + "mobileHeading3": "Titre 3", + "textColor": "Couleur du texte", + "backgroundColor": "Couleur du fond", + "addYourLink": "Ajoutez votre lien", + "openLink": "Ouvrir le lien", + "copyLink": "Copier le lien", + "removeLink": "Supprimer le lien", + "editLink": "Modifier le lien", + "linkText": "Texte", + "linkTextHint": "Veuillez saisir du texte", + "linkAddressHint": "Veuillez entrer l'URL", + "highlightColor": "Couleur de surlignage", + "clearHighlightColor": "Effacer la couleur de surlignage", + "customColor": "Couleur personnalisée", + "hexValue": "Valeur hexadécimale", + "opacity": "Opacité", + "resetToDefaultColor": "Réinitialiser la couleur par défaut", + "ltr": "LTR", + "rtl": "RTL", + "auto": "Auto", + "cut": "Couper", + "copy": "Copier", + "paste": "Color", + "find": "Chercher", + "previousMatch": "Occurence précédente", + "nextMatch": "Occurence suivante", + "closeFind": "Fermer", + "replace": "Remplacer", + "replaceAll": "Tout remplacer", + "regex": "Regex", + "caseSensitive": "Sensible à la casse", + "uploadImage": "Téléverser une image", + "urlImage": "URL de l'image ", + "incorrectLink": "Lien incorrect", + "upload": "Téléverser", + "chooseImage": "Choisissez une image", + "loading": "Chargement", + "imageLoadFailed": "Impossible de charger l'image", + "divider": "Séparateur", + "table": "Tableau", + "colAddBefore": "Ajouter avant", + "rowAddBefore": "Ajouter avant", + "colAddAfter": "Ajouter après", + "rowAddAfter": "Ajouter après", + "colRemove": "Retirer", + "rowRemove": "Retirer", + "colDuplicate": "Dupliquer", + "rowDuplicate": "Dupliquer", + "colClear": "Effacer le ontenu", + "rowClear": "Effacer le ontenu", + "slashPlaceHolder": "Tapez '/' pour insérer un bloc ou commencez à écrire", + "typeSomething": "Écrivez quelque chose...", + "toggleListShortForm": "Basculer", + "quoteListShortForm": "Citation", + "mathEquationShortForm": "Formule", + "codeBlockShortForm": "Code" + }, + "favorite": { + "noFavorite": "Aucune page favorite", + "noFavoriteHintText": "Faites glisser la page vers la gauche pour l'ajouter à vos favoris" + }, + "cardDetails": { + "notesPlaceholder": "Entrez un / pour insérer un bloc ou commencez à taper" + }, + "blockPlaceholders": { + "todoList": "À faire", + "bulletList": "Liste", + "numberList": "Liste", + "quote": "Citation", + "heading": "Titre {}" + }, + "titleBar": { + "pageIcon": "Icône de page", + "language": "Langue", + "font": "Police ", + "actions": "Actions", + "date": "Date", + "addField": "Ajouter un champ", + "userIcon": "Icône utilisateur" + }, + "noLogFiles": "Il n'y a pas de log" } diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 8455de0e8dcd9..683b29c649aca 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -25,10 +25,10 @@ "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", "alreadyHaveAnAccount": "Avez-vous déjà un compte ?", - "emailHint": "E-mail", + "emailHint": "Courriel", "passwordHint": "Mot de passe", "repeatPasswordHint": "Ressaisir votre mot de passe", - "signUpWith": "Se connecter avec:" + "signUpWith": "Se connecter avec :" }, "signIn": { "loginTitle": "Connexion à @:appName", @@ -38,17 +38,17 @@ "buttonText": "Se connecter", "signingInText": "Connexion en cours...", "forgotPassword": "Mot de passe oublié ?", - "emailHint": "E-mail", + "emailHint": "Courriel", "passwordHint": "Mot de passe", - "dontHaveAnAccount": "Vous n'avez pas encore créé votre compte ?", + "dontHaveAnAccount": "Vous n'avez pas encore de compte ?", "repeatPasswordEmptyError": "Vous n'avez pas ressaisi votre mot de passe", "unmatchedPasswordError": "Les deux mots de passe ne sont pas identiques", - "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. S'il vous plaît, ne fermez pas cette page", + "syncPromptMessage": "La synchronisation des données peut prendre un certain temps. Merci de ne pas fermer pas cette page.", "or": "OU", "LogInWithGoogle": "Se connecter avec Google", "LogInWithGithub": "Se connecter avec Github", "LogInWithDiscord": "Se connecter avec Discord", - "signInWith": "Se connecter avec:", + "signInWith": "Se connecter avec :", "loginAsGuestButtonText": "Commencer" }, "workspace": { @@ -95,7 +95,7 @@ "unfavorite": "Retirer des favoris", "favorite": "Ajouter aux favoris", "openNewTab": "Ouvrir dans un nouvel onglet", - "moveTo": "Déplacer à", + "moveTo": "Déplacer vers", "addToFavorites": "Ajouter aux Favoris", "copyLink": "Copier le lien" }, @@ -107,8 +107,8 @@ "newBoardText": "Nouveau tableau", "trash": { "text": "Corbeille", - "restoreAll": "Restaurer tout", - "deleteAll": "Supprimer tout", + "restoreAll": "Tout restaurer", + "deleteAll": "Tout supprimer", "pageHeader": { "fileName": "Nom de fichier", "lastModified": "Dernière modification", @@ -123,15 +123,15 @@ "caption": "Cette action ne peut pas être annulée." }, "mobile": { - "actions": "Actions de corbeille", + "actions": "Actions de la corbeille", "empty": "La corbeille est vide", "emptyDescription": "Vous n'avez aucun fichier supprimé", - "isDeleted": "est supprimé", - "isRestored": "est restauré" + "isDeleted": "a été supprimé", + "isRestored": "a été restauré" } }, "deletePagePrompt": { - "text": "Cette page a été supprimée, vous pouvez la retrouver dans la corbeille", + "text": "Cette page se trouve dans la corbeille", "restore": "Restaurer la page", "deletePermanent": "Supprimer définitivement" }, @@ -143,8 +143,8 @@ "markdown": "Réduction", "debug": { "name": "Informations de Débogage", - "success": "Informations de Débogage copiées dans le presse-papiers !", - "fail": "Impossible de copier les informations de Débogage dans le presse-papiers" + "success": "Informations de débogage copiées dans le presse-papiers !", + "fail": "Impossible de copier les informations de débogage dans le presse-papiers" }, "feedback": "Retour" }, @@ -165,8 +165,8 @@ "numList": "Liste numérotée", "bulletList": "Liste à puces", "checkList": "To-Do List", - "inlineCode": "Code", - "quote": "Bloc de citation", + "inlineCode": "Code en ligne", + "quote": "Citation", "header": "En-tête", "highlight": "Surligner", "color": "Couleur", @@ -184,7 +184,7 @@ "referencePage": "Ce {nom} est référencé", "addBlockBelow": "Ajouter un bloc ci-dessous", "urlLaunchAccessory": "Ouvrir dans le navigateur", - "urlCopyAccessory": "Copier URL" + "urlCopyAccessory": "Copier l'URL" }, "sideBar": { "closeSidebar": "Fermer le menu latéral", @@ -218,7 +218,7 @@ "save": "Enregistrer", "generate": "Générer", "esc": "ESC", - "keep": "Donjon", + "keep": "Garder", "tryAgain": "Essayer à nouveau", "discard": "Jeter", "replace": "Remplacer", @@ -229,7 +229,7 @@ "delete": "Supprimer", "duplicate": "Dupliquer", "putback": "Remettre", - "update": "Mise à jour", + "update": "Mettre à jour", "share": "Partager", "removeFromFavorites": "Retirer des favoris", "addToFavorites": "Ajouter aux favoris", @@ -239,7 +239,8 @@ "yes": "Oui", "Done": "Fait", "Cancel": "Annuler", - "OK": "OK" + "OK": "OK", + "tryAGain": "Réessayer" }, "label": { "welcome": "Bienvenue !", @@ -256,9 +257,9 @@ "google": { "title": "CONNEXION VIA GOOGLE", "instruction1": "Pour importer vos contacts Google, vous devez autoriser cette application à l'aide de votre navigateur web.", - "instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte:", - "instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus:", - "instruction4": "Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription:" + "instruction2": "Copiez ce code dans votre presse-papiers en cliquant sur l'icône ou en sélectionnant le texte :", + "instruction3": "Accédez au lien suivant dans votre navigateur web et saisissez le code ci-dessus :", + "instruction4": "Appuyez sur le bouton ci-dessous lorsque vous avez terminé votre inscription :" } }, "settings": { @@ -267,12 +268,12 @@ "appearance": "Apparence", "language": "Langue", "user": "Utilisateur", - "files": "Des dossiers", + "files": "Dossiers", "notifications": "Notifications", "open": "Ouvrir les paramètres", "logout": "Se 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é le secret de cryptage", + "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", "enableSync": "Activer la synchronisation", @@ -281,12 +282,12 @@ "invalidCloudURLScheme": "Schéma invalide", "cloudServerType": "Serveur cloud", "cloudServerTypeTip": "Veuillez noter qu'il est possible que votre compte actuel soit déconnecté après avoir changé de serveur cloud.", - "cloudLocal": "Locale", + "cloudLocal": "Local", "cloudSupabase": "Supabase", - "cloudSupabaseUrl": "URL de supabase", - "cloudSupabaseUrlCanNotBeEmpty": "L'URL supabase ne peut pas être vide", - "cloudSupabaseAnonKey": "Clé anonyme supabase", - "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de la supabase n'est pas vide", + "cloudSupabaseUrl": "URL de Supabase", + "cloudSupabaseUrlCanNotBeEmpty": "L'URL Supabase ne peut pas être vide", + "cloudSupabaseAnonKey": "Clé anonyme Supabase", + "cloudSupabaseAnonKeyCanNotBeEmpty": "La clé anonyme ne peut pas être vide si l'URL de Supabase n'est pas vide", "cloudAppFlowy": "AppFlowy Cloud Bêta", "cloudAppFlowySelfHost": "AppFlowy Cloud auto-hébergé", "appFlowyCloudUrlCanNotBeEmpty": "L'URL cloud ne peut pas être vide", @@ -298,14 +299,14 @@ "cloudWSURL": "URL du websocket", "cloudWSURLHint": "Saisissez l'adresse websocket de votre serveur", "restartApp": "Redémarer", - "restartAppTip": "Redémarrez l'application pour que les modifications prennent effet. Veuillez noter que cela pourrait déconnecter votre compte actuel", + "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 cryptage pour sécuriser vos données avec ce secret. Rangez-le 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 secret de cryptage pour", - "clickToCopySecret": "Cliquez pour copier le secret", + "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 secret", + "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 »", "openHistoricalUser": "Cliquez pour ouvrir le compte anonyme", @@ -331,7 +332,7 @@ "search": "Recherche" }, "themeMode": { - "label": " Mode Thème", + "label": " Mode du Thème", "light": "Mode clair", "dark": "Mode sombre", "system": "S'adapter au système" @@ -345,12 +346,12 @@ "opacityEmptyError": "L'opacité ne peut pas être vide", "opacityRangeError": "L'opacité doit être comprise entre 1 et 100", "app": "Application", - "flowy": "Fluide", + "flowy": "Flowy", "apply": "Appliquer" }, "layoutDirection": { "label": "Orientation de la mise en page", - "hint": "Contrôlez le flux du contenu sur votre écran, de gauche à droite ou de droite à gauche.", + "hint": "Contrôlez l'orientation du contenu sur votre écran, de gauche à droite ou de droite à gauche.", "ltr": "LTR", "rtl": "RTL" }, @@ -363,11 +364,11 @@ "fallback": "Identique au sens de mise en page" }, "themeUpload": { - "button": "Télécharger", - "uploadTheme": "Télécharger le thème", - "description": "Téléchargez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", + "button": "Téléverser", + "uploadTheme": "Téléverser le thème", + "description": "Téléversez votre propre thème AppFlowy en utilisant le bouton ci-dessous.", "loading": "Veuillez patienter pendant que nous validons et téléchargeons votre thème...", - "uploadSuccess": "Votre thème a été téléchargé avec succès", + "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 : {}", @@ -377,11 +378,11 @@ "builtInsLabel": "Thèmes intégrés", "pluginsLabel": "Plugins", "dateFormat": { - "label": "Format de date", + "label": "Format de la date", "local": "Local", "us": "US", "iso": "ISO", - "friendly": "Amical", + "friendly": "Convivial", "dmy": "J/M/A" }, "timeFormat": { @@ -409,7 +410,7 @@ "open": "Ouvrir", "openFolder": "Ouvrir un dossier existant", "openFolderDesc": "Lisez-le et écrivez-le dans votre dossier AppFlowy existant", - "folderHintText": "nom de dossier", + "folderHintText": "Nom de dossier", "location": "Création d'un nouveau dossier", "locationDesc": "Choisissez un nom pour votre dossier de données AppFlowy", "browser": "Parcourir", @@ -419,21 +420,21 @@ "locationCannotBeEmpty": "Le chemin ne peut pas être vide", "pathCopiedSnackbar": "Chemin de stockage des fichiers copié dans le presse-papier !", "changeLocationTooltips": "Changer le répertoire de données", - "change": "Changement", + "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'exportation du fichier !", + "exportFileFail": "Échec de l'export du fichier !", "export": "Exporter" }, "user": { "name": "Nom", - "email": "E-mail", + "email": "Courriel", "tooltipSelectIcon": "Sélectionner l'icône", "selectAnIcon": "Sélectionnez une icône", - "pleaseInputYourOpenAIKey": "veuillez entrer votre clé OpenAI", - "pleaseInputYourStabilityAIKey": "veuillez saisir votre clé de Stability AI", + "pleaseInputYourOpenAIKey": "Veuillez entrer votre clé OpenAI", + "pleaseInputYourStabilityAIKey": "Veuillez saisir votre clé de Stability AI", "clickToLogout": "Cliquez pour déconnecter l'utilisateur actuel" }, "shortcuts": { @@ -453,7 +454,7 @@ "usernameEmptyError": "Le nom d'utilisateur ne peut pas être vide", "about": "À propos", "pushNotifications": "Notifications push", - "support": "Soutien", + "support": "Support", "joinDiscord": "Rejoignez-nous sur Discord", "privacyPolicy": "Politique de Confidentialité", "userAgreement": "Accord de l'utilisateur", @@ -474,7 +475,7 @@ "settings": { "filter": "Filtrer", "sort": "Trier", - "sortBy": "Filtrer par", + "sortBy": "Trier par", "properties": "Propriétés", "reorderPropertiesTooltip": "Faites glisser pour réorganiser les propriétés", "group": "Groupe", @@ -488,9 +489,9 @@ "boardSettings": "Paramètres du tableau", "calendarSettings": "Paramètres du calendrier", "createView": "Nouvelle vue", - "duplicateView": "Dupliquer vue", - "deleteView": "Supprimer vue", - "numberOfVisibleFields": "{} affiché", + "duplicateView": "Dupliquer la vue", + "deleteView": "Supprimer la vue", + "numberOfVisibleFields": "{} affiché(s)", "Properties": "Propriétés", "viewList": "Vues de base de données" }, @@ -498,29 +499,29 @@ "contains": "Contient", "doesNotContain": "Ne contient pas", "endsWith": "Se termine par", - "startWith": "Commence avec", + "startWith": "Commence par", "is": "Est", "isNot": "N'est pas", "isEmpty": "Est vide", "isNotEmpty": "N'est pas vide", "choicechipPrefix": { "isNot": "Pas", - "startWith": "Commence avec", + "startWith": "Commence par", "endWith": "Se termine par", "isEmpty": "est vide", "isNotEmpty": "n'est pas vide" } }, "checkboxFilter": { - "isChecked": "Vérifié", + "isChecked": "Coché", "isUnchecked": "Décoché", "choicechipPrefix": { "is": "est" } }, "checklistFilter": { - "isComplete": "est complet", - "isIncomplted": "est incomplet" + "isComplete": "fait", + "isIncomplted": "pas fait" }, "singleSelectOptionFilter": { "is": "Est", @@ -538,8 +539,8 @@ "is": "Est", "before": "Est avant", "after": "Est après", - "onOrBefore": "Est sur ou avant", - "onOrAfter": "Est sur ou après", + "onOrBefore": "Est le ou avant", + "onOrAfter": "Est le ou après", "between": "Est entre", "empty": "Est vide", "notEmpty": "N'est pas vide" @@ -554,13 +555,13 @@ "textFieldName": "Texte", "checkboxFieldName": "Case à cocher", "dateFieldName": "Date", - "updatedAtFieldName": "Heure de la dernière modification", - "createdAtFieldName": "Temps créé", + "updatedAtFieldName": "Dernière modification", + "createdAtFieldName": "Créé le", "numberFieldName": "Nombre", "singleSelectFieldName": "Sélectionner", - "multiSelectFieldName": "Multisélection", + "multiSelectFieldName": "Sélection multiple", "urlFieldName": "URL", - "checklistFieldName": "Liste de contrôle", + "checklistFieldName": "Check-list", "numberFormat": "Format du nombre", "dateFormat": "Format de la date", "includeTime": "Inclure l'heure", @@ -569,8 +570,8 @@ "dateFormatISO": "Année-Mois-Jour", "dateFormatLocal": "Mois/Jour/Année", "dateFormatUS": "Année/Mois/Jour", - "dateFormatDayMonthYear": "Jour mois année", - "timeFormat": "Format du temps", + "dateFormatDayMonthYear": "Jour/Mois/Année", + "timeFormat": "Format de l'heure", "invalidTimeFormat": "Format invalide", "timeFormatTwelveHour": "12 heures", "timeFormatTwentyFourHour": "24 heures", @@ -584,7 +585,7 @@ "visibility": "Visibilité", "propertyType": "Type de propriété", "addSelectOption": "Ajouter une option", - "typeANewOption": "Écrivez une nouvelle option", + "typeANewOption": "Saisissez une nouvelle option", "optionTitle": "Options", "addOption": "Ajouter une option", "editProperty": "Modifier la propriété", @@ -611,7 +612,7 @@ "sort": { "ascending": "Ascendant", "descending": "Descendant", - "deleteAllSorts": "Supprimer toutes sortes", + "deleteAllSorts": "Supprimer tous les tris", "addSort": "Ajouter un tri", "deleteSort": "Supprimer le tri" }, @@ -621,7 +622,7 @@ "titlePlaceholder": "Sans titre", "textPlaceholder": "Vide", "copyProperty": "Copie de la propriété dans le presse-papiers", - "count": "Nombre", + "count": "Compte", "newRow": "Nouvelle ligne", "action": "Action", "add": "Cliquez sur ajouter ci-dessous", @@ -639,17 +640,17 @@ "yellowColor": "Jaune", "limeColor": "Citron vert", "greenColor": "Vert", - "aquaColor": "Aqua", + "aquaColor": "Turquoise", "blueColor": "Bleu", "deleteTag": "Supprimer l'étiquette", "colorPanelTitle": "Couleurs", "panelTitle": "Sélectionnez une option ou créez-en une", "searchOption": "Rechercher une option", "searchOrCreateOption": "Rechercher ou créer une option...", - "createNew": "Créer un nouveau", + "createNew": "Créer une nouvelle", "orSelectOne": "Ou sélectionnez une option", - "typeANewOption": "Écrivez une nouvelle option", - "tagName": "Nom du tag" + "typeANewOption": "Saisissez une nouvelle option", + "tagName": "Nom de l'étiquette" }, "checklist": { "taskHint": "Description de la tâche", @@ -660,6 +661,7 @@ }, "menuName": "Grille", "referencedGridPrefix": "Vue", + "calculate": "Calculer", "calculationTypeLabel": { "none": "Vide", "average": "Moyenne", @@ -697,17 +699,17 @@ "codeBlock": "Bloc de code" }, "plugins": { - "referencedBoard": "Conseil référencé", + "referencedBoard": "Tableau référencé", "referencedGrid": "Grille référencée", "referencedCalendar": "Calendrier référencé", "referencedDocument": "Document référencé", "autoGeneratorMenuItemName": "Rédacteur OpenAI", - "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire n'importe quoi...", + "autoGeneratorTitleName": "OpenAI : Demandez à l'IA d'écrire quelque chose...", "autoGeneratorLearnMore": "Apprendre encore plus", "autoGeneratorGenerate": "Générer", "autoGeneratorHintText": "Demandez à OpenAI...", "autoGeneratorCantGetOpenAIKey": "Impossible d'obtenir la clé OpenAI", - "autoGeneratorRewrite": "Récrire", + "autoGeneratorRewrite": "Réécrire", "smartEdit": "Assistants IA", "openAI": "OpenAI", "smartEditFixSpelling": "Corriger l'orthographe", @@ -725,10 +727,10 @@ "quoteList": "Liste de citations", "numberedList": "Liste numérotée", "bulletedList": "Liste à puces", - "todoList": "Liste à faire", - "callout": "Appeler", + "todoList": "Liste de tâches", + "callout": "Encadré", "cover": { - "changeCover": "Changer de couverture", + "changeCover": "Changer la couverture", "colors": "Couleurs", "images": "Images", "clearAll": "Tout Effacer", @@ -748,9 +750,9 @@ "couldNotFetchImage": "Impossible de récupérer l'image", "imageSavingFailed": "Échec de l'enregistrement de l'image", "addIcon": "Ajouter une icône", - "changeIcon": "Changer d'icône", + "changeIcon": "Changer l'icône", "coverRemoveAlert": "Il sera retiré de la couverture après sa suppression.", - "alertDialogConfirmation": "Es-tu sur de vouloir continuer?" + "alertDialogConfirmation": "Voulez-vous vraiment continuer?" }, "mathEquation": { "name": "Équation mathématique", @@ -816,8 +818,8 @@ "imageBlock": { "placeholder": "Cliquez pour ajouter une image", "upload": { - "label": "Télécharger", - "placeholder": "Cliquez pour télécharger l'image" + "label": "Téléverser", + "placeholder": "Cliquez pour téléverser l'image" }, "url": { "label": "URL de l'image", @@ -853,7 +855,7 @@ "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 au plus 10Mo" + "uploadImageErrorImageSizeTooBig": "L'image doit faire moins de 10Mo" }, "codeBlock": { "language": { @@ -881,6 +883,7 @@ "label": "Lien vers la page", "tooltip": "Cliquez pour ouvrir la page" }, + "deletedContent": "Ce document n'existe pas ou a été supprimé", "deleted": "Supprimer" }, "toolbar": { @@ -917,7 +920,7 @@ "showOnCard": "Afficher les détails de la carte", "setting": "Paramètre", "propertyName": "Nom de la propriété", - "menuName": "Conseil", + "menuName": "Tableau", "showUngrouped": "Afficher les éléments non regroupés", "ungroupedButtonText": "Non groupé", "ungroupedButtonTooltip": "Contient des cartes qui n'appartiennent à aucun groupe", @@ -939,8 +942,8 @@ "navigation": { "today": "Aujourd'hui", "jumpToday": "Aller à aujourd'hui", - "previousMonth": "Le mois précédent", - "nextMonth": "Le mois prochain" + "previousMonth": "Mois précédent", + "nextMonth": "Mois prochain" }, "mobileEventScreen": { "emptyTitle": "Pas d'événements", @@ -953,7 +956,8 @@ "layoutDateField": "Calendrier de mise en page par", "changeLayoutDateField": "Modifier le champ de mise en page", "noDateTitle": "Pas de date", - "unscheduledEventsTitle": "Événements imprévus", + "noDateHint": "Les événements non planifiés s'afficheront ici", + "unscheduledEventsTitle": "Événements non planifiés", "clickToAdd": "Cliquez pour ajouter au calendrier", "name": "Disposition du calendrier", "noDateHint": "Les événements non planifiés s'afficheront ici" @@ -998,15 +1002,15 @@ }, "emoji": { "emojiTab": "Émoji", - "search": "Chercher émoji", - "noRecent": "Aucun emoji récent", - "noEmojiFound": "Aucun emoji trouvé", + "search": "Chercher un émoji", + "noRecent": "Aucun émoji récent", + "noEmojiFound": "Aucun émoji trouvé", "filter": "Filtrer", "random": "Aléatoire", - "selectSkinTone": "Choisir la teinte de la peau", - "remove": "Supprimer émoticône", + "selectSkinTone": "Choisir le teint de la peau", + "remove": "Supprimer l'émoji", "categories": { - "smileys": "Smileys & émoticones", + "smileys": "Smileys & émoticônes", "people": "Personnes & corps", "animals": "Animaux & Nature", "food": "Nourriture & Boisson", @@ -1016,7 +1020,7 @@ "symbols": "Symboles", "flags": "Drapeaux", "nature": "Nature", - "frequentlyUsed": "Fréquemment utilisé" + "frequentlyUsed": "Fréquemment utilisés" }, "skinTone": { "default": "Défaut", @@ -1043,9 +1047,9 @@ "datePicker": { "dateTimeFormatTooltip": "Modifier le format de la date et de l'heure dans les paramètres", "dateFormat": "Format de date", - "includeTime": "Inclure le temps", + "includeTime": "Inclure l'heure", "isRange": "Date de fin", - "timeFormat": "Format du temps", + "timeFormat": "Format de l'heure", "clearDate": "Effacer la date", "reminderLabel": "Rappel", "selectReminder": "Sélectionnez un rappel", @@ -1062,7 +1066,7 @@ "oneDayBefore": "1 jour avant", "twoDaysBefore": "2 jours avant", "oneWeekBefore": "1 semaine avant", - "custom": "Personnaliser" + "custom": "Personnalisé" } }, "relativeDates": { @@ -1119,6 +1123,7 @@ "editor": { "bold": "Gras", "bulletedList": "Liste à puces", + "bulletedListShortForm": "Puces", "checkbox": "Case à cocher", "embedCode": "Code intégré", "heading1": "H1", @@ -1172,9 +1177,9 @@ "lightLightTint3": "Rose clair", "lightLightTint4": "Orange", "lightLightTint5": "Jaune", - "lightLightTint6": "Lime", + "lightLightTint6": "Citron vert", "lightLightTint7": "Vert", - "lightLightTint8": "Aqua", + "lightLightTint8": "Turquoise", "lightLightTint9": "Bleu", "urlHint": "URL", "mobileHeading1": "Titre 1", @@ -1191,7 +1196,7 @@ "linkTextHint": "Veuillez saisir du texte", "linkAddressHint": "Veuillez entrer l'URL", "highlightColor": "Couleur de surlignage", - "clearHighlightColor": "Effacer couleur de surlignage", + "clearHighlightColor": "Effacer la couleur de surlignage", "customColor": "Couleur personnalisée", "hexValue": "Valeur hexadécimale", "opacity": "Opacité", @@ -1203,21 +1208,21 @@ "copy": "Copier", "paste": "Color", "find": "Chercher", - "previousMatch": "Match précédent", - "nextMatch": "Prochain match", + "previousMatch": "Occurence précédente", + "nextMatch": "Occurence suivante", "closeFind": "Fermer", "replace": "Remplacer", "replaceAll": "Tout remplacer", "regex": "Regex", - "caseSensitive": "Sensible aux majuscules et minuscules", - "uploadImage": "Télécharger une image", + "caseSensitive": "Sensible à la casse", + "uploadImage": "Téléverser une image", "urlImage": "URL de l'image ", "incorrectLink": "Lien incorrect", - "upload": "Télécharger", + "upload": "Téléverser", "chooseImage": "Choisissez une image", "loading": "Chargement", "imageLoadFailed": "Impossible de charger l'image", - "divider": "Diviseur", + "divider": "Séparateur", "table": "Tableau", "colAddBefore": "Ajouter avant", "rowAddBefore": "Ajouter avant", @@ -1231,6 +1236,7 @@ "rowClear": "Effacer le ontenu", "slashPlaceHolder": "Tapez '/' pour insérer un bloc ou commencez à écrire", "typeSomething": "Écrivez quelque chose...", + "toggleListShortForm": "Basculer", "quoteListShortForm": "Citation", "mathEquationShortForm": "Formule", "codeBlockShortForm": "Code" From d5ee372131e5d558bee9913059eb887d746436d1 Mon Sep 17 00:00:00 2001 From: Bastian Leicht Date: Sun, 18 Feb 2024 00:36:23 +0100 Subject: [PATCH 42/50] chore: update translations (#4615) Co-authored-by: nathan From b2e55c4e2e7c9c07e229c26689d697fe583940c2 Mon Sep 17 00:00:00 2001 From: "Nathan.fooo" <86001920+appflowy@users.noreply.github.com> Date: Sun, 18 Feb 2024 16:40:18 +0800 Subject: [PATCH 43/50] Fix: init state (#4672) * chore: fix login state * chore: change log * chore: fix flutter version --- CHANGELOG.md | 4 ++++ frontend/Makefile.toml | 2 +- frontend/appflowy_flutter/pubspec.yaml | 2 +- frontend/rust-lib/flowy-server/src/af_cloud/server.rs | 6 +++--- frontend/rust-lib/flowy-user-pub/src/entities.rs | 1 + frontend/rust-lib/flowy-user/src/user_manager/manager.rs | 2 +- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ad262e1ccf3..f4d52fe998e49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ # Release Notes +## Version 0.4.9 - 02/17/2024 +### Bug Fixes +- Resolved the issue that caused users to be redirected to the Sign In page +- ## Version 0.4.8 - 02/13/2024 ### Bug Fixes - Fixed a possible error when loading workspaces diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml index 7f05064bedc72..336b2f545ca48 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.4.8" +APPFLOWY_VERSION = "0.4.9" FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite" PRODUCT_NAME = "AppFlowy" MACOSX_DEPLOYMENT_TARGET = "11.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 85edb513ff683..9e81b44df5613 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -15,7 +15,7 @@ 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.4.8 +version: 0.4.9 environment: flutter: ">=3.18.0-0.2.pre" 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 f82297356c549..e3b1d3c9f2e4c 100644 --- a/frontend/rust-lib/flowy-server/src/af_cloud/server.rs +++ b/frontend/rust-lib/flowy-server/src/af_cloud/server.rs @@ -110,7 +110,7 @@ impl AppFlowyServer for AppFlowyCloudServer { fn subscribe_token_state(&self) -> Option> { let mut token_state_rx = self.client.subscribe_token_state(); - let (watch_tx, watch_rx) = watch::channel(UserTokenState::Invalid); + let (watch_tx, watch_rx) = watch::channel(UserTokenState::Init); let weak_client = Arc::downgrade(&self.client); af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { @@ -265,7 +265,7 @@ fn spawn_ws_conn( event!(tracing::Level::INFO, "🟢reconnecting websocket"); let _ = ws_client.connect(ws_addr, &cloned_device_id).await; }, - Err(err) => error!("Failed to get ws url: {}", err), + Err(err) => error!("Failed to get ws url: {}, connect state:{:?}", err, state), } } } @@ -288,6 +288,7 @@ fn spawn_ws_conn( let weak_api_client = Arc::downgrade(api_client); af_spawn(async move { while let Ok(token_state) = token_state_rx.recv().await { + info!("🟢token state: {:?}", token_state); match token_state { TokenState::Refresh => { if let (Some(api_client), Some(ws_client)) = @@ -295,7 +296,6 @@ fn spawn_ws_conn( { match api_client.ws_url(&device_id).await { Ok(ws_addr) => { - info!("🟢token state: {:?}, reconnecting websocket", token_state); let _ = ws_client.connect(ws_addr, &device_id).await; }, Err(err) => error!("Failed to get ws url: {}", err), diff --git a/frontend/rust-lib/flowy-user-pub/src/entities.rs b/frontend/rust-lib/flowy-user-pub/src/entities.rs index cebab748362f7..9728c0cd09ec1 100644 --- a/frontend/rust-lib/flowy-user-pub/src/entities.rs +++ b/frontend/rust-lib/flowy-user-pub/src/entities.rs @@ -375,6 +375,7 @@ pub struct AFCloudOAuthParams { #[derive(Clone, Debug)] pub enum UserTokenState { + Init, Refresh { token: String }, Invalid, } 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 1e44a19b0b998..af75c0d39527b 100644 --- a/frontend/rust-lib/flowy-user/src/user_manager/manager.rs +++ b/frontend/rust-lib/flowy-user/src/user_manager/manager.rs @@ -227,8 +227,8 @@ impl UserManager { { error!("Sign out when token invalid failed: {:?}", err); } - // Force user to sign out when the token is invalid }, + UserTokenState::Init => {}, } } }); From 252699d24986376b6b827279c01914b2772e21e2 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 19 Feb 2024 12:49:55 +0700 Subject: [PATCH 44/50] chore: upgrade to flutter 3.19.0 (#4677) * chore: upgrade to flutter 3.19.0 * chore: adjust to the latest flutter convention * chore: update CI flutter version * fix: flutter analyze * fix: flutter analyze * fix: flutter analyze * chore: fix docker build --- .github/workflows/flutter_ci.yaml | 12 +- .github/workflows/mobile_ci.yaml | 4 +- .github/workflows/release.yml | 10 +- .github/workflows/rust_coverage.yml | 4 +- .../ios/Flutter/AppFrameworkInfo.plist | 2 +- frontend/appflowy_flutter/ios/Podfile | 2 +- frontend/appflowy_flutter/ios/Podfile.lock | 8 +- .../ios/Runner.xcodeproj/project.pbxproj | 8 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../lib/core/raw_keyboard_extension.dart | 19 --- .../field/mobile_quick_field_editor.dart | 2 +- .../setting/language_setting_group.dart | 2 +- .../widgets/board_column_header.dart | 9 +- .../cell_editor/checklist_cell_editor.dart | 10 +- .../actions/block_action_add_button.dart | 3 +- .../base/page_reference_commands.dart | 5 +- .../image/image_placeholder.dart | 2 +- .../math_equation_block_component.dart | 7 +- .../widgets/inline_actions_handler.dart | 15 +- .../startup/tasks/memory_leak_detector.dart | 2 +- .../menu/sidebar/folder/favorite_folder.dart | 3 +- .../menu/sidebar/folder/personal_folder.dart | 3 +- ...etting_file_import_appflowy_data_view.dart | 2 +- .../settings_customize_shortcuts_view.dart | 19 ++- ...settings_file_customize_location_view.dart | 8 +- .../widgets/date_picker_dialog.dart | 12 +- .../presentation/widgets/left_bar_item.dart | 86 ---------- .../macos/Runner.xcodeproj/project.pbxproj | 2 +- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- .../appflowy_backend/example/pubspec.yaml | 2 +- .../packages/appflowy_backend/pubspec.yaml | 2 +- .../example/lib/example_button.dart | 6 +- .../appflowy_popover/example/lib/main.dart | 4 +- .../appflowy_popover/example/pubspec.yaml | 2 +- .../appflowy_popover/lib/src/mask.dart | 3 +- .../appflowy_popover/lib/src/popover.dart | 7 +- .../packages/appflowy_popover/pubspec.yaml | 2 +- .../packages/flowy_infra/lib/icon_data.dart | 1 + .../packages/flowy_infra/pubspec.yaml | 4 +- .../example/lib/home/home_screen.dart | 2 +- .../example/lib/keyboard/keyboard_screen.dart | 2 +- .../flowy_infra_ui/example/lib/main.dart | 2 +- .../example/lib/overlay/overlay_screen.dart | 2 +- .../flowy_infra_ui/example/pubspec.yaml | 5 +- .../pubspec.yaml | 2 +- .../flowy_infra_ui_web/pubspec.yaml | 2 +- .../lib/src/flowy_overlay/flowy_overlay.dart | 2 +- .../lib/src/flowy_overlay/list_overlay.dart | 8 +- .../lib/src/flowy_overlay/option_overlay.dart | 8 +- .../lib/src/focus/auto_unfocus_overlay.dart | 4 +- .../keyboard_visibility_detector.dart | 9 +- .../lib/style_widget/bar_title.dart | 4 +- .../lib/style_widget/button.dart | 12 +- .../lib/style_widget/close_button.dart | 4 +- .../lib/style_widget/color_picker.dart | 4 +- .../lib/style_widget/container.dart | 5 +- .../lib/style_widget/hover.dart | 8 +- .../lib/style_widget/icon_button.dart | 5 +- .../lib/style_widget/image_icon.dart | 3 +- .../lib/style_widget/progress_indicator.dart | 2 +- .../style_widget/scrolling/styled_list.dart | 4 +- .../scrolling/styled_scroll_bar.dart | 5 +- .../scrolling/styled_scrollview.dart | 8 +- .../lib/style_widget/text_input.dart | 15 +- .../widget/buttons/base_styled_button.dart | 4 +- .../lib/widget/buttons/primary_button.dart | 8 +- .../lib/widget/constraint_flex_view.dart | 5 +- .../lib/widget/dialog/styled_dialogs.dart | 14 +- .../lib/widget/ignore_parent_gesture.dart | 4 +- .../lib/widget/mouse_hover_builder.dart | 3 +- .../lib/widget/rounded_button.dart | 8 +- .../lib/widget/rounded_input_field.dart | 4 +- .../flowy_infra_ui/lib/widget/spacing.dart | 2 +- .../packages/flowy_infra_ui/pubspec.yaml | 2 +- .../packages/flowy_svg/analysis_options.yaml | 2 +- frontend/appflowy_flutter/pubspec.lock | 150 +++++++++--------- frontend/appflowy_flutter/pubspec.yaml | 8 +- .../settings/shortcuts_list_tile_test.dart | 4 +- frontend/scripts/docker-buildfiles/Dockerfile | 2 +- .../scripts/install_dev_env/install_ios.sh | 12 +- .../scripts/install_dev_env/install_linux.sh | 12 +- .../scripts/install_dev_env/install_macos.sh | 12 +- .../install_dev_env/install_windows.sh | 12 +- 83 files changed, 286 insertions(+), 407 deletions(-) delete mode 100644 frontend/appflowy_flutter/lib/core/raw_keyboard_extension.dart delete mode 100644 frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml index 439df3143af68..5c9b76ff9e43d 100644 --- a/.github/workflows/flutter_ci.yaml +++ b/.github/workflows/flutter_ci.yaml @@ -23,7 +23,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.18.0-0.2.pre" + FLUTTER_VERSION: "3.19.0" RUST_TOOLCHAIN: "1.75" CARGO_MAKE_VERSION: "0.36.6" @@ -76,7 +76,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - uses: Swatinem/rust-cache@v2 @@ -157,7 +157,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - uses: Swatinem/rust-cache@v2 @@ -260,7 +260,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - uses: taiki-e/install-action@v2 @@ -334,7 +334,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - uses: taiki-e/install-action@v2 @@ -425,7 +425,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - uses: taiki-e/install-action@v2 diff --git a/.github/workflows/mobile_ci.yaml b/.github/workflows/mobile_ci.yaml index 1fa136052ccc1..702b7a523a89e 100644 --- a/.github/workflows/mobile_ci.yaml +++ b/.github/workflows/mobile_ci.yaml @@ -18,7 +18,7 @@ on: - "!frontend/appflowy_tauri/**" env: - FLUTTER_VERSION: "3.18.0-0.2.pre" + FLUTTER_VERSION: "3.19.0" concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -57,7 +57,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb767f2094191..9514748ae173c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - "*" env: - FLUTTER_VERSION: "3.18.0-0.2.pre" + FLUTTER_VERSION: "3.19.0" RUST_TOOLCHAIN: "1.75" jobs: @@ -57,7 +57,7 @@ jobs: - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain @@ -143,7 +143,7 @@ jobs: - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain @@ -243,7 +243,7 @@ jobs: - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain @@ -348,7 +348,7 @@ jobs: - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} - name: Install Rust toolchain diff --git a/.github/workflows/rust_coverage.yml b/.github/workflows/rust_coverage.yml index 61cdb7882296e..426e36b619e05 100644 --- a/.github/workflows/rust_coverage.yml +++ b/.github/workflows/rust_coverage.yml @@ -10,7 +10,7 @@ on: env: CARGO_TERM_COLOR: always - FLUTTER_VERSION: "3.18.0-0.2.pre" + FLUTTER_VERSION: "3.19.0" RUST_TOOLCHAIN: "1.75" jobs: @@ -33,7 +33,7 @@ jobs: id: flutter uses: subosito/flutter-action@v2 with: - channel: "beta" + channel: "stable" flutter-version: ${{ env.FLUTTER_VERSION }} cache: true diff --git a/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist b/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist index 9625e105df39e..7c56964006274 100644 --- a/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist +++ b/frontend/appflowy_flutter/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 11.0 + 12.0 diff --git a/frontend/appflowy_flutter/ios/Podfile b/frontend/appflowy_flutter/ios/Podfile index c1a0ca9179cc1..131b4885eabaa 100644 --- a/frontend/appflowy_flutter/ios/Podfile +++ b/frontend/appflowy_flutter/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '11.0' +# platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock index 97f3d764e633f..da33f7ea91973 100644 --- a/frontend/appflowy_flutter/ios/Podfile.lock +++ b/frontend/appflowy_flutter/ios/Podfile.lock @@ -169,8 +169,8 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 integration_test: 13825b8a9334a850581300559b8839134b124670 @@ -189,6 +189,6 @@ SPEC CHECKSUMS: Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 -PODFILE CHECKSUM: 8c681999c7764593c94846b2a64b44d86f7a27ac +PODFILE CHECKSUM: d94f9be27d1db182e9bc77d10f065555d518f127 -COCOAPODS: 1.12.1 +COCOAPODS: 1.15.2 diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj index 17a40b25c3455..2fae19e29dc7e 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj @@ -143,7 +143,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -330,7 +330,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -419,7 +419,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -468,7 +468,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index b52b2e698b7e7..e67b2808af02f 100644 --- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ keysPressed.any( - (key) => [ - LogicalKeyboardKey.alt, - LogicalKeyboardKey.altLeft, - LogicalKeyboardKey.altRight, - ].contains(key), - ); - - bool get isControlPressed => keysPressed.any( - (key) => [ - LogicalKeyboardKey.control, - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.controlRight, - ].contains(key), - ); -} diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart index 593eaf265a99b..b70c508aab59a 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart @@ -113,7 +113,7 @@ class _QuickEditFieldState extends State { controller.text = fieldOptionValues.name; }); } else { - if (mounted) { + if (context.mounted) { context.pop(); } } diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart index 9a4bc494f0f01..6f4e65f2b4de7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/language_setting_group.dart @@ -48,7 +48,7 @@ class _LanguageSettingGroupState extends State { final newLocale = await context.push(LanguagePickerScreen.routeName); if (newLocale != null && newLocale != locale) { - if (mounted) { + if (context.mounted) { context .read() .setLocale(context, newLocale); diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart index 22da35deaa663..06eb9be584d21 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_column_header.dart @@ -138,12 +138,11 @@ class _BoardColumnHeaderState extends State { Widget _buildTextField(BuildContext context) { return Expanded( - child: RawKeyboardListener( + child: KeyboardListener( focusNode: FocusNode(), - onKey: (event) { - if (event is RawKeyDownEvent && - [LogicalKeyboardKey.enter, LogicalKeyboardKey.escape] - .contains(event.logicalKey)) { + onKeyEvent: (event) { + if ([LogicalKeyboardKey.enter, LogicalKeyboardKey.escape] + .contains(event.logicalKey)) { _saveEdit(); } }, diff --git a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart index 6e69ded8a5a2f..a2d0c4adb60a5 100644 --- a/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart +++ b/frontend/appflowy_flutter/lib/plugins/database/widgets/cell_editor/checklist_cell_editor.dart @@ -34,9 +34,8 @@ class _GridChecklistCellState extends State { void initState() { super.initState(); newTaskFocusNode = FocusNode( - onKey: (node, event) { - if (event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { + onKeyEvent: (node, event) { + if (event.logicalKey == LogicalKeyboardKey.escape) { node.unfocus(); return KeyEventResult.handled; } @@ -170,9 +169,8 @@ class _ChecklistItemState extends State { super.initState(); _textController = TextEditingController(text: widget.task.data.name); _focusNode = FocusNode( - onKey: (node, event) { - if (event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { + onKeyEvent: (node, event) { + if (event.logicalKey == LogicalKeyboardKey.escape) { node.unfocus(); return KeyEventResult.handled; } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart index 70b5066c9451a..b58b0a5646484 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:appflowy/core/raw_keyboard_extension.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_button.dart'; @@ -46,7 +45,7 @@ class BlockAddButton extends StatelessWidget { ], ), onTap: () { - final isAltPressed = RawKeyboard.instance.isAltPressed; + final isAltPressed = HardwareKeyboard.instance.isAltPressed; final transaction = editorState.transaction; diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart index a5f5bd39dabcc..f78b73bef7efd 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/base/page_reference_commands.dart @@ -79,7 +79,10 @@ Future inlinePageReferenceCommandHandler( } } - // ignore: use_build_context_synchronously + if (!context.mounted) { + return false; + } + final service = InlineActionsService( context: context, handlers: [ diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index 8eddfdd179e5d..3a680d1c6bda1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -199,7 +199,7 @@ class ImagePlaceholderState extends State { imageType = CustomImageType.internal; } - if (path == null && context.mounted) { + if (mounted && path == null) { showSnackBarMessage( context, LocaleKeys.document_imageBlock_error_invalidImage.tr(), diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart index 0040f73df24c6..0fa144b02ed3c 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/math_equation/math_equation_block_component.dart @@ -196,12 +196,11 @@ class MathEquationBlockComponentWidgetState title: Text( LocaleKeys.document_plugins_mathEquation_editMathEquation.tr(), ), - content: RawKeyboardListener( + content: KeyboardListener( focusNode: FocusNode(), - onKey: (key) { - if (key is! RawKeyDownEvent) return; + onKeyEvent: (key) { if (key.logicalKey == LogicalKeyboardKey.enter && - !key.isShiftPressed) { + !HardwareKeyboard.instance.isShiftPressed) { updateMathEquation(controller.text, context); } else if (key.logicalKey == LogicalKeyboardKey.escape) { dismiss(context); diff --git a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart index 8e442320193b0..6f8babf22f867 100644 --- a/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart +++ b/frontend/appflowy_flutter/lib/plugins/inline_actions/widgets/inline_actions_handler.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart'; import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart'; @@ -10,6 +7,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/style_widget/text.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// All heights are in physical pixels const double _groupTextHeight = 14; // 12 height + 2 bottom spacing @@ -145,7 +144,7 @@ class _InlineActionsHandlerState extends State { Widget build(BuildContext context) { return Focus( focusNode: _focusNode, - onKey: onKey, + onKeyEvent: onKeyEvent, child: Container( constraints: BoxConstraints.loose(const Size(200, _menuHeight)), decoration: BoxDecoration( @@ -208,11 +207,7 @@ class _InlineActionsHandlerState extends State { InlineActionsMenuItem handlerOf(int groupIndex, int handlerIndex) => results[groupIndex].results[handlerIndex]; - KeyEventResult onKey(focus, event) { - if (event is! RawKeyDownEvent) { - return KeyEventResult.ignored; - } - + KeyEventResult onKeyEvent(focus, KeyEvent event) { const moveKeys = [ LogicalKeyboardKey.arrowUp, LogicalKeyboardKey.arrowDown, @@ -348,7 +343,7 @@ class _InlineActionsHandlerState extends State { if (key == LogicalKeyboardKey.arrowUp || (key == LogicalKeyboardKey.tab && - RawKeyboard.instance.isShiftPressed)) { + HardwareKeyboard.instance.isShiftPressed)) { if (_selectedIndex == 0 && _selectedGroup > 0) { _selectedGroup -= 1; _selectedIndex = lengthOfGroup(_selectedGroup) - 1; diff --git a/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart b/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart index a4ae27a7fcb8b..f35c955b3da1c 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/memory_leak_detector.dart @@ -34,7 +34,7 @@ class MemoryLeakDetectorTask extends LaunchTask { ), ); - MemoryAllocations.instance.addListener((p0) { + FlutterMemoryAllocations.instance.addListener((p0) { LeakTracking.dispatchObjectEvent(p0.toMap()); }); 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 index a1be03031149e..d61feb25de3f5 100644 --- 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 @@ -1,4 +1,3 @@ -import 'package:appflowy/core/raw_keyboard_extension.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart'; import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart'; @@ -54,7 +53,7 @@ class FavoriteFolder extends StatelessWidget { view: view, level: 0, onSelected: (view) { - if (RawKeyboard.instance.isControlPressed) { + if (HardwareKeyboard.instance.isControlPressed) { context.read().openTab(view); } 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 index fafc2989c8fff..e6352f5c38e41 100644 --- 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 @@ -1,4 +1,3 @@ -import 'package:appflowy/core/raw_keyboard_extension.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/menu/menu_bloc.dart'; @@ -53,7 +52,7 @@ class PersonalFolder extends StatelessWidget { leftPadding: 16, isFeedback: false, onSelected: (view) { - if (RawKeyboard.instance.isControlPressed) { + if (HardwareKeyboard.instance.isControlPressed) { context.read().openTab(view); } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart index fa689f4849d23..a54e461db0d6b 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/setting_file_import_appflowy_data_view.dart @@ -139,7 +139,7 @@ class _ImportAppFlowyDataButtonState extends State { onTap: () async { final path = await getIt().getDirectoryPath(); - if (path == null || !mounted) { + if (path == null || !context.mounted) { return; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart index 382c3d9833fd5..a98b18c615adb 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart @@ -145,12 +145,11 @@ class ShortcutsListTile extends StatelessWidget { final formKey = GlobalKey(); return AlertDialog( title: Text(LocaleKeys.settings_shortcuts_updateShortcutStep.tr()), - content: RawKeyboardListener( + content: KeyboardListener( focusNode: FocusNode(), - onKey: (key) { - if (key is! RawKeyDownEvent) return; + onKeyEvent: (key) { if (key.logicalKey == LogicalKeyboardKey.enter && - !key.isShiftPressed) { + !HardwareKeyboard.instance.isShiftPressed) { if (controller.text == shortcutEvent.command) { _dismiss(builderContext); } @@ -161,7 +160,7 @@ class ShortcutsListTile extends StatelessWidget { } else if (key.logicalKey == LogicalKeyboardKey.escape) { _dismiss(builderContext); } else { - //extract the keybinding command from the rawkeyevent. + //extract the keybinding command from the key event. controller.text = key.convertToCommand; } }, @@ -207,19 +206,19 @@ class ShortcutsListTile extends StatelessWidget { void _dismiss(BuildContext context) => Navigator.of(context).pop(); } -extension on RawKeyEvent { +extension on KeyEvent { String get convertToCommand { String command = ''; - if (isAltPressed) { + if (HardwareKeyboard.instance.isAltPressed) { command += 'alt+'; } - if (isControlPressed) { + if (HardwareKeyboard.instance.isControlPressed) { command += 'ctrl+'; } - if (isShiftPressed) { + if (HardwareKeyboard.instance.isShiftPressed) { command += 'shift+'; } - if (isMetaPressed) { + if (HardwareKeyboard.instance.isMetaPressed) { command += 'meta+'; } diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart index e53d6f6a26c26..0116690dbbd1e 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/settings_file_customize_location_view.dart @@ -202,12 +202,12 @@ class _ChangeStoragePathButtonState extends State<_ChangeStoragePathButton> { if (path == null || widget.usingPath == path) { return; } - if (!mounted) { + if (!context.mounted) { return; } await context.read().setCustomPath(path); await runAppFlowy(isAnon: true); - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, @@ -272,14 +272,14 @@ class _RecoverDefaultStorageButtonState if (widget.usingPath == path) { return; } - if (!mounted) { + if (!context.mounted) { return; } await context .read() .resetDataStoragePathToApplicationDefault(); await runAppFlowy(isAnon: true); - if (mounted) { + if (context.mounted) { Navigator.of(context).pop(); } }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart index ce79c932b3c25..10eb49929244c 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/date_picker_dialog.dart @@ -1,6 +1,3 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; - import 'package:appflowy/workspace/presentation/widgets/date_picker/appflowy_date_picker.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/date_time_format_ext.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/utils/user_time_format_ext.dart'; @@ -9,6 +6,8 @@ import 'package:appflowy_backend/protobuf/flowy-user/date_time.pbenum.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; import 'package:flowy_infra_ui/style_widget/decoration.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; /// Provides arguemnts for [AppFlowyDatePicker] when showing /// a [DatePickerMenu] @@ -118,11 +117,10 @@ class DatePickerMenu extends DatePickerService { child: SizedBox( height: editorSize.height, width: editorSize.width, - child: RawKeyboardListener( + child: KeyboardListener( focusNode: FocusNode()..requestFocus(), - onKey: (event) { - if (event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { + onKeyEvent: (event) { + if (event.logicalKey == LogicalKeyboardKey.escape) { dismiss(); } }, diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart deleted file mode 100644 index 4c142be09ba8b..0000000000000 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/left_bar_item.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:appflowy/workspace/application/view/view_listener.dart'; -import 'package:appflowy/workspace/application/view/view_service.dart'; -import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; -import 'package:flutter/material.dart'; - -// TODO: Remove this file after the migration is done. -class ViewLeftBarItem extends StatefulWidget { - ViewLeftBarItem({ - required this.view, - }) : super(key: ValueKey(view.id)); - - final ViewPB view; - - @override - State createState() => _ViewLeftBarItemState(); -} - -class _ViewLeftBarItemState extends State { - final _controller = TextEditingController(); - final _focusNode = FocusNode(); - late final ViewListener _viewListener; - late ViewPB view; - - @override - void initState() { - super.initState(); - view = widget.view; - _focusNode.addListener(_handleFocusChanged); - _viewListener = ViewListener(viewId: widget.view.id); - _viewListener.start( - onViewUpdated: (updatedView) { - if (mounted) { - setState(() { - view = updatedView; - _controller.text = view.name; - }); - } - }, - ); - _controller.text = view.name; - } - - @override - void dispose() { - _controller.dispose(); - _focusNode.removeListener(_handleFocusChanged); - _focusNode.dispose(); - _viewListener.stop(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - key: ValueKey(_controller.text), - onDoubleTap: () { - _controller.selection = TextSelection( - baseOffset: 0, - extentOffset: _controller.text.length, - ); - }, - child: TextField( - controller: _controller, - focusNode: _focusNode, - scrollPadding: EdgeInsets.zero, - decoration: const InputDecoration( - contentPadding: EdgeInsets.symmetric(vertical: 4.0), - border: InputBorder.none, - isDense: true, - ), - style: Theme.of(context).textTheme.bodyMedium, - ), - ); - } - - void _handleFocusChanged() { - if (_controller.text.isEmpty) { - _controller.text = view.name; - return; - } - - if (_controller.text != view.name) { - ViewBackendService.updateView(viewId: view.id, name: _controller.text); - } - } -} diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj index 0357a297b3e90..13448a56c9b14 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/project.pbxproj @@ -206,7 +206,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { diff --git a/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 10d57fd28e3a1..74866db678246 100644 --- a/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/frontend/appflowy_flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ createState() => _PopoverMenuState(); @@ -77,11 +77,11 @@ class ExampleButton extends StatelessWidget { final PopoverDirection? direction; const ExampleButton({ - Key? key, + super.key, required this.label, this.direction, this.offset = Offset.zero, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart index 96c11dabbf14b..f4e017aa8f8a7 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/lib/main.dart @@ -7,7 +7,7 @@ void main() { } class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); // This widget is the root of your application. @override @@ -32,7 +32,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - const MyHomePage({Key? key, required this.title}) : super(key: key); + const MyHomePage({super.key, required this.title}); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/example/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/example/pubspec.yaml index 8a685fe20269a..8131f84949230 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/example/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/example/pubspec.yaml @@ -45,7 +45,7 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart index 30c744902e99f..f68fe954451d8 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/mask.dart @@ -70,8 +70,7 @@ class PopoverMask extends StatelessWidget { final void Function() onTap; final Decoration? decoration; - const PopoverMask({Key? key, required this.onTap, this.decoration}) - : super(key: key); + const PopoverMask({super.key, required this.onTap, this.decoration}); @override Widget build(BuildContext context) { 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 e55a6f6643e74..8a18b447a7e67 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart +++ b/frontend/appflowy_flutter/packages/appflowy_popover/lib/src/popover.dart @@ -163,8 +163,7 @@ class PopoverState extends State { return FocusScope( onKey: (node, event) { - if (event is RawKeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { + if (event.logicalKey == LogicalKeyboardKey.escape) { _removeRootOverlay(); return KeyEventResult.handled; } @@ -265,7 +264,7 @@ class PopoverContainer extends StatefulWidget { final void Function() onCloseAll; const PopoverContainer({ - Key? key, + super.key, required this.popupBuilder, required this.direction, required this.popoverLink, @@ -273,7 +272,7 @@ class PopoverContainer extends StatefulWidget { required this.windowPadding, required this.onClose, required this.onCloseAll, - }) : super(key: key); + }); @override State createState() => PopoverContainerState(); diff --git a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml index 137faaccae147..9dd11b27ac281 100644 --- a/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/appflowy_popover/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.1 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart index e6df4c7428c81..c797d4c513f09 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/icon_data.dart @@ -13,6 +13,7 @@ /// /// /// +library; // ignore_for_file: constant_identifier_names import 'package:flutter/widgets.dart'; diff --git a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml index d3cfa7c9c0194..6e6322f879625 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra/pubspec.yaml @@ -26,8 +26,8 @@ dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.2.0 - flutter_lints: ^2.0.1 - freezed: 2.2.0 + flutter_lints: ^3.0.1 + freezed: ^2.4.7 json_serializable: ^6.5.4 # For information on the generic Dart part of this file, see the diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart index 954930e9cd053..875440c04eacb 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/home/home_screen.dart @@ -5,7 +5,7 @@ import '../keyboard/keyboard_screen.dart'; import 'demo_item.dart'; class HomeScreen extends StatelessWidget { - const HomeScreen({Key? key}) : super(key: key); + const HomeScreen({super.key}); static List items = [ SectionHeaderItem('Widget Demos'), diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart index d7704366f2b1b..2b1a1cf59e5d7 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/keyboard/keyboard_screen.dart @@ -19,7 +19,7 @@ class KeyboardItem extends DemoItem { } class KeyboardScreen extends StatefulWidget { - const KeyboardScreen({Key? key}) : super(key: key); + const KeyboardScreen({super.key}); @override State createState() => _KeyboardScreenState(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart index 9554d93f41f83..ecaa6266c6891 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/main.dart @@ -9,7 +9,7 @@ void main() { } class ExampleApp extends StatelessWidget { - const ExampleApp({Key? key}) : super(key: key); + const ExampleApp({super.key}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart index ae5cf7ef67c47..bec711cc98676 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/lib/overlay/overlay_screen.dart @@ -44,7 +44,7 @@ class OverlayDemoConfiguration extends ChangeNotifier { } class OverlayScreen extends StatelessWidget { - const OverlayScreen({Key? key}) : super(key: key); + const OverlayScreen({super.key}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml index b9c8ec058e80e..3cd07738f89f8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/example/pubspec.yaml @@ -4,7 +4,8 @@ description: Demonstrates how to use the flowy_infra_ui plugin. publish_to: 'none' # Remove this line if you wish to publish to pub.dev environment: - sdk: ">=2.12.0 <3.0.0" + flutter: ">=3.19.0" + sdk: ">=3.1.5 <4.0.0" dependencies: flutter: @@ -20,7 +21,7 @@ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.1 flutter: uses-material-design: true diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml index 2f375be36765c..f2e3eb87491de 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_platform_interface/pubspec.yaml @@ -16,6 +16,6 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.1 flutter: \ No newline at end of file diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml index 224d7bf47feff..bbdac0d2e401b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/flowy_infra_ui_web/pubspec.yaml @@ -18,7 +18,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.1 flutter: plugin: diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart index 4978906e93a97..0345102d8c917 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/flowy_overlay.dart @@ -78,7 +78,7 @@ abstract mixin class FlowyOverlayDelegate { } class FlowyOverlay extends StatefulWidget { - const FlowyOverlay({Key? key, required this.child}) : super(key: key); + const FlowyOverlay({super.key, required this.child}); final Widget child; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart index eecc81283cc1d..2b61839ac349b 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/list_overlay.dart @@ -17,13 +17,13 @@ class ListOverlayFooter { class ListOverlay extends StatelessWidget { const ListOverlay({ - Key? key, + super.key, required this.itemBuilder, this.itemCount = 0, this.controller, this.constraints = const BoxConstraints(), this.footer, - }) : super(key: key); + }); final IndexedWidgetBuilder itemBuilder; final int itemCount; @@ -117,8 +117,8 @@ class OverlayContainer extends StatelessWidget { required this.child, this.constraints, this.padding = overlayContainerPadding, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart index d6dfc4607d6ff..5248665c410c0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/flowy_overlay/option_overlay.dart @@ -10,11 +10,11 @@ class OptionItem { class OptionOverlay extends StatelessWidget { const OptionOverlay({ - Key? key, + super.key, required this.items, this.onHover, this.onTap, - }) : super(key: key); + }); final List items; final IndexedValueCallback? onHover; @@ -69,8 +69,8 @@ class OptionOverlay extends StatelessWidget { class _OptionListItem extends StatelessWidget { const _OptionListItem( this.value, { - Key? key, - }) : super(key: key); + super.key, + }); final T value; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart index 45faebc07d901..148a812910db2 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/focus/auto_unfocus_overlay.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; class AutoUnfocus extends StatelessWidget { const AutoUnfocus({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); final Widget child; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart index 643ddd94b1d3b..cbe9a0790828d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/src/keyboard/keyboard_visibility_detector.dart @@ -5,10 +5,10 @@ import 'package:flutter/material.dart'; class KeyboardVisibilityDetector extends StatefulWidget { const KeyboardVisibilityDetector({ - Key? key, + super.key, required this.child, this.onKeyboardVisibilityChange, - }) : super(key: key); + }); final Widget child; final void Function(bool)? onKeyboardVisibilityChange; @@ -57,10 +57,9 @@ class _KeyboardVisibilityDetectorState class _KeyboardVisibilityDetectorInheritedWidget extends InheritedWidget { const _KeyboardVisibilityDetectorInheritedWidget({ - Key? key, required this.isKeyboardVisible, - required Widget child, - }) : super(key: key, child: child); + required super.child, + }); final bool isKeyboardVisible; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart index 47cd1cd8e9c33..cc54a8ba008f9 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/bar_title.dart @@ -4,9 +4,9 @@ class FlowyBarTitle extends StatelessWidget { final String title; const FlowyBarTitle({ - Key? key, + super.key, required this.title, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart index 237f3e5199820..e2cd3bc48e83f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/button.dart @@ -31,7 +31,7 @@ class FlowyButton extends StatelessWidget { final bool expand; const FlowyButton({ - Key? key, + super.key, required this.text, this.onTap, this.onSecondaryTap, @@ -52,7 +52,7 @@ class FlowyButton extends StatelessWidget { this.showDefaultBoxDecorationOnMobile = false, this.iconPadding = 6, this.expand = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -168,7 +168,7 @@ class FlowyTextButton extends StatelessWidget { // final HoverDisplayConfig? hoverDisplay; const FlowyTextButton( this.text, { - Key? key, + super.key, this.onPressed, this.fontSize, this.fontColor, @@ -184,7 +184,7 @@ class FlowyTextButton extends StatelessWidget { this.constraints = const BoxConstraints(minWidth: 0.0, minHeight: 0.0), this.decoration, this.fontFamily, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -260,7 +260,7 @@ class FlowyRichTextButton extends StatelessWidget { // final HoverDisplayConfig? hoverDisplay; const FlowyRichTextButton( this.text, { - Key? key, + super.key, this.onPressed, this.overflow = TextOverflow.ellipsis, this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6), @@ -272,7 +272,7 @@ class FlowyRichTextButton extends StatelessWidget { this.tooltip, this.constraints = const BoxConstraints(minWidth: 58.0, minHeight: 30.0), this.decoration, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart index 371ee123ed81e..94b1d3d40eea0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/close_button.dart @@ -4,9 +4,9 @@ class FlowyCloseButton extends StatelessWidget { final VoidCallback? onPressed; const FlowyCloseButton({ - Key? key, + super.key, this.onPressed, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart index 50b32d2f3bf97..afb4a787ed5b3 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/color_picker.dart @@ -24,7 +24,7 @@ class FlowyColorPicker extends StatelessWidget { final Border? border; const FlowyColorPicker({ - Key? key, + super.key, required this.colors, this.selected, this.onTap, @@ -32,7 +32,7 @@ class FlowyColorPicker extends StatelessWidget { this.iconSize = 16, this.itemHeight = 32, this.border, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart index 94db80afc2420..32f9809f685da 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/container.dart @@ -14,7 +14,7 @@ class FlowyContainer extends StatelessWidget { final BoxBorder? border; const FlowyContainer(this.color, - {Key? key, + {super.key, this.borderRadius, this.shadows, this.child, @@ -23,8 +23,7 @@ class FlowyContainer extends StatelessWidget { this.align, this.margin, this.duration, - this.border}) - : super(key: key); + this.border}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart index 585ae7b5a84c4..299eb76015f3d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/hover.dart @@ -24,7 +24,7 @@ class FlowyHover extends StatefulWidget { final bool Function()? buildWhenOnHover; const FlowyHover({ - Key? key, + super.key, this.builder, this.child, this.style, @@ -33,7 +33,7 @@ class FlowyHover extends StatefulWidget { this.cursor, this.resetHoverOnRebuild = true, this.buildWhenOnHover, - }) : super(key: key); + }); @override State createState() => _FlowyHoverState(); @@ -136,10 +136,10 @@ class FlowyHoverContainer extends StatelessWidget { final Widget child; const FlowyHoverContainer({ - Key? key, + super.key, required this.child, required this.style, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart index 3102f0276ed35..010c7cd6f901c 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/icon_button.dart @@ -23,7 +23,7 @@ class FlowyIconButton extends StatelessWidget { final bool? isSelected; const FlowyIconButton({ - Key? key, + super.key, this.width = 30, this.height, this.onPressed, @@ -40,8 +40,7 @@ class FlowyIconButton extends StatelessWidget { required this.icon, }) : assert((richTooltipText != null && tooltipText == null) || (richTooltipText == null && tooltipText != null) || - (richTooltipText == null && tooltipText == null)), - super(key: key); + (richTooltipText == null && tooltipText == null)); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart index 6c3810fb7b29a..f0c979b5cf134 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/image_icon.dart @@ -6,8 +6,7 @@ class FlowyImageIcon extends StatelessWidget { final Color? color; final double? size; - const FlowyImageIcon(this.image, {Key? key, this.color, this.size}) - : super(key: key); + const FlowyImageIcon(this.image, {super.key, this.color, this.size}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart index 8eb2a120471b4..ccf6608dd4a98 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/progress_indicator.dart @@ -13,7 +13,7 @@ List _kDefaultRainbowColors = const [ // CircularProgressIndicator() class FlowyProgressIndicator extends StatelessWidget { - const FlowyProgressIndicator({Key? key}) : super(key: key); + const FlowyProgressIndicator({super.key}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart index eaa0e43996c09..a3f262c070e3c 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_list.dart @@ -18,7 +18,7 @@ class StyledListView extends StatefulWidget { final IndexedWidgetBuilder itemBuilder; StyledListView({ - Key? key, + super.key, required this.itemBuilder, required this.itemCount, this.itemExtent, @@ -26,7 +26,7 @@ class StyledListView extends StatefulWidget { this.padding, this.barSize, this.scrollbarPadding, - }) : super(key: key) { + }) { assert(itemExtent != 0, 'Item extent should never be 0, null is ok.'); } diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart index 767ee68a5cafc..b3326d0e60e89 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scroll_bar.dart @@ -23,7 +23,7 @@ class StyledScrollbar extends StatefulWidget { final double? contentSize; const StyledScrollbar( - {Key? key, + {super.key, this.size, required this.axis, required this.controller, @@ -32,8 +32,7 @@ class StyledScrollbar extends StatefulWidget { this.showTrack = false, this.autoHideScrollbar = true, this.handleColor, - this.trackColor}) - : super(key: key); + this.trackColor}); @override ScrollbarState createState() => ScrollbarState(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart index d255555ca0df5..664dbc7ed1f3d 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/scrolling/styled_scrollview.dart @@ -16,7 +16,7 @@ class StyledSingleChildScrollView extends StatefulWidget { final Widget? child; const StyledSingleChildScrollView({ - Key? key, + super.key, required this.child, this.contentSize, this.axis = Axis.vertical, @@ -26,7 +26,7 @@ class StyledSingleChildScrollView extends StatefulWidget { this.scrollbarPadding, this.barSize = 8, this.autoHideScrollbar = true, - }) : super(key: key); + }); @override State createState() => @@ -89,14 +89,14 @@ class StyledCustomScrollView extends StatefulWidget { final double barSize; const StyledCustomScrollView({ - Key? key, + super.key, this.axis = Axis.vertical, this.trackColor, this.handleColor, this.verticalController, this.slivers = const [], this.barSize = 8, - }) : super(key: key); + }); @override StyledCustomScrollViewState createState() => StyledCustomScrollViewState(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart index 7aad277b11de7..8e3bcc9bf5172 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/style_widget/text_input.dart @@ -166,14 +166,11 @@ class StyledSearchTextInputState extends State { widget.controller ?? TextEditingController(text: widget.initialValue); _focusNode = FocusNode( debugLabel: widget.label ?? '', - onKey: (FocusNode node, RawKeyEvent evt) { - if (evt is RawKeyDownEvent) { - if (evt.logicalKey == LogicalKeyboardKey.escape) { - widget.onEditingCancel?.call(); - return KeyEventResult.handled; - } + onKeyEvent: (node, event) { + if (event.logicalKey == LogicalKeyboardKey.escape) { + widget.onEditingCancel?.call(); + return KeyEventResult.handled; } - return KeyEventResult.ignored; }, canRequestFocus: true, @@ -273,12 +270,12 @@ class ThinUnderlineBorder extends InputBorder { /// and right corners have a circular radius of 4.0. The [borderRadius] /// parameter must not be null. const ThinUnderlineBorder({ - BorderSide borderSide = const BorderSide(), + super.borderSide = const BorderSide(), this.borderRadius = const BorderRadius.only( topLeft: Radius.circular(4.0), topRight: Radius.circular(4.0), ), - }) : super(borderSide: borderSide); + }); /// The radii of the border's rounded rectangle corners. /// diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart index b9c5894b3af9d..ee51d400c9fba 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/base_styled_button.dart @@ -22,7 +22,7 @@ class BaseStyledButton extends StatefulWidget { final Color outlineColor; const BaseStyledButton({ - Key? key, + super.key, required this.child, this.onPressed, this.onFocusChanged, @@ -39,7 +39,7 @@ class BaseStyledButton extends StatefulWidget { this.useBtnText = true, this.autoFocus = false, this.outlineColor = Colors.transparent, - }) : super(key: key); + }); @override State createState() => BaseStyledBtnState(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart index dbdd47155d967..a2fe2f091e2a1 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/buttons/primary_button.dart @@ -9,8 +9,7 @@ class PrimaryTextButton extends StatelessWidget { final TextButtonMode mode; const PrimaryTextButton(this.label, - {Key? key, this.onPressed, this.mode = TextButtonMode.big}) - : super(key: key); + {super.key, this.onPressed, this.mode = TextButtonMode.big}); @override Widget build(BuildContext context) { @@ -31,11 +30,10 @@ class PrimaryButton extends StatelessWidget { final TextButtonMode mode; const PrimaryButton( - {Key? key, + {super.key, required this.child, this.onPressed, - this.mode = TextButtonMode.big}) - : super(key: key); + this.mode = TextButtonMode.big}); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart index 8fb60fda1dd6f..63a5dd5f609d8 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/constraint_flex_view.dart @@ -7,11 +7,10 @@ class ConstrainedFlexView extends StatelessWidget { final EdgeInsets scrollPadding; const ConstrainedFlexView(this.minSize, - {Key? key, + {super.key, required this.child, this.axis = Axis.horizontal, - this.scrollPadding = EdgeInsets.zero}) - : super(key: key); + this.scrollPadding = EdgeInsets.zero}); bool get isHz => axis == Axis.horizontal; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart index 74754fec2baa2..ffab50de4b6f0 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/dialog/styled_dialogs.dart @@ -10,10 +10,10 @@ extension IntoDialog on Widget { Future show(BuildContext context) async { FocusNode dialogFocusNode = FocusNode(); await Dialogs.show( - child: RawKeyboardListener( + child: KeyboardListener( focusNode: dialogFocusNode, - onKey: (value) { - if (value.isKeyPressed(LogicalKeyboardKey.escape)) { + onKeyEvent: (event) { + if (event.logicalKey == LogicalKeyboardKey.escape) { Navigator.of(context).pop(); } }, @@ -36,7 +36,7 @@ class StyledDialog extends StatelessWidget { final bool shrinkWrap; const StyledDialog({ - Key? key, + super.key, required this.child, this.maxWidth, this.maxHeight, @@ -45,7 +45,7 @@ class StyledDialog extends StatelessWidget { this.bgColor, this.borderRadius = const BorderRadius.all(Radius.circular(6)), this.shrinkWrap = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -126,11 +126,11 @@ class StyledDialogRoute extends PopupRoute { required this.barrier, Duration transitionDuration = const Duration(milliseconds: 300), RouteTransitionsBuilder? transitionBuilder, - RouteSettings? settings, + super.settings, }) : _pageBuilder = pageBuilder, _transitionDuration = transitionDuration, _transitionBuilder = transitionBuilder, - super(settings: settings, filter: barrier.filter); + super(filter: barrier.filter); @override bool get barrierDismissible { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart index 9a679775b4fbe..664d5cd492580 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/ignore_parent_gesture.dart @@ -2,10 +2,10 @@ import 'package:flutter/material.dart'; class IgnoreParentGestureWidget extends StatelessWidget { const IgnoreParentGestureWidget({ - Key? key, + super.key, required this.child, this.onPress, - }) : super(key: key); + }); final Widget child; final VoidCallback? onPress; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart index 81529ba16ca1e..dab9a4640dedd 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/mouse_hover_builder.dart @@ -6,8 +6,7 @@ class MouseHoverBuilder extends StatefulWidget { final bool isClickable; const MouseHoverBuilder( - {Key? key, required this.builder, this.isClickable = false}) - : super(key: key); + {super.key, required this.builder, this.isClickable = false}); final HoverBuilder builder; diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart index a329074d438ec..d4e9ed48c1f9f 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_button.dart @@ -15,7 +15,7 @@ class RoundedTextButton extends StatelessWidget { final double? fontSize; const RoundedTextButton({ - Key? key, + super.key, this.onPressed, this.title, this.width, @@ -26,7 +26,7 @@ class RoundedTextButton extends StatelessWidget { this.hoverColor, this.textColor, this.fontSize, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -63,14 +63,14 @@ class RoundedImageButton extends StatelessWidget { final Widget child; const RoundedImageButton({ - Key? key, + super.key, this.press, required this.size, this.borderRadius = BorderRadius.zero, this.borderColor = Colors.transparent, this.color = Colors.transparent, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart index 27949fb100b45..40978aa461234 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/rounded_input_field.dart @@ -29,7 +29,7 @@ class RoundedInputField extends StatefulWidget { final Function(String)? onFieldSubmitted; const RoundedInputField({ - Key? key, + super.key, this.hintText, this.errorText = "", this.initialValue, @@ -52,7 +52,7 @@ class RoundedInputField extends StatefulWidget { this.autoFocus = false, this.maxLength, this.onFieldSubmitted, - }) : super(key: key); + }); @override State createState() => _RoundedInputFieldState(); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart index abe813710c3a3..1abb5b8fdb68a 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/lib/widget/spacing.dart @@ -4,7 +4,7 @@ class Space extends StatelessWidget { final double width; final double height; - const Space(this.width, this.height, {Key? key}) : super(key: key); + const Space(this.width, this.height, {super.key}); @override Widget build(BuildContext context) => SizedBox(width: width, height: height); diff --git a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml index f55e604091c2f..816ac4d160842 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml +++ b/frontend/appflowy_flutter/packages/flowy_infra_ui/pubspec.yaml @@ -37,7 +37,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.1 + flutter_lints: ^3.0.1 flutter: plugin: diff --git a/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml b/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml index 9749d57481ae5..315807278ec47 100644 --- a/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml +++ b/frontend/appflowy_flutter/packages/flowy_svg/analysis_options.yaml @@ -1,3 +1,3 @@ -include: package:very_good_analysis/analysis_options.5.0.0.yaml +include: package:very_good_analysis/analysis_options.yaml linter: diff --git a/frontend/appflowy_flutter/pubspec.lock b/frontend/appflowy_flutter/pubspec.lock index f05536a7f64e2..48f4da5ac09f2 100644 --- a/frontend/appflowy_flutter/pubspec.lock +++ b/frontend/appflowy_flutter/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "66.0.0" + version: "67.0.0" analyzer: dependency: "direct dev" description: name: analyzer - sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.4.1" animations: dependency: transitive description: @@ -53,11 +53,11 @@ packages: dependency: "direct main" description: path: "." - ref: e80edfc - resolved-ref: e80edfcaaa402c606a642bf9bd1abf523ed15c80 + ref: f38328d + resolved-ref: f38328d9e52be89b8036ae0ad3460ce9d6cc5be7 url: "https://github.com/AppFlowy-IO/appflowy-editor.git" source: git - version: "2.3.1" + version: "2.3.2" appflowy_editor_plugins: dependency: "direct main" description: @@ -102,18 +102,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + sha256: f53a110e3b48dcd78136c10daa5d51512443cea5e1348c9d80a320095fa2db9e url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: "02f04270be5abae8df171143e61a0058a7acbce5dcac887612e89bb40cca4c33" + sha256: "55a48f69e0d480717067c5377c8485a3fcd41f1701a820deef72fa0f4ee7215f" url: "https://pub.dev" source: hosted - version: "9.1.5" + version: "9.1.6" boolean_selector: dependency: transitive description: @@ -166,10 +166,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.0" built_collection: dependency: transitive description: @@ -359,10 +359,10 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -399,10 +399,10 @@ packages: dependency: "direct main" description: name: easy_localization - sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5 + sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" easy_logger: dependency: transitive description: @@ -455,10 +455,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -559,10 +559,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + sha256: "87325da1ac757fcc4813e6b34ed5dd61169973871fdf181d6c2109dd6935ece1" url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.4" flutter_cache_manager: dependency: "direct main" description: @@ -692,10 +692,10 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + sha256: "57247f692f35f068cae297549a46a9a097100685c6780fe67177503eea5ed4e5" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.4.7" freezed_annotation: dependency: "direct main" description: @@ -745,10 +745,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "07ee2436909f749d606f53521dc1725dd738dc5196e5ff815bc254253c594075" + sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" url: "https://pub.dev" source: hosted - version: "13.1.0" + version: "13.2.0" google_fonts: dependency: "direct main" description: @@ -761,10 +761,10 @@ packages: dependency: transitive description: name: gotrue - sha256: "879ac3e981bf5f9b3af156e421688b53823b7af17de7af0d2a37894a8e5f6532" + sha256: f40610bacf1074723354b0856a4f586508ffb075b799f72466f34e843133deb9 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.5.0" graphs: dependency: transitive description: @@ -950,18 +950,18 @@ packages: dependency: transitive description: name: irondash_engine_context - sha256: "294a0e21c4358ff17264e6b811c615b664cebb33b49ad2ddb54f8110e7714510" + sha256: "4f5e2629296430cce08cdff42e47cef07b8f74a64fdbdfb0525d147bc1a969a2" url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.2" irondash_message_channel: dependency: transitive description: name: irondash_message_channel - sha256: "131d64d97a3612bc3617aefc878f3e3a8e23e0ce18b3bba8e78cb1930befcec1" + sha256: dd581214215dca054bd9873209d690ec3609288c28774cb509dbd86b21180cf8 url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.6.0" isolates: dependency: transitive description: @@ -1014,18 +1014,26 @@ packages: dependency: "direct main" description: name: leak_tracker - sha256: "04be76c4a4bb50f14904e64749237e541e7c7bcf7ec0b196907322ab5d2fc739" + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 url: "https://pub.dev" source: hosted - version: "9.0.16" + version: "2.0.1" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: b06739349ec2477e943055aea30172c5c7000225f79dad4702e2ec0eda79a6ff + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "2.0.1" linked_scroll_controller: dependency: "direct main" description: @@ -1102,10 +1110,10 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -1222,10 +1230,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -1310,18 +1318,18 @@ packages: dependency: transitive description: name: pixel_snap - sha256: d31591a4f4aa8ed5dc6fc00b8d027338a5614dfbf5ca658b69d1faa7aba80af7 + sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" url: "https://pub.dev" source: hosted - version: "0.1.4" + version: "0.1.5" platform: dependency: transitive description: name: platform - sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" plugin_platform_interface: dependency: "direct dev" description: @@ -1358,10 +1366,10 @@ packages: dependency: transitive description: name: process - sha256: "266ca5be5820feefc777793d0a583acfc8c40834893c87c00c6c09e2cf58ea42" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.0.2" protobuf: dependency: "direct main" description: @@ -1526,10 +1534,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: @@ -1630,10 +1638,10 @@ packages: dependency: transitive description: name: simple_gesture_detector - sha256: "86d08f85f1f58583b7b4b941d989f48ea6ce08c1724a1d10954a277c2ec36592" + sha256: ba2cd5af24ff20a0b8d609cec3f40e5b0744d2a71804a2616ae086b9c19d19a3 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" sized_context: dependency: "direct main" description: @@ -1731,10 +1739,10 @@ packages: dependency: transitive description: name: storage_client - sha256: b49ff2e1e6738c0ef445546d6ec77040829947f0c7ef0b115acb125656127c83 + sha256: bf5589d5de61a2451edb1b8960a0e673d4bb5c42ecc4dddf7c051a93789ced34 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" stream_channel: dependency: transitive description: @@ -1779,10 +1787,10 @@ packages: dependency: transitive description: name: supabase - sha256: f299614f6f44ee26ff1bd293869272fab79a7b087320c3b40572792b32ded06c + sha256: "4bce9c49f264f4cd44b4ffc895647af2dca0c40125c169045be9f708fd2a2a40" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.0.7" supabase_flutter: dependency: "direct main" description: @@ -1796,18 +1804,18 @@ packages: dependency: "direct main" description: name: super_clipboard - sha256: "77f044320934386e0b7a3911e05312426d7f33deb6e8cdb28886663430b0e5b0" + sha256: "15d25eb88df8e904e0c2ef77378c6010cc57bbfc0b6f91f2416d08fad5fcca92" url: "https://pub.dev" source: hosted - version: "0.8.4" + version: "0.8.5" super_native_extensions: dependency: transitive description: name: super_native_extensions - sha256: "4699f5b00320290475953c914f823a8b44e10ed8c1e38ce5c8e8426336d11c15" + sha256: f96db6b137a0b135e43034289bb55ca6447b65225076036e81f97ebb6381ffeb url: "https://pub.dev" source: hosted - version: "0.8.4" + version: "0.8.5" sync_http: dependency: transitive description: @@ -1948,10 +1956,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" url_launcher_ios: dependency: transitive description: @@ -1980,10 +1988,10 @@ packages: dependency: "direct dev" description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: @@ -2029,26 +2037,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "4ac59808bbfca6da38c99f415ff2d3a5d7ca0a6b4809c71d9cf30fba5daf9752" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: f3247e7ab0ec77dc759263e68394990edc608fb2b480b80db8aa86ed09279e33 url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "18489bdd8850de3dd7ca8a34e0c446f719ec63e2bab2e7a8cc66a9028dd76c5a" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.10+1" vector_math: dependency: transitive description: @@ -2085,10 +2093,10 @@ packages: dependency: transitive description: name: web - sha256: edc8a9573dd8c5a83a183dae1af2b6fd4131377404706ca4e5420474784906fa + sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.2" web_socket_channel: dependency: transitive description: @@ -2170,5 +2178,5 @@ packages: source: hosted version: "2.0.0" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.18.0-0.2.pre" + dart: ">=3.3.0-279.1.beta <4.0.0" + flutter: ">=3.19.0" diff --git a/frontend/appflowy_flutter/pubspec.yaml b/frontend/appflowy_flutter/pubspec.yaml index 9e81b44df5613..bd2d71207702a 100644 --- a/frontend/appflowy_flutter/pubspec.yaml +++ b/frontend/appflowy_flutter/pubspec.yaml @@ -18,7 +18,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev version: 0.4.9 environment: - flutter: ">=3.18.0-0.2.pre" + flutter: ">=3.19.0" sdk: ">=3.1.5 <4.0.0" # Dependencies specify other packages that your package needs in order to work. @@ -124,7 +124,7 @@ dependencies: image_picker: ^1.0.4 image_gallery_saver: ^2.0.3 cached_network_image: ^3.3.0 - leak_tracker: ^9.0.6 + leak_tracker: ^10.0.0 keyboard_height_plugin: ^0.0.5 scrollable_positioned_list: ^0.3.8 flutter_cache_manager: ^3.3.1 @@ -139,7 +139,7 @@ dev_dependencies: integration_test: sdk: flutter build_runner: ^2.4.4 - freezed: ^2.3.4 + freezed: ^2.4.7 bloc_test: ^9.1.2 json_serializable: ^6.7.0 envied_generator: ^0.5.2 @@ -165,7 +165,7 @@ dependency_overrides: appflowy_editor: git: url: https://github.com/AppFlowy-IO/appflowy-editor.git - ref: "e80edfc" + ref: "f38328d" uuid: ^4.1.0 diff --git a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart b/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart index 9f0e28effcc58..f4bfbc7c1aee4 100644 --- a/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart +++ b/frontend/appflowy_flutter/test/widget_test/workspace/settings/shortcuts_list_tile_test.dart @@ -66,7 +66,7 @@ void main() { await widgetTester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); - expect(find.byType(RawKeyboardListener), findsOneWidget); + expect(find.byType(KeyboardListener), findsOneWidget); }); testWidgets("updates the text with new key event", @@ -83,7 +83,7 @@ void main() { await widgetTester.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); - expect(find.byType(RawKeyboardListener), findsOneWidget); + expect(find.byType(KeyboardListener), findsOneWidget); await widgetTester.sendKeyEvent(LogicalKeyboardKey.keyC); diff --git a/frontend/scripts/docker-buildfiles/Dockerfile b/frontend/scripts/docker-buildfiles/Dockerfile index 3f58943c88cdb..e9f73d3d0c867 100644 --- a/frontend/scripts/docker-buildfiles/Dockerfile +++ b/frontend/scripts/docker-buildfiles/Dockerfile @@ -39,7 +39,7 @@ RUN source ~/.cargo/env && \ RUN sudo pacman -S --noconfirm git tar gtk3 RUN curl -sSfL \ --output flutter.tar.xz \ - https://storage.googleapis.com/flutter_infra_release/releases/beta/linux/flutter_linux_3.18.0-0.2.pre-beta.tar.xz && \ + https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.19.0-stable.tar.xz && \ tar -xf flutter.tar.xz && \ rm flutter.tar.xz RUN flutter config --enable-linux-desktop diff --git a/frontend/scripts/install_dev_env/install_ios.sh b/frontend/scripts/install_dev_env/install_ios.sh index 29b44deaca85f..0ce5cfb5d428a 100644 --- a/frontend/scripts/install_dev_env/install_ios.sh +++ b/frontend/scripts/install_dev_env/install_ios.sh @@ -44,9 +44,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.18.0-0.2.pre2 -if [ "$FLUTTER_VERSION" = "3.18.0-0.2.pre2" ]; then - echo "Flutter version is already 3.18.0-0.2.pre2" +# Check if the current version is 3.19.0 +if [ "$FLUTTER_VERSION" = "3.19.0" ]; then + echo "Flutter version is already 3.19.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -55,12 +55,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.18.0-0.2.pre2 of Flutter - git checkout 3.18.0-0.2.pre2 + # Use git to checkout version 3.19.0 of Flutter + git checkout 3.19.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.18.0-0.2.pre2" + echo "Switched to Flutter version 3.19.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_linux.sh b/frontend/scripts/install_dev_env/install_linux.sh index 0477791ce6fb7..d1f85445a2673 100755 --- a/frontend/scripts/install_dev_env/install_linux.sh +++ b/frontend/scripts/install_dev_env/install_linux.sh @@ -38,9 +38,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.18.0-0.2.pre2 -if [ "$FLUTTER_VERSION" = "3.18.0-0.2.pre2" ]; then - echo "Flutter version is already 3.18.0-0.2.pre2" +# Check if the current version is 3.19.0 +if [ "$FLUTTER_VERSION" = "3.19.0" ]; then + echo "Flutter version is already 3.19.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -49,12 +49,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.18.0-0.2.pre2 of Flutter - git checkout 3.18.0-0.2.pre2 + # Use git to checkout version 3.19.0 of Flutter + git checkout 3.19.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.18.0-0.2.pre2" + echo "Switched to Flutter version 3.19.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_macos.sh b/frontend/scripts/install_dev_env/install_macos.sh index 763e655d88750..10f894e13c525 100755 --- a/frontend/scripts/install_dev_env/install_macos.sh +++ b/frontend/scripts/install_dev_env/install_macos.sh @@ -41,9 +41,9 @@ printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oE 'Flutter [^ ]+' | grep -oE '[^ ]+$') -# Check if the current version is 3.18.0-0.2.pre2 -if [ "$FLUTTER_VERSION" = "3.18.0-0.2.pre2" ]; then - echo "Flutter version is already 3.18.0-0.2.pre2" +# Check if the current version is 3.19.0 +if [ "$FLUTTER_VERSION" = "3.19.0" ]; then + echo "Flutter version is already 3.19.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -52,12 +52,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.18.0-0.2.pre2 of Flutter - git checkout 3.18.0-0.2.pre2 + # Use git to checkout version 3.19.0 of Flutter + git checkout 3.19.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.18.0-0.2.pre2" + echo "Switched to Flutter version 3.19.0" fi # Enable linux desktop diff --git a/frontend/scripts/install_dev_env/install_windows.sh b/frontend/scripts/install_dev_env/install_windows.sh index 56afaf998934e..aef80844a0e0a 100644 --- a/frontend/scripts/install_dev_env/install_windows.sh +++ b/frontend/scripts/install_dev_env/install_windows.sh @@ -48,9 +48,9 @@ fi printMessage "Setting up Flutter" # Get the current Flutter version FLUTTER_VERSION=$(flutter --version | grep -oP 'Flutter \K\S+') -# Check if the current version is 3.18.0-0.2.pre2 -if [ "$FLUTTER_VERSION" = "3.18.0-0.2.pre2" ]; then - echo "Flutter version is already 3.18.0-0.2.pre2" +# Check if the current version is 3.19.0 +if [ "$FLUTTER_VERSION" = "3.19.0" ]; then + echo "Flutter version is already 3.19.0" else # Get the path to the Flutter SDK FLUTTER_PATH=$(which flutter) @@ -59,12 +59,12 @@ else current_dir=$(pwd) cd $FLUTTER_PATH - # Use git to checkout version 3.18.0-0.2.pre2 of Flutter - git checkout 3.18.0-0.2.pre2 + # Use git to checkout version 3.19.0 of Flutter + git checkout 3.19.0 # Get back to current working directory cd "$current_dir" - echo "Switched to Flutter version 3.18.0-0.2.pre2" + echo "Switched to Flutter version 3.19.0" fi # Add pub cache and cargo to PATH From 26f8bbf7c6ca9e37d3d9f4ede166055c71d8619f Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 19 Feb 2024 16:24:47 +0700 Subject: [PATCH 45/50] feat: optimize image upload process and display an error message if upload fails (#4679) * chore: optimize image upload * feat: show upload image status * chore: upload the ai image to cloud server --- .../document/application/doc_service.dart | 2 + .../header/document_header_node_widget.dart | 2 +- .../image/image_placeholder.dart | 39 +++++++++++++------ .../editor_plugins/image/image_util.dart | 9 +++-- .../image/resizeable_image.dart | 1 + .../lib/shared/cloud_image_checker.dart | 20 ++++++++++ frontend/resources/translations/en.json | 3 +- 7 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart 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 74684a19d59a3..9009ea52ffe08 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/application/doc_service.dart @@ -104,12 +104,14 @@ class DocumentService { /// Upload a file to the cloud storage. Future> uploadFile({ required String localFilePath, + bool isAsync = true, }) async { final workspace = await FolderEventReadCurrentWorkspace().send(); return workspace.fold((l) async { final payload = UploadFileParamsPB( workspaceId: l.id, localFilePath: localFilePath, + isAsync: isAsync, ); final result = await DocumentEventUploadFile(payload).send(); return result.swap(); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart index 49a85406dbfc8..784a7f246c184 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart @@ -615,7 +615,7 @@ class DocumentCoverState extends State { details = await saveImageToLocalStorage(details); } else { // else we should save the image to cloud storage - details = await saveImageToCloudStorage(details); + (details, _) = await saveImageToCloudStorage(details); } } widget.onChangeCover(type, details); diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart index 3a680d1c6bda1..064580b75272d 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_placeholder.dart @@ -44,6 +44,8 @@ class ImagePlaceholderState extends State { final documentService = DocumentService(); late final editorState = context.read(); + bool showLoading = false; + @override Widget build(BuildContext context) { final Widget child = DecoratedBox( @@ -65,9 +67,19 @@ class ImagePlaceholderState extends State { size: Size.square(24), ), const HSpace(10), - FlowyText( - LocaleKeys.document_plugins_image_addAnImage.tr(), - ), + ...showLoading + ? [ + FlowyText( + LocaleKeys.document_imageBlock_imageIsUploading.tr(), + ), + const HSpace(8), + const CircularProgressIndicator.adaptive(), + ] + : [ + FlowyText( + LocaleKeys.document_plugins_image_addAnImage.tr(), + ), + ], ], ), ), @@ -188,6 +200,7 @@ class ImagePlaceholderState extends State { final transaction = editorState.transaction; String? path; + String? errorMessage; CustomImageType imageType = CustomImageType.local; // if the user is using local authenticator, we need to save the image to local storage @@ -195,14 +208,22 @@ class ImagePlaceholderState extends State { path = await saveImageToLocalStorage(url); } else { // else we should save the image to cloud storage - path = await saveImageToCloudStorage(url); + setState(() { + showLoading = true; + }); + (path, errorMessage) = await saveImageToCloudStorage(url); + setState(() { + showLoading = false; + }); imageType = CustomImageType.internal; } if (mounted && path == null) { showSnackBarMessage( context, - LocaleKeys.document_imageBlock_error_invalidImage.tr(), + errorMessage == null + ? LocaleKeys.document_imageBlock_error_invalidImage.tr() + : ': $errorMessage', ); return; } @@ -244,12 +265,8 @@ class ImagePlaceholderState extends State { final response = await get(uri); await File(copyToPath).writeAsBytes(response.bodyBytes); - - final transaction = editorState.transaction; - transaction.updateNode(widget.node, { - ImageBlockKeys.url: copyToPath, - }); - await editorState.apply(transaction); + await insertLocalImage(copyToPath); + await File(copyToPath).delete(); } catch (e) { Log.error('cannot save image file', e); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart index 12fbf93b0772c..d6a31bb7eaab8 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/image_util.dart @@ -34,19 +34,22 @@ Future saveImageToLocalStorage(String localImagePath) async { } } -Future saveImageToCloudStorage(String localImagePath) async { +Future<(String? path, String? errorMessage)> saveImageToCloudStorage( + String localImagePath, +) async { final documentService = DocumentService(); final result = await documentService.uploadFile( localFilePath: localImagePath, + isAsync: false, ); return result.fold( - (l) => null, + (l) => (null, l.msg), (r) async { await CustomImageCacheManager().putFile( r.url, File(localImagePath).readAsBytesSync(), ); - return r.url; + return (r.url, null); }, ); } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart index 7b04ca3119f68..5038e781a0309 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/image/resizeable_image.dart @@ -102,6 +102,7 @@ class _ResizableImageState extends State { progressIndicatorBuilder: (context, url, progress) => _buildLoading(context), ); + child = _cacheImage!; } else { // load local file diff --git a/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart new file mode 100644 index 0000000000000..5a7bac2c75e4f --- /dev/null +++ b/frontend/appflowy_flutter/lib/shared/cloud_image_checker.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'; +import 'package:http/http.dart' as http; + +Future isImageExistOnCloud({ + required String url, + required UserProfilePB userProfilePB, +}) async { + final header = {}; + final token = userProfilePB.token; + try { + final decodedToken = jsonDecode(token); + header['Authorization'] = 'Bearer ${decodedToken['access_token']}'; + final response = await http.get(Uri.http(url), headers: header); + return response.statusCode == 200; + } catch (_) { + return false; + } +} diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index 70fb0fffc1b5b..4b5c29da69a63 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -862,7 +862,8 @@ "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" + "uploadImageErrorImageSizeTooBig": "Image size must be less than 10MB", + "imageIsUploading": "Image is uploading" }, "codeBlock": { "language": { From 2b6b10788d243e9d427e56a22dbb0255e603a271 Mon Sep 17 00:00:00 2001 From: Arcesilas Date: Mon, 19 Feb 2024 10:25:00 +0100 Subject: [PATCH 46/50] chore: update fr-FR and fr-CA translations (#4676) Co-authored-by: Lucas.Xu --- frontend/resources/translations/fr-CA.json | 16 ++++++++-------- frontend/resources/translations/fr-FR.json | 17 ++++++++--------- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/frontend/resources/translations/fr-CA.json b/frontend/resources/translations/fr-CA.json index 68e9068061c16..022a097001155 100644 --- a/frontend/resources/translations/fr-CA.json +++ b/frontend/resources/translations/fr-CA.json @@ -74,12 +74,12 @@ "copyLink": "Copier le lien" }, "moreAction": { - "fontSize": "Taille de police", - "import": "Importer", - "moreOptions": "Plus d'options", "small": "petit", "medium": "moyen", - "large": "grand" + "large": "grand", + "fontSize": "Taille de police", + "import": "Importer", + "moreOptions": "Plus d'options" }, "importPanel": { "textAndMarkdown": "Texte et Markdown", @@ -726,7 +726,7 @@ "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", "createInlineMathEquation": "Créer une équation", "fonts": "Polices", - "toggleList": "Basculer la liste", + "toggleList": "Liste pliable", "quoteList": "Liste de citations", "numberedList": "Liste numérotée", "bulletedList": "Liste à puces", @@ -959,10 +959,10 @@ "layoutDateField": "Calendrier de mise en page par", "changeLayoutDateField": "Modifier le champ de mise en page", "noDateTitle": "Pas de date", - "noDateHint": "Les événements non planifiés s'afficheront ici", "unscheduledEventsTitle": "Événements non planifiés", "clickToAdd": "Cliquez pour ajouter au calendrier", - "name": "Disposition du calendrier" + "name": "Disposition du calendrier", + "noDateHint": "Les événements non planifiés s'afficheront ici" }, "referencedCalendarPrefix": "Vue", "quickJumpYear": "Sauter à" @@ -1238,7 +1238,7 @@ "rowClear": "Effacer le ontenu", "slashPlaceHolder": "Tapez '/' pour insérer un bloc ou commencez à écrire", "typeSomething": "Écrivez quelque chose...", - "toggleListShortForm": "Basculer", + "toggleListShortForm": "Plier / Déplier", "quoteListShortForm": "Citation", "mathEquationShortForm": "Formule", "codeBlockShortForm": "Code" diff --git a/frontend/resources/translations/fr-FR.json b/frontend/resources/translations/fr-FR.json index 683b29c649aca..5c372dd6fc072 100644 --- a/frontend/resources/translations/fr-FR.json +++ b/frontend/resources/translations/fr-FR.json @@ -74,12 +74,12 @@ "copyLink": "Copier le lien" }, "moreAction": { - "fontSize": "Taille de police", - "import": "Importer", - "moreOptions": "Plus d'options", "small": "petit", "medium": "moyen", - "large": "grand" + "large": "grand", + "fontSize": "Taille de police", + "import": "Importer", + "moreOptions": "Plus d'options" }, "importPanel": { "textAndMarkdown": "Texte et Markdown", @@ -723,7 +723,7 @@ "discardResponse": "Voulez-vous supprimer les réponses de l'IA ?", "createInlineMathEquation": "Créer une équation", "fonts": "Polices", - "toggleList": "Basculer la liste", + "toggleList": "Liste pliable", "quoteList": "Liste de citations", "numberedList": "Liste numérotée", "bulletedList": "Liste à puces", @@ -883,8 +883,8 @@ "label": "Lien vers la page", "tooltip": "Cliquez pour ouvrir la page" }, - "deletedContent": "Ce document n'existe pas ou a été supprimé", - "deleted": "Supprimer" + "deleted": "Supprimer", + "deletedContent": "Ce document n'existe pas ou a été supprimé" }, "toolbar": { "resetToDefaultFont": "Réinitialiser aux valeurs par défaut" @@ -956,7 +956,6 @@ "layoutDateField": "Calendrier de mise en page par", "changeLayoutDateField": "Modifier le champ de mise en page", "noDateTitle": "Pas de date", - "noDateHint": "Les événements non planifiés s'afficheront ici", "unscheduledEventsTitle": "Événements non planifiés", "clickToAdd": "Cliquez pour ajouter au calendrier", "name": "Disposition du calendrier", @@ -1236,7 +1235,7 @@ "rowClear": "Effacer le ontenu", "slashPlaceHolder": "Tapez '/' pour insérer un bloc ou commencez à écrire", "typeSomething": "Écrivez quelque chose...", - "toggleListShortForm": "Basculer", + "toggleListShortForm": "Plier / Déplier", "quoteListShortForm": "Citation", "mathEquationShortForm": "Formule", "codeBlockShortForm": "Code" From 8eaadccda054726dcd7ceaf75c7a8cac03034b41 Mon Sep 17 00:00:00 2001 From: Sore <88344148+SoranTabesh@users.noreply.github.com> Date: Mon, 19 Feb 2024 13:13:01 +0330 Subject: [PATCH 47/50] chore: add ckb-KUR translations (#4658) * Create ckb-KUR.json Add Translate (Kurdish-ckb Language) * Update language.dart i added ckb lang(kurdish). * Update app_widget.dart --- .../lib/startup/tasks/app_widget.dart | 1 + .../packages/flowy_infra/lib/language.dart | 5 + frontend/resources/translations/ckb-KUR.json | 677 ++++++++++++++++++ 3 files changed, 683 insertions(+) create mode 100644 frontend/resources/translations/ckb-KUR.json diff --git a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart index d167684db72a6..2c9d66d43f3fb 100644 --- a/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart +++ b/frontend/appflowy_flutter/lib/startup/tasks/app_widget.dart @@ -62,6 +62,7 @@ class InitAppWidgetTask extends LaunchTask { Locale('am', 'ET'), Locale('ar', 'SA'), Locale('ca', 'ES'), + Locale('ckb', 'KU'), Locale('de', 'DE'), Locale('en'), Locale('es', 'VE'), diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index 7fed756a7fbaa..fc3c61515002e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -22,6 +22,11 @@ String languageFromLocale(Locale locale) { return "العربية"; case "ca": return "Català"; + case "ckb": + switch (locale.countryCode) { + case "KU": + return "کوردی"; + } case "de": return "Deutsch"; case "es": diff --git a/frontend/resources/translations/ckb-KUR.json b/frontend/resources/translations/ckb-KUR.json new file mode 100644 index 0000000000000..647a488239656 --- /dev/null +++ b/frontend/resources/translations/ckb-KUR.json @@ -0,0 +1,677 @@ +{ + "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": "چوونه‌ژووره‌وه‌", + "continueAnonymousUser": "وەک بەکارهێنەری میوان بەردەوام بە", + "buttonText": "چوونه‌ژووره‌وه‌", + "forgotPassword": "تێپه‌ڕه‌وشەت لەبیر كردووە ؟", + "emailHint": "📧️ئیمەیڵ", + "passwordHint": "🔑️تێپه‌ڕه‌وشه", + "dontHaveAnAccount": "ئەکاونتت نییە؟", + "repeatPasswordEmptyError": "ناتوانرێت تێپه‌ڕه‌وشه بەتاڵ بێت", + "unmatchedPasswordError": "دووبارەکراوەی تێپه‌ڕه‌وشه هەمان تێپه‌ڕه‌وشه نییە", + "signInWith": "ناونووسین وە:", + "loginAsGuestButtonText": "دەست پێ بکە" + }, + "workspace": { + "create": "دروستکردنی شوێنی کارکردن", + "hint": "شوێنی کارکردن", + "notFoundError": "هیچ شوێنێکی کار نەدۆزراوە" + }, + "shareAction": { + "buttonText": "هاوبەشکردن", + "workInProgress": "بەم زووانە", + "markdown": "Markdown", + "copyLink": "کۆپی کردنی لینک" + }, + "moreAction": { + "small": "بچووک", + "medium": "ناوەند", + "large": "گەورە", + "fontSize": "قەبارەی قەڵەم", + "import": "زیادکردن", + "moreOptions": "بژاردەی زیاتر" + }, + "importPanel": { + "textAndMarkdown": "Text & Markdown", + "documentFromV010": "به‌ڵگه‌نامه لە وەشانی 0.1.0", + "databaseFromV010": "داتابەیس لە وەشانی 0.1.0", + "csv": "CSV", + "database": "🪪️داتابەیس-بنکەدراوە" + }, + "disclosureAction": { + "rename": "گۆڕینی ناو", + "delete": "سڕینەوە", + "duplicate": "دووبارەکردنەوە", + "unfavorite": "سڕینەوە لە دڵخوازەکان", + "favorite": "خستنە لیستی هەڵبژاردەکان", + "openNewTab": "لە تابێکی نوێدا بکرێتەوە", + "moveTo": "گواستنەوە بۆ", + "addToFavorites": "خستنە لیستی هەڵبژاردەکان", + "copyLink": "کۆپی کردنی لینک" + }, + "blankPageTitle": "لاپەڕەی بەتاڵ", + "newPageText": "لاپەڕەی نوێ", + "trash": { + "text": "زبڵدان", + "restoreAll": "گەڕاندنەوەی هەموو", + "deleteAll": "هەمووی بسڕەوە", + "pageHeader": { + "fileName": "ناوی پەڕگە", + "lastModified": "دوایین پیاچوونەوە", + "created": "دروستکراوە" + }, + "confirmDeleteAll": { + "title": "دەتەوێت هەموو لاپەڕەکانی ناو زبڵدان بسڕیتەوە؟", + "caption": "ئەم کارە پێچەوانە نابێتەوە." + }, + "confirmRestoreAll": { + "title": "ئایا دەتەوێت هەموو لاپەڕەکانی ناو زبڵدانەکە بگەڕێنێتەوە؟", + "caption": "ئەم کارە پێچەوانە نابێتەوە." + } + }, + "deletePagePrompt": { + "text": "ئەم لاپەڕەیە لە زبڵداندایە", + "restore": "گەڕانەوەی لاپەڕە", + "deletePermanent": "سڕینەوەی هەمیشەیی" + }, + "dialogCreatePageNameHint": "ناوی لاپەڕە", + "questionBubble": { + "shortcuts": "کورتە ڕێگاکان", + "whatsNew": "نوێترین", + "help": "پشتیوانی و یارمەتی", + "markdown": "Markdown", + "debug": { + "name": "زانیاری دیباگ", + "success": "زانیارییەکانی دیباگ کۆپی کراون بۆ کلیپبۆرد!", + "fail": "ناتوانرێت زانیارییەکانی دیباگ کۆپی بکات بۆ کلیپبۆرد" + }, + "feedback": "فیدباک" + }, + "menuAppHeader": { + "moreButtonToolTip": "سڕینەوە، گۆڕینی ناو، و زۆر شتی تر...", + "addPageTooltip": "لاپەڕەیەک لە ناوەوە زیاد بکە", + "defaultNewPageName": "بێ ناونیشان", + "renameDialog": "گۆڕینی ناو" + }, + "toolbar": { + "undo": "پاشەکشە", + "redo": "Redo", + "bold": "تۆخ", + "italic": "لار", + "underline": "هێڵ بەژێرداهێنان", + "strike": "لەگەڵ هێڵ لە ناوەڕاستدا", + "numList": "لیستی ژمارەدار", + "bulletList": "بووڵت لیست", + "checkList": "لیستی پشکنین", + "inlineCode": "کۆدی ناو هێڵ", + "quote": "ده‌ق", + "header": "سه‌رپه‌ڕه‌", + "highlight": "بەرجەستەکردن⚡️", + "color": "ڕەنگ", + "addLink": "زیادکردنی لینک", + "link": "🔗️لینک" + }, + "tooltip": { + "lightMode": "مۆدی کاڵ/لاییت", + "darkMode": "مۆدی تاریک", + "openAsPage": "کردنەوە وەک لاپەڕە", + "addNewRow": "زیادکردنی ڕیزێکی نوێ", + "openMenu": "Menu کردنەوەی", + "dragRow": "بۆ ڕێکخستنەوەی ڕیزەکە فشارێکی درێژ بکە", + "viewDataBase": "بینینی بنکەدراوە", + "referencePage": "ئەم {name} ڕەوانە کراوە", + "addBlockBelow": "لە خوارەوە بلۆکێک زیاد بکە" + }, + "sideBar": { + "closeSidebar": "داخستنی سایدبار", + "openSidebar": "کردنەوەی سایدبار", + "personal": "کەسی", + "favorites": "دڵخوازەکان", + "clickToHidePersonal": "بۆ شاردنەوەی بەشی کەسی کلیک بکە", + "clickToHideFavorites": "بۆ شاردنەوەی بەشی دڵخوازەکان کلیک بکە", + "addAPage": "زیاد کردنی لاپەڕەیەک" + }, + "notifications": { + "export": { + "markdown": "گۆڕینی دەق بۆ تێبینی", + "path": "Documents/flowy" + } + }, + "contactsPage": { + "title": "پەیوەندییەکان", + "whatsHappening": "چی ڕوودەدات", + "addContact": "زیادکردنی پەیوەندی", + "editContact": "دەستکاریکردنی پەیوەندی" + }, + "button": { + "done": "ئەنجامدرا", + "signIn": "چوونە ژوورەوە", + "signOut": "دەرچوون", + "complete": "تەواوە", + "save": "پاشەکەوتکردن", + "generate": "دروستکردن", + "esc": "ESC", + "keep": "پاراستن", + "tryAgain": "جارێکی تر هەوڵبدەرەوە", + "discard": "ڕەتکردنەوە", + "replace": "شوێن گرتنەوە", + "insertBelow": "insert لە خوارەوە", + "upload": "بارکردن...", + "edit": "بژارکردن", + "delete": "سڕینەوە", + "duplicate": "هاوشێوە کردن", + "putback": "بیخەرەوە بۆ دواوە", + "Done": "تەواوه", + "Cancel": "ڕەتکردن", + "OK": "ئۆکەی" + }, + "label": { + "welcome": "بەخێربێن!", + "firstName": "ناوی یەکەم", + "middleName": "ناوی ناوەڕاست", + "lastName": "ناوی کۆتایی", + "stepX": "Step {X}" + }, + "oAuth": { + "err": { + "failedTitle": "ناتوانرێت پەیوەندی بە ئەکاونتەکەتەوە بکرێت", + "failedMsg": "تکایە دڵنیابە لە تەواوکردنی پرۆسەی چوونەژوورەوە لە وێبگەڕەکەتدا." + }, + "google": { + "title": "چوونە ژوورەوە بە ئەکاونتی گووگڵ", + "instruction1": "بۆ دەستگەیشتن بە کانتەکتەکان لە گووگڵ، پێویستە لە ڕێگەی وێبگەڕەکەتەوە بچیتە ناو ئەم بەرنامەیە.", + "instruction2": "ئەم کۆدە بە کرتەکردن لەسەر ئایکۆن یان دەق هەڵبژێرە کۆپی بکە بۆ کلیپبۆردەکەت:", + "instruction3": "لە وێبگەڕەکەتدا بڕۆ بۆ ئەم بەستەرەی خوارەوە و ئەو کۆدەی سەرەوە دابنێ:", + "instruction4": "دوای تەواوکردنی ناو نووسین، کرتە بکە سەر ئەم دوگمەیەی خوارەوە" + } + }, + "settings": { + "title": "ڕێکخستنەکان", + "menu": { + "appearance": "ڕووکار", + "language": "زمانەکان", + "user": "بەکارهێنەر", + "files": "فایلەکان", + "open": "کردنەوەی ڕێکخستنەکان", + "logout": "دەرچوون", + "logoutPrompt": "دڵنیای کە دەتەوێت بچیتە دەرەوە؟", + "syncSetting": "ڕێکخستنەکانی هاوکاتکردن", + "enableSync": "چالاک کردنی هاوکاتکردن", + "historicalUserList": "مێژووی چوونەژوورەوەی بەکارهێنەر", + "historicalUserListTooltip": "ئەم لیستە ئەکاونتە بێناوەکانت پیشان دەدات. دەتوانیت کلیک لەسەر ئەکاونتێک بکەیت بۆ بینینی وردەکارییەکانی. ئەکاونتی بێناو بە کلیک کردن لەسەر دوگمەی دەستپێکردن دروست دەکرێت", + "openHistoricalUser": "بۆ کردنەوەی ئەکاونتی بێناو کلیک بکە" + }, + "appearance": { + "resetSetting": "ڕێکخستن لە سفرەوە", + "fontFamily": { + "label": "فۆنتفامیلی", + "search": "گەڕان" + }, + "themeMode": { + "label": "مۆدی تێم", + "light": "مۆدی کاڵ/لاییت", + "dark": "مۆدی تاریک", + "system": "خۆگونجاندن لەگەڵ تێمی سیستەمدا" + }, + "themeUpload": { + "button": "بارکردن", + "description": "بە بەکارهێنانی دوگمەی خوارەوە تێمی AppFlowy ـەکەت باربکە.", + "loading": "تکایە چاوەڕوان بن تا ئێمە تێمی قاڵبەکەت پشتڕاست دەکەینەوە و بار دەکەین...", + "uploadSuccess": "تێمی قاڵبەکەت بە سەرکەوتوویی بارکرا", + "deletionFailure": "تێمەکە نەسڕدرایەوە. هەوڵبدە بە دەستی لابەریت.", + "filePickerDialogTitle": "پەڕگەیەکی .flowy_plugin هەڵبژێرە", + "urlUploadFailure": "ناتوانرێت URL بکرێتەوە: {}", + "failure": "تێمی قاڵبی بارکراو نادروستە." + }, + "theme": "تێم و دەرکەوتن", + "builtInsLabel": "قاڵبی پێش دروستکراو", + "pluginsLabel": "پێوەکراوەکان" + }, + "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": "گەڕاندنەوە بۆ دایرێکتۆری پێشووی داتاکان", + "exportFileSuccess": "هەناردەکردنی فایل بە سەرکەوتوویی!", + "exportFileFail": "هەناردەکردنی فایلەکە شکستی هێنا!", + "export": "هەناردەکردن" + }, + "user": { + "name": "ناو", + "selectAnIcon": "هەڵبژاردنی وێنۆچكه‌", + "pleaseInputYourOpenAIKey": "🔑️تکایە کلیلی OpenAI ـەکەت بنووسە", + "clickToLogout": "بۆ دەرچوون لە بەکارهێنەری ئێستا کلیک بکە" + }, + "shortcuts": { + "shortcutsLabel": "کورتە ڕێگاکان", + "command": "فەرمان", + "keyBinding": "کورتکراوەکانی تەختەکلیل", + "addNewCommand": "زیاد کردنی فەرمانێکی نوێ", + "updateShortcutStep": "تێکەڵەی کلیلی دڵخواز داگرە و ENTER داگرە", + "shortcutIsAlreadyUsed": "ئەم کورتە ڕێگایە پێشتر بۆ: {conflict} بەکارهاتووە.", + "resetToDefault": "گەڕاندنەوە بۆ کلیلەکانی بنه‌ڕه‌ت", + "couldNotLoadErrorMsg": "کورتە ڕێگاکان نەتوانرا باربکرێن، تکایە دووبارە هەوڵبدەرەوە", + "couldNotSaveErrorMsg": "کورتە ڕێگاکان نەتوانرا پاشەکەوت بکرێن، تکایە دووبارە هەوڵبدەرەوە" + } + }, + "grid": { + "deleteView": "ئایا دڵنیای کە دەتەوێت ئەم دیمەنە بسڕیتەوە؟", + "createView": "نوێ", + "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": "ناونیشانی ئینتەرنێتی", + "checklistFieldName": "لیستی پشکنین", + "numberFormat": "فۆرمات ژمارە", + "dateFormat": "فۆرمات ڕێکەوت", + "includeTime": "کات لەخۆ بگرێت", + "dateFormatFriendly": "Mang Roj, Sall", + "dateFormatISO": "Sall-Mang-Roj", + "dateFormatLocal": "Mang/Roj/Sall", + "dateFormatUS": "Sall/Mang/Roj", + "dateFormatDayMonthYear": "Roj/Mang/Sall", + "timeFormat": "فۆرماتی کات", + "invalidTimeFormat": "فۆرماتێکی نادروست", + "timeFormatTwelveHour": "دوانزە کاتژمێر", + "timeFormatTwentyFourHour": "بیست و چوار کاتژمێر", + "clearDate": "سڕینەوەی ڕێکەوت", + "addSelectOption": "زیادکردنی بژاردەیەک", + "optionTitle": "بژاردەکان", + "addOption": "زیادکردنی بژاردە", + "editProperty": "دەستکاریکردنی تایبەتمەندی", + "newProperty": "تایبەتمەندی نوێ", + "deleteFieldPromptMessage": "ئایا دڵنیایت لە سڕدنەوەی ئەم تایبەتمەندییە؟", + "newColumn": "ستوونی نوێ" + }, + "sort": { + "ascending": "هەڵکشاو", + "descending": "بەرەو خوار", + "deleteAllSorts": "هەموو ڕیزکردنەکان لاببە", + "addSort": "زیادکردنی ڕیزکردن" + }, + "row": { + "duplicate": "دووبارە کردنەوە", + "delete": "سڕینەوە", + "textPlaceholder": "بەتاڵ", + "copyProperty": "تایبەتمەندی کۆپی کرا بۆ کلیپبۆرد", + "count": "سەرژمێرکردن", + "newRow": "ڕیزی نوێ", + "action": "کردەوە" + }, + "selectOption": { + "create": "دروستکردن", + "purpleColor": "مۆر", + "pinkColor": "پەمەیی", + "lightPinkColor": "پەمەیی کاڵ", + "orangeColor": "پرتەقاڵی", + "yellowColor": "زەرد", + "limeColor": "لیمۆیی", + "greenColor": "سەوز", + "aquaColor": "ئاکوا", + "blueColor": "شین", + "deleteTag": "سڕینەوە تاگ", + "colorPanelTitle": "ڕەنگەکان", + "panelTitle": "بژاردەیەک زیاد یان دروستی بکە.", + "searchOption": "گەڕان بەدوای بژاردەیەکدا" + }, + "checklist": { + "addNew": "شتێک زیاد بکە" + }, + "menuName": "تۆڕ", + "referencedGridPrefix": "نواندن" + }, + "document": { + "menuName": "بەڵگەنامە", + "date": { + "timeHintTextInTwelveHour": "01:00 PM", + "timeHintTextInTwentyFourHour": "13:00" + }, + "slashMenu": { + "board": { + "selectABoardToLinkTo": "بۆردێک هەڵبژێرە بۆ ئەوەی لینکی بۆ بدەیت", + "createANewBoard": "بۆردێکی نوێ دروست بکە" + }, + "grid": { + "selectAGridToLinkTo": "Gridێک هەڵبژێرە بۆ ئەوەی لینکی بۆ بکەیت", + "createANewGrid": "دروستکردنی تۆڕێکی نوێی پیشاندانی" + }, + "calendar": { + "selectACalendarToLinkTo": "ساڵنامەیەک هەڵبژێرە بۆ ئەوەی لینکی بۆ بکەیت.", + "createANewCalendar": "ساڵنامەیەکی نوێ دروست بکە" + } + }, + "selectionMenu": { + "outline": "گەڵاڵە" + }, + "plugins": { + "referencedBoard": "بۆردی چاوگ", + "referencedGrid": "تۆڕی چاوگ", + "referencedCalendar": "ساڵنامەی چاوگ", + "autoGeneratorMenuItemName": "OpenAI نووسەری", + "autoGeneratorTitleName": "داوا لە AI بکە هەر شتێک بنووسێت...", + "autoGeneratorLearnMore": "زیاتر زانین", + "autoGeneratorGenerate": "بنووسە", + "autoGeneratorHintText": "لە OpenAI پرسیار بکە...", + "autoGeneratorCantGetOpenAIKey": "نەتوانرا کلیلی OpenAI بەدەست بهێنرێت", + "autoGeneratorRewrite": "دووبارە نووسینەوە", + "smartEdit": "یاریدەدەری زیرەک", + "openAI": "OpenAI ژیری دەستکرد", + "smartEditFixSpelling": "ڕاستکردنەوەی نووسین", + "warning": "⚠️ وەڵامەکانی AI دەتوانن هەڵە یان چەواشەکارانە بن", + "smartEditSummarize": "کورتەنووسی", + "smartEditImproveWriting": "پێشخستن نوووسین", + "smartEditMakeLonger": "درێژتری بکەرەوە", + "smartEditCouldNotFetchResult": "هیچ ئەنجامێک لە OpenAI وەرنەگیرا", + "smartEditCouldNotFetchKey": "نەتوانرا کلیلی OpenAI بهێنێتە ئاراوە", + "smartEditDisabled": "لە ڕێکخستنەکاندا پەیوەندی بە OpenAI بکە", + "discardResponse": "ئایا دەتەوێت وەڵامەکانی AI بسڕیتەوە؟", + "createInlineMathEquation": "درووست کردنی هاوکێشە", + "toggleList": "toggle لیست", + "cover": { + "changeCover": "گۆڕینی بەرگ", + "colors": "ڕەنگەکان", + "images": "وێنەکان", + "clearAll": "سڕینەوەی هەموو", + "abstract": "کورتە", + "addCover": "زیاد کردنی بەرگ", + "addLocalImage": "خستنه‌سه‌ری وێنەی خۆماڵی", + "invalidImageUrl": "ڕێڕەوی وێنەکە نادروستە", + "failedToAddImageToGallery": "نەمتوانی وێنەکەت بخەمە پێشانگا😔️", + "enterImageUrl": "ڕێڕەوی وێنەکە بنووسە", + "add": "زیادکردن", + "back": "چوونە دووا", + "saveToGallery": "پاشەکەوت لە پێشانگا", + "removeIcon": "سڕینەوەی وێنۆچكه‌", + "pasteImageUrl": "نووسینی ڕێڕەوی وێنە", + "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": "بۆ دروستکردنی خشتەی ناوەڕۆک سەردێڕەکان داخڵ بکە" + } + }, + "textBlock": { + "placeholder": "بۆ فەرمانەکان '/' بنووسە" + }, + "title": { + "placeholder": "بێ ناونیشان" + }, + "imageBlock": { + "placeholder": "بۆ زیادکردنی وێنە کلیک بکە", + "upload": { + "label": "بەرزکردنەوە", + "placeholder": "بۆ بارکردنی وێنە کلیک بکە" + }, + "url": { + "label": "بەستەری وێنە", + "placeholder": "بەستەری وێنەکە بنووسە" + }, + "support": "سنووری قەبارەی وێنە 5 مێگابایت. فۆرماتەکەنی پشتیوانی کراو: JPEG، PNG، GIF، SVG", + "error": { + "invalidImage": "وێنەیەکی نادروست", + "invalidImageSize": "قەبارەی وێنەکە دەبێت لە 5 مێگابایت کەمتر بێت", + "invalidImageFormat": "فۆرماتی وێنەکە پشتیوانی ناکرێ. فۆرماتەکەنی پشتیوانی کراو: JPEG، PNG، GIF، SVG", + "invalidImageUrl": "Urlی وێنەکە نادروستە" + } + }, + "codeBlock": { + "language": { + "label": "زمان", + "placeholder": "هەڵبژاردنی زمان" + } + }, + "inlineLink": { + "placeholder": "ڕێڕەوەکە بلکێنە یان بینووسە", + "openInNewTab": "کردنەوە لە تابێکی نوێدا", + "copyLink": "کۆپی کردنی بەستەر", + "removeLink": "لابردنی بەستەر", + "url": { + "label": "بەستەر", + "placeholder": "بەستەرەکە بنووسە" + }, + "title": { + "label": "سه‌ردێڕی بەستەرەکە", + "placeholder": "سه‌ردێڕی بەستەرەکە بنووسە" + } + }, + "mention": { + "placeholder": "باسی کەسێک یان لاپەڕەیەک یان بەروارێک یان سرووشتێک بکە...", + "page": { + "label": "لینکی پەیج", + "tooltip": "کلیک بکە بۆ کردنەوەی لاپەڕەکە" + } + } + }, + "board": { + "column": { + "createNewCard": "دروست کردنی نوێ" + }, + "menuName": "تەختە", + "referencedBoardPrefix": "دیمەنی..." + }, + "calendar": { + "menuName": "ساڵنامە", + "defaultNewCalendarTitle": "بێ ناونیشان", + "navigation": { + "today": "ئەمڕۆ", + "jumpToday": "باز بدە بۆ ئەمڕۆ", + "previousMonth": "مانگی پێشوو", + "nextMonth": "مانگی داهاتوو" + }, + "settings": { + "showWeekNumbers": "پیشاندانی ژمارەکانی هەفتە", + "showWeekends": "پیشاندانی کۆتایی هەفتە", + "firstDayOfWeek": "سەرەتای هەفتە لە", + "layoutDateField": "ڕیزبەستی ساڵنامە بە", + "noDateTitle": "بە بێ بەروارێک", + "clickToAdd": "بۆ زیادکردن بۆ ساڵنامە کرتە بکە", + "name": "شێواز و گه‌ڵاڵه‌به‌ندی ڕۆژژمێر", + "noDateHint": "ڕووداوە بێ بەرنامە داڕێژراوەکان لێرەدا نیشان دەدرێن" + }, + "referencedCalendarPrefix": "دیمەنی..." + }, + "errorDialog": { + "title": "هەڵەی⛔️ AppFlowy", + "howToFixFallback": "ببورن بۆ کێشەکە🥺️! پرسەکە و وەسفەکەی لە لاپەڕەی GitHub ـمان بنێرن.", + "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": "زۆرجار بەکارت هێناوە" + } + } +} From 3a4247c3049b26f2f658ffc7c02a4a80806494ef Mon Sep 17 00:00:00 2001 From: Ansah Mohammad Date: Mon, 19 Feb 2024 19:00:28 +0530 Subject: [PATCH 48/50] feat: convert time format from HHmm to HH:mm in time field (#4641) * feat: add a HHmm to HH:mm function * fix: Replaced string.parse() with string.parseStrict() to address time input bug * feat: add TextFormatter for formatting the time input --- .../mention/mention_date_block.dart | 4 +- .../date_picker/widgets/time_text_field.dart | 43 +++++++++++++++++++ .../lib/style_widget/text_field.dart | 3 ++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart index 001135f447d2f..b9c0a8c7d392a 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/mention/mention_date_block.dart @@ -264,10 +264,10 @@ class _MentionDateBlockState extends State { try { if (timeFormat == TimeFormatPB.TwelveHour) { - return twelveHourFormat.parse(timeStr); + return twelveHourFormat.parseStrict(timeStr); } - return twentyFourHourFormat.parse(timeStr); + return twentyFourHourFormat.parseStrict(timeStr); } on FormatException { Log.error("failed to parse time string ($timeStr)"); return DateTime.now(); diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart index d0c5d74b66ea2..46c2e08a31099 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/widgets/date_picker/widgets/time_text_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/date_entities.pbenum.dart'; import 'package:appflowy_popover/appflowy_popover.dart'; @@ -96,8 +97,50 @@ class _TimeTextFieldState extends State { ? _maxLengthTwelveHour : _maxLengthTwentyFourHour, showCounter: false, + inputFormatters: [TimeInputFormatter(widget.timeFormat)], onSubmitted: widget.onSubmitted, ), ); } } + +class TimeInputFormatter extends TextInputFormatter { + TimeInputFormatter(this.timeFormat); + + final TimeFormatPB timeFormat; + static const int colonPosition = 2; + static const int spacePosition = 5; + + @override + TextEditingValue formatEditUpdate( + TextEditingValue oldValue, + TextEditingValue newValue, + ) { + final oldText = oldValue.text; + final newText = newValue.text; + + // If the user has typed enough for a time separator(:) and hasn't already typed + if (newText.length == colonPosition + 1 && + oldText.length == colonPosition && + !newText.contains(":")) { + return _formatText(newText, colonPosition, ':'); + } + + // If the user has typed enough for an AM/PM separator and hasn't already typed + if (timeFormat == TimeFormatPB.TwelveHour && + newText.length == spacePosition + 1 && + oldText.length == spacePosition && + newText[newText.length - 1] != ' ') { + return _formatText(newText, spacePosition, ' '); + } + + return newValue; + } + + TextEditingValue _formatText(String text, int index, String separator) { + return TextEditingValue( + text: '${text.substring(0, index)}$separator${text.substring(index)}', + selection: TextSelection.collapsed(offset: text.length + 1), + ); + } +} 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 ffedd06bd14ca..994e42720203b 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 @@ -31,6 +31,7 @@ class FlowyTextField extends StatefulWidget { final InputDecoration? decoration; final TextAlignVertical? textAlignVertical; final TextInputAction? textInputAction; + final List? inputFormatters; const FlowyTextField({ super.key, @@ -60,6 +61,7 @@ class FlowyTextField extends StatefulWidget { this.decoration, this.textAlignVertical, this.textInputAction, + this.inputFormatters, }); @override @@ -153,6 +155,7 @@ class FlowyTextFieldState extends State { style: widget.textStyle ?? Theme.of(context).textTheme.bodySmall, textAlignVertical: widget.textAlignVertical ?? TextAlignVertical.center, keyboardType: TextInputType.multiline, + inputFormatters: widget.inputFormatters, decoration: widget.decoration ?? InputDecoration( constraints: widget.hintTextConstraints ?? From a3a5709b70b78013120f50d33fa062c0063e90d1 Mon Sep 17 00:00:00 2001 From: Mathias Mogensen <42929161+Xazin@users.noreply.github.com> Date: Mon, 19 Feb 2024 14:32:01 +0100 Subject: [PATCH 49/50] fix: dispose of previous applaunch tasks (#4631) --- frontend/appflowy_flutter/lib/startup/startup.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/appflowy_flutter/lib/startup/startup.dart b/frontend/appflowy_flutter/lib/startup/startup.dart index e7422ded8f24d..4b5c97346812e 100644 --- a/frontend/appflowy_flutter/lib/startup/startup.dart +++ b/frontend/appflowy_flutter/lib/startup/startup.dart @@ -76,6 +76,11 @@ class FlowyRunner { IntegrationTestHelper.rustEnvsBuilder = rustEnvsBuilder; } + // Clear and dispose tasks from previous AppLaunch + if (getIt.isRegistered(instance: AppLauncher)) { + await getIt().dispose(); + } + // Clear all the states in case of rebuilding. await getIt.reset(); From 42cb032bca6ebc4125e5b1356fd1805eabfbae3b Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 19 Feb 2024 20:48:06 +0700 Subject: [PATCH 50/50] fix: flutter analyze (#4680) --- .../settings/widgets/emoji_picker/emoji_shortcut_event.dart | 2 +- .../appflowy_flutter/packages/flowy_infra/lib/language.dart | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart index e55d0325f477c..078cf64963f47 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/emoji_picker/emoji_shortcut_event.dart @@ -1,6 +1,6 @@ +import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:flutter/material.dart'; -import 'package:appflowy/workspace/presentation/settings/widgets/emoji_picker/emoji_picker.dart'; final CommandShortcutEvent emojiShortcutEvent = CommandShortcutEvent( key: 'Ctrl + Alt + E to show emoji picker', diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart index fc3c61515002e..2d21a67dc8a84 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/language.dart @@ -26,6 +26,8 @@ String languageFromLocale(Locale locale) { switch (locale.countryCode) { case "KU": return "کوردی"; + default: + return locale.languageCode; } case "de": return "Deutsch";