chore: merge with main

This commit is contained in:
Zack Fu Zi Xiang 2024-03-07 16:23:19 +08:00
commit da509392e9
No known key found for this signature in database
122 changed files with 1776 additions and 1095 deletions

104
.github/actions/flutter_build/action.yml vendored Normal file
View File

@ -0,0 +1,104 @@
name: Flutter Integration Test
description: Run integration tests for AppFlowy
inputs:
os:
description: "The operating system to run the tests on"
required: true
flutter_version:
description: "The version of Flutter to use"
required: true
rust_toolchain:
description: "The version of Rust to use"
required: true
cargo_make_version:
description: "The version of cargo-make to use"
required: true
rust_target:
description: "The target to build for"
required: true
flutter_profile:
description: "The profile to build with"
required: true
runs:
using: "composite"
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ inputs.rust_toolchain }}
target: ${{ inputs.rust_target }}
override: true
profile: minimal
- name: Export pub environment variables and add to PATH
run: |
if [ "$RUNNER_OS" == "Windows" ]; then
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
fi
shell: bash
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ inputs.flutter_version }}
cache: true
- uses: Swatinem/rust-cache@v2
with:
prefix-key: ${{ inputs.os }}
workspaces: |
frontend/rust-lib
cache-all-crates: true
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ inputs.cargo_make_version }}, duckscript_cli
- name: Install prerequisites
working-directory: frontend
run: |
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 keybinder-3.0 libnotify-dev
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
run: cargo make --profile ${{ inputs.flutter_profile }} appflowy-core-dev
shell: bash
- name: Run code generation
working-directory: frontend
run: cargo make code_generation
shell: bash
- name: Flutter Analyzer
working-directory: frontend/appflowy_flutter
run: flutter analyze .
shell: bash
- name: Compress appflowy_flutter
run: tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
shell: bash
- uses: actions/upload-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
path: appflowy_flutter.tar.gz

View File

@ -0,0 +1,78 @@
name: Flutter Integration Test
description: Run integration tests for AppFlowy
inputs:
test_path:
description: "The path to the integration test file"
required: true
flutter_version:
description: "The version of Flutter to use"
required: true
rust_toolchain:
description: "The version of Rust to use"
required: true
cargo_make_version:
description: "The version of cargo-make to use"
required: true
rust_target:
description: "The target to build for"
required: true
runs:
using: "composite"
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ inputs.RUST_TOOLCHAIN }}
target: ${{ inputs.rust_target }}
override: true
profile: minimal
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ inputs.flutter_version }}
cache: true
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ inputs.cargo_make_version }}
- name: Install prerequisites
working-directory: frontend
run: |
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 keybinder-3.0 libnotify-dev network-manager
shell: bash
- name: Enable Flutter Desktop
run: |
flutter config --enable-linux-desktop
shell: bash
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-ubuntu-latest
- name: Uncompressed appflowy_flutter
run: tar -xf appflowy_flutter.tar.gz
shell: bash
- name: Run Flutter integration tests
working-directory: frontend/appflowy_flutter
run: |
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test ${{ inputs.test_path }} -d Linux --coverage
shell: bash

View File

@ -32,28 +32,21 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
prepare: prepare-linux:
if: github.event.pull_request.draft != true if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: false fail-fast: true
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
flutter_profile: development-linux-x86_64 flutter_profile: development-linux-x86_64
target: x86_64-unknown-linux-gnu target: x86_64-unknown-linux-gnu
- os: macos-latest
flutter_profile: development-mac-x86_64
target: x86_64-apple-darwin
- os: windows-latest
flutter_profile: development-windows-x86
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
# the following step is required to avoid running out of space # the following step is required to avoid running out of space
- name: Maximize build space - name: Maximize build space
if: matrix.os == 'ubuntu-latest'
run: | run: |
sudo rm -rf /usr/share/dotnet sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc sudo rm -rf /opt/ghc
@ -63,80 +56,70 @@ jobs:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust toolchain - name: Flutter build
id: rust_toolchain uses: ./.github/actions/flutter_build
uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ env.RUST_TOOLCHAIN }} os: ${{ matrix.os }}
target: ${{ matrix.target }} flutter_version: ${{ env.FLUTTER_VERSION }}
override: true rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
profile: minimal cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
flutter_profile: ${{ matrix.flutter_profile }}
- name: Export pub environment variables and add to PATH prepare-windows:
run: | if: github.event.pull_request.draft != true
if [ "$RUNNER_OS" == "Windows" ]; then strategy:
echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV fail-fast: true
fi matrix:
shell: bash os: [windows-latest]
include:
- os: windows-latest
flutter_profile: development-windows-x86
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
- name: Install flutter steps:
id: flutter - name: Checkout source code
uses: subosito/flutter-action@v2 uses: actions/checkout@v4
- name: Flutter build
uses: ./.github/actions/flutter_build
with: with:
channel: "stable" os: ${{ matrix.os }}
flutter-version: ${{ env.FLUTTER_VERSION }} flutter_version: ${{ env.FLUTTER_VERSION }}
cache: true rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
flutter_profile: ${{ matrix.flutter_profile }}
- uses: Swatinem/rust-cache@v2 prepare-macos:
if: github.event.pull_request.draft != true
strategy:
fail-fast: true
matrix:
os: [macos-latest]
include:
- os: macos-latest
flutter_profile: development-mac-x86_64
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter build
uses: ./.github/actions/flutter_build
with: with:
prefix-key: ${{ matrix.os }} os: ${{ matrix.os }}
workspaces: | flutter_version: ${{ env.FLUTTER_VERSION }}
frontend/rust-lib rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cache-all-crates: true cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
- uses: taiki-e/install-action@v2 flutter_profile: ${{ matrix.flutter_profile }}
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}, duckscript_cli
- name: Install prerequisites
working-directory: frontend
run: |
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 keybinder-3.0 libnotify-dev
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
run: cargo make --profile ${{ matrix.flutter_profile }} appflowy-core-dev
- name: Run code generation
working-directory: frontend
run: cargo make code_generation
- name: Flutter Analyzer
working-directory: frontend/appflowy_flutter
run: flutter analyze .
- name: Compress appflowy_flutter
run: |
tar -czf appflowy_flutter.tar.gz frontend/appflowy_flutter
- uses: actions/upload-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
path: appflowy_flutter.tar.gz
unit_test: unit_test:
needs: [prepare] needs: [prepare-linux]
if: github.event.pull_request.draft != true if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: false fail-fast: false
@ -227,7 +210,7 @@ jobs:
shell: bash shell: bash
cloud_integration_test: cloud_integration_test:
needs: [prepare] needs: [prepare-linux]
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -314,8 +297,9 @@ jobs:
flutter test integration_test/cloud/cloud_runner.dart -d Linux --coverage flutter test integration_test/cloud/cloud_runner.dart -d Linux --coverage
shell: bash shell: bash
integration_test: # split the integration tests into different machines to minimize the time
needs: [prepare] integration_test_1:
needs: [prepare-linux]
if: github.event.pull_request.draft != true if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: false fail-fast: false
@ -323,158 +307,65 @@ jobs:
os: [ubuntu-latest] os: [ubuntu-latest]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
flutter_profile: development-linux-x86_64 target: 'x86_64-unknown-linux-gnu'
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust toolchain - name: Flutter Integration Test 1
id: rust_toolchain uses: ./.github/actions/flutter_integration_test
uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ env.RUST_TOOLCHAIN }} test_path: integration_test/desktop_runner_1.dart
target: ${{ matrix.target }} flutter_version: ${{ env.FLUTTER_VERSION }}
override: true rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
profile: minimal cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
- name: Install flutter integration_test_2:
id: flutter needs: [prepare-linux]
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- uses: taiki-e/install-action@v2
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: Install prerequisites
working-directory: frontend
run: |
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 keybinder-3.0 libnotify-dev
fi
shell: bash
- name: Enable Flutter Desktop
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
flutter config --enable-linux-desktop
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter config --enable-macos-desktop
elif [ "$RUNNER_OS" == "Windows" ]; then
git config --system core.longpaths true
flutter config --enable-windows-desktop
fi
shell: bash
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
- name: Uncompressed appflowy_flutter
run: tar -xf appflowy_flutter.tar.gz
- name: Run flutter pub get
working-directory: frontend
run: cargo make pub_get
- name: Run Flutter integration tests
working-directory: frontend/appflowy_flutter
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
export DISPLAY=:99
sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 &
sudo apt-get install network-manager
flutter test integration_test/runner.dart -d Linux --coverage
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter test integration_test/runner.dart -d macOS --coverage
elif [ "$RUNNER_OS" == "Windows" ]; then
flutter test integration_test/runner.dart -d Windows --coverage
fi
shell: bash
build:
needs: [prepare]
if: github.event.pull_request.draft != true if: github.event.pull_request.draft != true
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest]
include: include:
- os: ubuntu-latest - os: ubuntu-latest
flutter_profile: development-linux-x86_64 target: 'x86_64-unknown-linux-gnu'
target: x86_64-unknown-linux-gnu
- os: macos-latest
flutter_profile: development-mac-x86_64
target: x86_64-apple-darwin
- os: windows-latest
flutter_profile: development-windows-x86
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Rust toolchain - name: Flutter Integration Test 2
id: rust_toolchain uses: ./.github/actions/flutter_integration_test
uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ env.RUST_TOOLCHAIN }} test_path: integration_test/desktop_runner_2.dart
target: ${{ matrix.target }} flutter_version: ${{ env.FLUTTER_VERSION }}
override: true rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
profile: minimal cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
rust_target: ${{ matrix.target }}
- name: Install flutter integration_test_3:
id: flutter needs: [prepare-linux]
uses: subosito/flutter-action@v2 if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
include:
- os: ubuntu-latest
target: 'x86_64-unknown-linux-gnu'
runs-on: ${{ matrix.os }}
steps:
- name: Checkout source code
uses: actions/checkout@v4
- name: Flutter Integration Test 3
uses: ./.github/actions/flutter_integration_test
with: with:
channel: "stable" test_path: integration_test/desktop_runner_3.dart
flutter-version: ${{ env.FLUTTER_VERSION }} flutter_version: ${{ env.FLUTTER_VERSION }}
cache: true rust_toolchain: ${{ env.RUST_TOOLCHAIN }}
cargo_make_version: ${{ env.CARGO_MAKE_VERSION }}
- uses: taiki-e/install-action@v2 rust_target: ${{ matrix.target }}
with:
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
- name: Install prerequisites
working-directory: frontend
run: |
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 keybinder-3.0 libnotify-dev
fi
shell: bash
- name: Enable Flutter Desktop
run: |
if [ "$RUNNER_OS" == "Linux" ]; then
flutter config --enable-linux-desktop
elif [ "$RUNNER_OS" == "macOS" ]; then
flutter config --enable-macos-desktop
elif [ "$RUNNER_OS" == "Windows" ]; then
git config --system core.longpaths true
flutter config --enable-windows-desktop
fi
shell: bash
- uses: actions/download-artifact@v4
with:
name: ${{ github.run_id }}-${{ matrix.os }}
- name: Uncompressed appflowy_flutter
run: tar -xf appflowy_flutter.tar.gz
- name: Build flutter product
working-directory: frontend
run: |
cargo make --profile ${{ matrix.flutter_profile }} appflowy-make-product-dev

View File

@ -25,6 +25,22 @@ jobs:
test-on-ubuntu: test-on-ubuntu:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
# - name: Maximize build space
# uses: easimon/maximize-build-space@master
# with:
# root-reserve-mb: 2048
# swap-size-mb: 1024
# remove-dotnet: 'true'
#
# # the following step is required to avoid running out of space
# - name: Maximize build space
# run: |
# sudo rm -rf /usr/share/dotnet
# sudo rm -rf /opt/ghc
# sudo rm -rf "/usr/local/share/boost"
# sudo rm -rf "$AGENT_TOOLSDIRECTORY"
# sudo docker image prune --all --force
- name: Checkout source code - name: Checkout source code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -86,3 +102,8 @@ jobs:
- name: clippy rust-lib - name: clippy rust-lib
run: cargo clippy --all-targets -- -D warnings run: cargo clippy --all-targets -- -D warnings
working-directory: frontend/rust-lib working-directory: frontend/rust-lib
- name: Clean up Docker images
run: |
docker image prune -af
docker volume prune -f

View File

@ -25,32 +25,33 @@ jobs:
platform: [ubuntu-latest] platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
env:
CI: true
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
if: matrix.platform == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node - name: setup node
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: ${{ env.NODE_VERSION }}
- name: Cache Rust Dependencies - name: setup pnpm
uses: Swatinem/rust-cache@v2 uses: pnpm/action-setup@v2
with: with:
key: rust-dependencies-${{ runner.os }} version: ${{ env.PNPM_VERSION }}
workspaces: |
frontend/rust-lib
frontend/appflowy_tauri/src-tauri
- name: Cache Node.js dependencies
uses: actions/cache@v2
with:
path: ~/.npm
key: npm-${{ runner.os }}
- name: Cache node_modules
uses: actions/cache@v2
with:
path: frontend/appflowy_tauri/node_modules
key: node-modules-${{ runner.os }}
- name: Install Rust toolchain - name: Install Rust toolchain
id: rust_toolchain id: rust_toolchain
@ -60,15 +61,23 @@ jobs:
override: true override: true
profile: minimal profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_tauri/node_modules
key: node-modules-${{ runner.os }}
- name: install dependencies (windows only) - name: install dependencies (windows only)
if: matrix.platform == 'windows-latest' if: matrix.platform == 'windows-latest'
working-directory: frontend working-directory: frontend
run: | run: |
cargo install --force cargo-make
cargo install --force duckscript_cli cargo install --force duckscript_cli
vcpkg integrate install vcpkg integrate install
cargo make appflowy-tauri-deps-tools
npm install -g pnpm@${{ env.PNPM_VERSION }}
- name: install dependencies (ubuntu only) - name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest' if: matrix.platform == 'ubuntu-latest'
@ -76,35 +85,29 @@ jobs:
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
npm install -g pnpm@${{ env.PNPM_VERSION }}
- name: install dependencies (macOS only) - name: install cargo-make
if: matrix.platform == 'macos-latest'
working-directory: frontend working-directory: frontend
run: | run: |
cargo install --force cargo-make cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools cargo make appflowy-tauri-deps-tools
npm install -g pnpm@${{ env.PNPM_VERSION }}
- name: Build - name: install frontend dependencies
working-directory: frontend/appflowy_tauri working-directory: frontend/appflowy_tauri
run: | run: |
mkdir dist mkdir dist
pnpm install pnpm install
cargo make --cwd .. tauri_build cargo make --cwd .. tauri_build
- name: frontend tests and linting
working-directory: frontend/appflowy_tauri
run: |
pnpm test pnpm test
pnpm test:errors pnpm test:errors
- name: Check for uncommitted changes
run: |
diff_files=$(git status --porcelain)
if [ -n "$diff_files" ]; then
echo "There are uncommitted changes in the working tree. Please commit them before pushing."
exit 1
fi
- uses: tauri-apps/tauri-action@v0 - uses: tauri-apps/tauri-action@v0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri

153
.github/workflows/tauri_release.yml vendored Normal file
View File

@ -0,0 +1,153 @@
name: Publish Tauri Release
on:
workflow_dispatch:
inputs:
branch:
description: 'The branch to release'
required: true
default: 'main'
version:
description: 'The version to release'
required: true
default: '0.0.0'
env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.75"
jobs:
publish-tauri:
permissions:
contents: write
strategy:
fail-fast: false
matrix:
settings:
- platform: windows-latest
args: "--verbose"
target: "windows-x86_64"
- platform: macos-latest
args: "--target x86_64-apple-darwin"
target: "macos-x86_64"
- platform: ubuntu-latest
args: "--target x86_64-unknown-linux-gnu"
target: "linux-x86_64"
runs-on: ${{ matrix.settings.platform }}
env:
CI: true
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
- name: Maximize build space (ubuntu only)
if: matrix.settings.platform == 'ubuntu-latest'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
sudo docker image prune --all --force
sudo rm -rf /opt/hostedtoolcache/codeQL
sudo rm -rf ${GITHUB_WORKSPACE}/.git
sudo rm -rf $ANDROID_HOME/ndk
- name: setup node
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: setup pnpm
uses: pnpm/action-setup@v2
with:
version: ${{ env.PNPM_VERSION }}
- name: Install Rust toolchain
id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
profile: minimal
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
- name: install dependencies (windows only)
if: matrix.settings.platform == 'windows-latest'
working-directory: frontend
run: |
cargo install --force duckscript_cli
vcpkg integrate install
- name: install dependencies (ubuntu only)
if: matrix.settings.platform == 'ubuntu-latest'
working-directory: frontend
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install cargo-make
working-directory: frontend
run: |
cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
- name: install frontend dependencies
working-directory: frontend/appflowy_tauri
run: |
mkdir dist
pnpm install
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
cargo make --cwd .. tauri_build
- uses: tauri-apps/tauri-action@dev
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
CI: true
with:
args: ${{ matrix.settings.args }}
appVersion: ${{ github.event.inputs.version }}
tauriScript: pnpm tauri
projectPath: frontend/appflowy_tauri
- name: Upload EXE package(windows only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'windows-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.exe
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
- name: Upload DMG package(macos only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'macos-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.dmg
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
- name: Upload Deb package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.deb
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
- name: Upload AppImage package(ubuntu only)
uses: actions/upload-artifact@v4
if: matrix.settings.platform == 'ubuntu-latest'
with:
name: ${{ env.PACKAGE_PREFIX }}.AppImage
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage

View File

@ -16,7 +16,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.tapCreateRowButtonInGrid(); await tester.tapCreateRowButtonInGrid();
// The initial number of rows is 3 // 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4); await tester.assertNumberOfRowsInGridPage(4);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
@ -31,9 +31,8 @@ void main() {
await tester.tapCreateRowButtonInRowMenuOfGrid(); await tester.tapCreateRowButtonInRowMenuOfGrid();
// The initial number of rows is 3 // 3 initial rows + 1 created
await tester.assertNumberOfRowsInGridPage(4); await tester.assertNumberOfRowsInGridPage(4);
await tester.assertRowCountInGridPage(4);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
@ -48,9 +47,8 @@ void main() {
await tester.tapRowMenuButtonInGrid(); await tester.tapRowMenuButtonInGrid();
await tester.tapDeleteOnRowMenu(); await tester.tapDeleteOnRowMenu();
// The initial number of rows is 3 // 3 initial rows - 1 deleted
await tester.assertNumberOfRowsInGridPage(2); await tester.assertNumberOfRowsInGridPage(2);
await tester.assertRowCountInGridPage(2);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });
@ -60,7 +58,6 @@ void main() {
await tester.tapGoButton(); await tester.tapGoButton();
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid); await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
await tester.assertRowCountInGridPage(3);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}); });

View File

@ -1,68 +0,0 @@
import 'desktop/uncategorized/appearance_settings_test.dart' as appearance_test_runner;
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/database/database_calendar_test.dart' as database_calendar_test;
import 'desktop/database/database_cell_test.dart' as database_cell_test;
import 'desktop/database/database_field_settings_test.dart'
as database_field_settings_test;
import 'desktop/database/database_field_test.dart' as database_field_test;
import 'desktop/database/database_filter_test.dart' as database_filter_test;
import 'desktop/database/database_row_page_test.dart' as database_row_page_test;
import 'desktop/database/database_row_test.dart' as database_row_test;
import 'desktop/database/database_setting_test.dart' as database_setting_test;
import 'desktop/database/database_share_test.dart' as database_share_test;
import 'desktop/database/database_sort_test.dart' as database_sort_test;
import 'desktop/database/database_view_test.dart' as database_view_test;
import 'desktop/document/document_test_runner.dart' as document_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
import 'desktop/uncategorized/import_files_test.dart' as import_files_test;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
Future<void> runIntegrationOnDesktop() async {
// This test must be run first, otherwise the CI will fail.
first_test.main();
switch_folder_test.main();
share_markdown_test.main();
import_files_test.main();
// Document integration tests
document_test_runner.startTesting();
// Sidebar integration tests
sidebar_test_runner.startTesting();
// Board integration test
board_test_runner.startTesting();
// Database integration tests
database_cell_test.main();
database_field_test.main();
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
// Tabs
tabs_test.main();
// Others
hotkeys_test.main();
emoji_shortcut_test.main();
// Appearance integration test
appearance_test_runner.main();
// User settings
settings_test_runner.main();
}

View File

@ -0,0 +1,21 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/document/document_test_runner.dart' as document_test_runner;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/switch_folder_test.dart' as switch_folder_test;
Future<void> main() async {
await runIntegration1OnDesktop();
}
Future<void> runIntegration1OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
switch_folder_test.main();
document_test_runner.startTesting();
// DON'T add more tests here. This is the first test runner for desktop.
}

View File

@ -0,0 +1,40 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/database/database_calendar_test.dart' as database_calendar_test;
import 'desktop/database/database_cell_test.dart' as database_cell_test;
import 'desktop/database/database_field_settings_test.dart'
as database_field_settings_test;
import 'desktop/database/database_field_test.dart' as database_field_test;
import 'desktop/database/database_filter_test.dart' as database_filter_test;
import 'desktop/database/database_row_page_test.dart' as database_row_page_test;
import 'desktop/database/database_row_test.dart' as database_row_test;
import 'desktop/database/database_setting_test.dart' as database_setting_test;
import 'desktop/database/database_share_test.dart' as database_share_test;
import 'desktop/database/database_sort_test.dart' as database_sort_test;
import 'desktop/database/database_view_test.dart' as database_view_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
Future<void> main() async {
await runIntegration2OnDesktop();
}
Future<void> runIntegration2OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
database_cell_test.main();
database_field_test.main();
database_field_settings_test.main();
database_share_test.main();
database_row_page_test.main();
database_row_test.main();
database_setting_test.main();
database_filter_test.main();
database_sort_test.main();
database_view_test.main();
database_calendar_test.main();
// DON'T add more tests here. This is the second test runner for desktop.
}

View File

@ -0,0 +1,36 @@
import 'package:integration_test/integration_test.dart';
import 'desktop/board/board_test_runner.dart' as board_test_runner;
import 'desktop/settings/settings_runner.dart' as settings_test_runner;
import 'desktop/sidebar/sidebar_test_runner.dart' as sidebar_test_runner;
import 'desktop/uncategorized/appearance_settings_test.dart'
as appearance_test_runner;
import 'desktop/uncategorized/emoji_shortcut_test.dart' as emoji_shortcut_test;
import 'desktop/uncategorized/empty_test.dart' as first_test;
import 'desktop/uncategorized/hotkeys_test.dart' as hotkeys_test;
import 'desktop/uncategorized/import_files_test.dart' as import_files_test;
import 'desktop/uncategorized/share_markdown_test.dart' as share_markdown_test;
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
Future<void> main() async {
await runIntegration3OnDesktop();
}
Future<void> runIntegration3OnDesktop() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// This test must be run first, otherwise the CI will fail.
first_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
hotkeys_test.main();
emoji_shortcut_test.main();
appearance_test_runner.main();
settings_test_runner.main();
share_markdown_test.main();
import_files_test.main();
sidebar_test_runner.startTesting();
board_test_runner.startTesting();
tabs_test.main();
}

View File

@ -1,5 +1,8 @@
import 'package:integration_test/integration_test.dart';
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test; import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
Future<void> runIntegrationOnMobile() async { Future<void> runIntegrationOnMobile() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
anonymous_sign_in_test.main(); anonymous_sign_in_test.main();
} }

View File

@ -1,8 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:integration_test/integration_test.dart'; import 'desktop_runner_1.dart';
import 'desktop_runner_2.dart';
import 'desktop_runner.dart'; import 'desktop_runner_3.dart';
import 'mobile_runner.dart'; import 'mobile_runner.dart';
/// The main task runner for all integration tests in AppFlowy. /// The main task runner for all integration tests in AppFlowy.
@ -13,9 +13,10 @@ import 'mobile_runner.dart';
/// Once removed, the integration_test.yaml must be updated to exclude this as /// Once removed, the integration_test.yaml must be updated to exclude this as
/// as the test target. /// as the test target.
Future<void> main() async { Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) { if (Platform.isLinux || Platform.isMacOS || Platform.isWindows) {
await runIntegrationOnDesktop(); await runIntegration1OnDesktop();
await runIntegration2OnDesktop();
await runIntegration3OnDesktop();
} else if (Platform.isIOS || Platform.isAndroid) { } else if (Platform.isIOS || Platform.isAndroid) {
await runIntegrationOnMobile(); await runIntegrationOnMobile();
} else { } else {

View File

@ -1,26 +1,12 @@
import 'dart:io'; 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';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart'; import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart'; import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart'; import 'package:appflowy/plugins/database/board/presentation/widgets/board_column_header.dart';
import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart'; import 'package:appflowy/plugins/database/calendar/application/calendar_bloc.dart';
@ -30,6 +16,9 @@ import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_e
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart'; import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart'; import 'package:appflowy/plugins/database/calendar/presentation/toolbar/calendar_layout_setting.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart'; import 'package:appflowy/plugins/database/grid/presentation/grid_page.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/filter/choicechip/checkbox.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checkbox.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/checklist/checklist.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart';
@ -43,6 +32,7 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/header/deskt
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
@ -52,14 +42,22 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filt
import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/sort_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart'; import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart'; import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/timestamp.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/url.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart'; import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
import 'package:appflowy/plugins/database/widgets/row/row_action.dart'; import 'package:appflowy/plugins/database/widgets/row/row_action.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart'; import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart'; import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
@ -70,6 +68,7 @@ import 'package:appflowy/plugins/database/widgets/setting/database_setting_actio
import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/database_settings_list.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart'; import 'package:appflowy/plugins/database/widgets/setting/setting_property_list.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart'; import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/end_time_button.dart';
@ -77,6 +76,7 @@ import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/remi
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart'; import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart'; import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart'; import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart'; import 'package:appflowy_board/appflowy_board.dart';
import 'package:calendar_view/calendar_view.dart'; import 'package:calendar_view/calendar_view.dart';
@ -86,10 +86,9 @@ import 'package:flowy_infra_ui/style_widget/text_input.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart'; import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:table_calendar/table_calendar.dart';
// Non-exported member of the table_calendar library // Non-exported member of the table_calendar library
import 'package:table_calendar/src/widgets/cell_content.dart'; import 'package:table_calendar/src/widgets/cell_content.dart';
import 'package:table_calendar/table_calendar.dart';
import 'base.dart'; import 'base.dart';
import 'common_operations.dart'; import 'common_operations.dart';
@ -974,11 +973,6 @@ extension AppFlowyDatabaseTest on WidgetTester {
await tapButtonWithName(LocaleKeys.grid_row_delete.tr()); await tapButtonWithName(LocaleKeys.grid_row_delete.tr());
} }
Future<void> assertRowCountInGridPage(int num) async {
final text = find.text('${rowCountString()} $num', findRichText: true);
expect(text, findsOneWidget);
}
Future<void> createField(FieldType fieldType, String name) async { Future<void> createField(FieldType fieldType, String name) async {
await scrollToRight(find.byType(GridPage)); await scrollToRight(find.byType(GridPage));
await tapNewPropertyButton(); await tapNewPropertyButton();

View File

@ -1,6 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart'; import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart'; import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
@ -14,8 +17,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embe
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart'; import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log; import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart'; import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -132,8 +133,7 @@ class EditorOperations {
of: find.byType(EmbedImageUrlWidget), of: find.byType(EmbedImageUrlWidget),
matching: find.byType(TextField), matching: find.byType(TextField),
); );
final textField = tester.widget<TextField>(imageUrlTextField); await tester.enterText(imageUrlTextField, imageUrl);
textField.controller?.text = imageUrl;
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.tapButton( await tester.tapButton(
find.descendant( find.descendant(

View File

@ -58,4 +58,10 @@ class KVKeys {
/// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause /// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause
/// the text to be too large and not aligned with the icon /// the text to be too large and not aligned with the icon
static const String textScaleFactor = 'textScaleFactor'; static const String textScaleFactor = 'textScaleFactor';
/// The key for saving the feature flags
///
/// The value is a json string with the following format:
/// {'feature_flag_1': true, 'feature_flag_2': false}
static const String featureFlag = 'featureFlag';
} }

View File

@ -281,7 +281,7 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) { if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) {
return AppFlowyCloudConfiguration( return AppFlowyCloudConfiguration(
base_url: "$baseUrl:8000", base_url: "$baseUrl:8000",
ws_base_url: "ws://${baseUri.host}:8000/ws", ws_base_url: "ws://${baseUri.host}:8000/ws/v1",
gotrue_url: "$baseUrl:9999", gotrue_url: "$baseUrl:9999",
); );
} else { } else {
@ -319,7 +319,7 @@ Future<String> _getAppFlowyCloudWSUrl(String baseURL) async {
// Construct the WebSocket URL directly from the parsed URI. // Construct the WebSocket URL directly from the parsed URI.
final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws'; final wsScheme = uri.isScheme('HTTPS') ? 'wss' : 'ws';
final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws'); final wsUrl = Uri(scheme: wsScheme, host: uri.host, path: '/ws/v1');
return wsUrl.toString(); return wsUrl.toString();
} catch (e) { } catch (e) {

View File

@ -42,12 +42,13 @@ class OptionTextField extends StatelessWidget {
width: 38, width: 38,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
color: type.mobileIconBackgroundColor, color: Theme.of(context).brightness == Brightness.light
? type.mobileIconBackgroundColor
: type.mobileIconBackgroundColorDark,
), ),
child: Center( child: Center(
child: FlowySvg( child: FlowySvg(
type.svgData, type.svgData,
blendMode: null,
size: const Size.square(22), size: const Size.square(22),
), ),
), ),

View File

@ -1,10 +1,12 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar.dart'; import 'package:appflowy/mobile/presentation/base/app_bar.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart'; import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_search_text_field.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart'; import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart'; import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
@ -23,9 +25,7 @@ class FontPickerScreen extends StatelessWidget {
} }
class LanguagePickerPage extends StatefulWidget { class LanguagePickerPage extends StatefulWidget {
const LanguagePickerPage({ const LanguagePickerPage({super.key});
super.key,
});
@override @override
State<LanguagePickerPage> createState() => _LanguagePickerPageState(); State<LanguagePickerPage> createState() => _LanguagePickerPageState();
@ -52,6 +52,7 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
body: SafeArea( body: SafeArea(
child: Scrollbar( child: Scrollbar(
child: ListView.builder( child: ListView.builder(
itemCount: availableFonts.length + 1, // with search bar
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
// search bar // search bar
@ -65,7 +66,8 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
setState(() { setState(() {
availableFonts = _availableFonts availableFonts = _availableFonts
.where( .where(
(element) => parseFontFamilyName(element) (font) => font
.parseFontFamilyName()
.toLowerCase() .toLowerCase()
.contains(keyword.toLowerCase()), .contains(keyword.toLowerCase()),
) )
@ -75,8 +77,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
), ),
); );
} }
final fontFamilyName = availableFonts[index - 1]; final fontFamilyName = availableFonts[index - 1];
final displayName = parseFontFamilyName(fontFamilyName); final displayName = fontFamilyName.parseFontFamilyName();
return FlowyOptionTile.checkbox( return FlowyOptionTile.checkbox(
text: displayName, text: displayName,
isSelected: selectedFontFamilyName == fontFamilyName, isSelected: selectedFontFamilyName == fontFamilyName,
@ -86,17 +89,9 @@ class _LanguagePickerPageState extends State<LanguagePickerPage> {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
); );
}, },
itemCount: availableFonts.length + 1, // with search bar
), ),
), ),
), ),
); );
} }
String parseFontFamilyName(String fontFamilyName) {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return fontFamilyName
.replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
} }

View File

@ -11,8 +11,22 @@ extension CalcTypeLabel on CalculationType {
LocaleKeys.grid_calculationTypeLabel_median.tr(), LocaleKeys.grid_calculationTypeLabel_median.tr(),
CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(), CalculationType.Min => LocaleKeys.grid_calculationTypeLabel_min.tr(),
CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(), CalculationType.Sum => LocaleKeys.grid_calculationTypeLabel_sum.tr(),
CalculationType.Count =>
LocaleKeys.grid_calculationTypeLabel_count.tr(),
CalculationType.CountEmpty =>
LocaleKeys.grid_calculationTypeLabel_countEmpty.tr(),
CalculationType.CountNonEmpty =>
LocaleKeys.grid_calculationTypeLabel_countNonEmpty.tr(),
_ => throw UnimplementedError( _ => throw UnimplementedError(
'Label for $this has not been implemented', 'Label for $this has not been implemented',
), ),
}; };
String get shortLabel => switch (this) {
CalculationType.CountEmpty =>
LocaleKeys.grid_calculationTypeLabel_countEmptyShort.tr(),
CalculationType.CountNonEmpty =>
LocaleKeys.grid_calculationTypeLabel_countNonEmptyShort.tr(),
_ => label,
};
} }

View File

@ -128,7 +128,7 @@ extension NumberFormatExtension on NumberFormatPB {
} }
} }
String iconSymbol() { String iconSymbol([bool defaultPrefixInc = true]) {
switch (this) { switch (this) {
case NumberFormatPB.ArgentinePeso: case NumberFormatPB.ArgentinePeso:
return "\$"; return "\$";
@ -169,7 +169,7 @@ extension NumberFormatExtension on NumberFormatPB {
case NumberFormatPB.NorwegianKrone: case NumberFormatPB.NorwegianKrone:
return "kr"; return "kr";
case NumberFormatPB.Num: case NumberFormatPB.Num:
return "#"; return defaultPrefixInc ? "#" : "";
case NumberFormatPB.Percent: case NumberFormatPB.Percent:
return "%"; return "%";
case NumberFormatPB.PhilippinePeso: case NumberFormatPB.PhilippinePeso:

View File

@ -0,0 +1,34 @@
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
extension AvailableCalculations on FieldType {
List<CalculationType> calculationsForFieldType() {
final calculationTypes = [
CalculationType.Count,
];
// These FieldTypes cannot be empty, no need to count empty/non-empty
if (![FieldType.Checkbox, FieldType.LastEditedTime, FieldType.CreatedTime]
.contains(this)) {
calculationTypes.addAll([
CalculationType.CountEmpty,
CalculationType.CountNonEmpty,
]);
}
switch (this) {
case FieldType.Number:
calculationTypes.addAll([
CalculationType.Sum,
CalculationType.Average,
CalculationType.Min,
CalculationType.Max,
CalculationType.Median,
]);
break;
default:
break;
}
return calculationTypes;
}
}

View File

@ -12,7 +12,6 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.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/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart'; import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
import 'package:flowy_infra_ui/widget/error_page.dart'; import 'package:flowy_infra_ui/widget/error_page.dart';
@ -237,7 +236,6 @@ class _GridPageContentState extends State<GridPageContent> {
viewId: widget.view.id, viewId: widget.view.id,
scrollController: _scrollController, scrollController: _scrollController,
), ),
const _GridFooter(),
], ],
); );
} }
@ -431,40 +429,3 @@ class _WrapScrollView extends StatelessWidget {
); );
} }
} }
class _GridFooter extends StatelessWidget {
const _GridFooter();
@override
Widget build(BuildContext context) {
return BlocSelector<GridBloc, GridState, int>(
selector: (state) => state.rowCount,
builder: (context, rowCount) {
return Padding(
padding: GridSize.contentInsets,
child: RichText(
text: TextSpan(
text: rowCountString(),
style: Theme.of(context)
.textTheme
.bodyMedium!
.copyWith(color: Theme.of(context).hintColor),
children: [
TextSpan(
text: ' $rowCount',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: AFThemeExtension.of(context).gridRowCountColor,
),
),
],
),
),
);
},
);
}
}
String rowCountString() {
return '${LocaleKeys.grid_row_count.tr()} :';
}

View File

@ -5,9 +5,11 @@ import 'package:appflowy/plugins/database/application/calculations/calculation_t
import 'package:appflowy/plugins/database/application/field/field_info.dart'; import 'package:appflowy/plugins/database/application/field/field_info.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart'; import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart'; import 'package:appflowy/plugins/database/grid/application/calculations/calculations_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/calculations/field_type_calc_ext.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculation_selector.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/calculation_type_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart'; import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/remove_calculation_button.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/calculation_entities.pb.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_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-database2/number_entities.pb.dart';
@ -67,31 +69,29 @@ class _CalculateCellState extends State<CalculateCell> {
), ),
), ),
), ),
...CalculationType.values.map( ...widget.fieldInfo.fieldType.calculationsForFieldType().map(
(type) => CalculationTypeItem( (type) => CalculationTypeItem(
type: type, type: type,
onTap: () { onTap: () {
if (type != widget.calculation?.calculationType) { if (type != widget.calculation?.calculationType) {
context.read<CalculationsBloc>().add( context.read<CalculationsBloc>().add(
CalculationsEvent.updateCalculationType( CalculationsEvent.updateCalculationType(
widget.fieldInfo.id, widget.fieldInfo.id,
type, type,
calculationId: widget.calculation?.id, calculationId: widget.calculation?.id,
), ),
); );
} }
}, },
), ),
), ),
], ],
), ),
); );
}, },
child: widget.fieldInfo.fieldType == FieldType.Number child: widget.calculation != null
? widget.calculation != null ? _showCalculateValue(context, prefix)
? _showCalculateValue(context, prefix) : CalculationSelector(isSelected: isSelected),
: CalculationSelector(isSelected: isSelected)
: const SizedBox.shrink(),
), ),
); );
} }
@ -107,7 +107,7 @@ class _CalculateCellState extends State<CalculateCell> {
children: [ children: [
Flexible( Flexible(
child: FlowyText( child: FlowyText(
widget.calculation!.calculationType.label, widget.calculation!.calculationType.shortLabel,
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@ -133,9 +133,8 @@ class _CalculateCellState extends State<CalculateCell> {
} }
String _withoutTrailingZeros(String value) { String _withoutTrailingZeros(String value) {
final regex = RegExp(r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$'); if (trailingZerosRegex.hasMatch(value)) {
if (regex.hasMatch(value)) { final match = trailingZerosRegex.firstMatch(value)!;
final match = regex.firstMatch(value)!;
return match.group(1)!; return match.group(1)!;
} }
@ -146,7 +145,7 @@ class _CalculateCellState extends State<CalculateCell> {
FieldType.Number => FieldType.Number =>
NumberTypeOptionPB.fromBuffer(widget.fieldInfo.field.typeOptionData) NumberTypeOptionPB.fromBuffer(widget.fieldInfo.field.typeOptionData)
.format .format
.iconSymbol(), .iconSymbol(false),
_ => null, _ => null,
}; };
} }

View File

@ -20,6 +20,7 @@ const List<FieldType> _supportedFieldTypes = [
FieldType.URL, FieldType.URL,
FieldType.LastEditedTime, FieldType.LastEditedTime,
FieldType.CreatedTime, FieldType.CreatedTime,
FieldType.Relation,
]; ];
class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate { class FieldTypeList extends StatelessWidget with FlowyOverlayDelegate {

View File

@ -84,12 +84,11 @@ class _MobileSelectOptionEditorState extends State<MobileSelectOptionEditor> {
const height = 44.0; const height = 44.0;
return Stack( return Stack(
children: [ children: [
Align( if (showMoreOptions)
alignment: Alignment.centerLeft, Align(
child: showMoreOptions alignment: Alignment.centerLeft,
? AppBarBackButton(onTap: _popOrBack) child: AppBarBackButton(onTap: _popOrBack),
: AppBarCloseButton(onTap: _popOrBack), ),
),
SizedBox( SizedBox(
height: 44.0, height: 44.0,
child: Align( child: Align(

View File

@ -1,10 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
RegExp _hrefRegex = RegExp(
r'https?://(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(?:/[^\s]*)?',
);
extension PasteFromPlainText on EditorState { extension PasteFromPlainText on EditorState {
Future<void> pastePlainText(String plainText) async { Future<void> pastePlainText(String plainText) async {
if (await pasteHtmlIfAvailable(plainText)) { if (await pasteHtmlIfAvailable(plainText)) {
@ -23,7 +20,7 @@ extension PasteFromPlainText on EditorState {
.map((e) { .map((e) {
// parse the url content // parse the url content
final Attributes attributes = {}; final Attributes attributes = {};
if (_hrefRegex.hasMatch(e)) { if (hrefRegex.hasMatch(e)) {
attributes[AppFlowyRichTextKeys.href] = e; attributes[AppFlowyRichTextKeys.href] = e;
} }
return Delta()..insert(e, attributes: attributes); return Delta()..insert(e, attributes: attributes);
@ -45,7 +42,7 @@ extension PasteFromPlainText on EditorState {
if (selection == null || if (selection == null ||
!selection.isSingle || !selection.isSingle ||
selection.isCollapsed || selection.isCollapsed ||
!_hrefRegex.hasMatch(plainText)) { !hrefRegex.hasMatch(plainText)) {
return false; return false;
} }

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmbedImageUrlWidget extends StatefulWidget { class EmbedImageUrlWidget extends StatefulWidget {
const EmbedImageUrlWidget({ const EmbedImageUrlWidget({
@ -16,6 +18,7 @@ class EmbedImageUrlWidget extends StatefulWidget {
} }
class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> { class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
bool isUrlValid = true;
String inputText = ''; String inputText = '';
@override @override
@ -25,8 +28,15 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
FlowyTextField( FlowyTextField(
hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(), hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),
onChanged: (value) => inputText = value, onChanged: (value) => inputText = value,
onEditingComplete: () => widget.onSubmit(inputText), onEditingComplete: submit,
), ),
if (!isUrlValid) ...[
const VSpace(8),
FlowyText(
LocaleKeys.document_plugins_cover_invalidImageUrl.tr(),
color: Theme.of(context).colorScheme.error,
),
],
const VSpace(8), const VSpace(8),
SizedBox( SizedBox(
width: 160, width: 160,
@ -37,10 +47,20 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
LocaleKeys.document_imageBlock_embedLink_label.tr(), LocaleKeys.document_imageBlock_embedLink_label.tr(),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
onTap: () => widget.onSubmit(inputText), onTap: submit,
), ),
), ),
], ],
); );
} }
void submit() {
if (checkUrlValidity(inputText)) {
return widget.onSubmit(inputText);
}
setState(() => isUrlValid = false);
}
bool checkUrlValidity(String url) => imgUrlRegex.hasMatch(url);
} }

View File

@ -1,12 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart';
import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart'; import 'package:appflowy/mobile/presentation/setting/font/font_picker_screen.dart';
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart'; import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/_toolbar_theme.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart'; import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/util/google_font_family_extension.dart'; import 'package:appflowy/util/google_font_family_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart'; import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';

View File

@ -1,3 +1,4 @@
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -59,3 +60,15 @@ class TemporaryDirectoryCache implements ICache {
await tmpDir.delete(recursive: true); await tmpDir.delete(recursive: true);
} }
} }
class FeatureFlagCache implements ICache {
@override
Future<int> cacheSize() async {
return 0;
}
@override
Future<void> clearAll() async {
await FeatureFlag.clear();
}
}

View File

@ -1,10 +1,17 @@
import 'dart:collection';
import 'dart:convert';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
/// The [FeatureFlag] is used to control the front-end features of the app. /// The [FeatureFlag] is used to control the front-end features of the app.
/// ///
/// For example, if your feature is still under development, /// For example, if your feature is still under development,
/// you can set the value to `false` to hide the feature. /// you can set the value to `false` to hide the feature.
enum FeatureFlag { enum FeatureFlag {
// Feature flags
// used to control the visibility of the collaborative workspace feature // used to control the visibility of the collaborative workspace feature
// if it's on, you can see the workspace list and the workspace settings // if it's on, you can see the workspace list and the workspace settings
// in the top-left corner of the app // in the top-left corner of the app
@ -14,7 +21,56 @@ enum FeatureFlag {
// if it's on, you can see the members settings in the settings page // if it's on, you can see the members settings in the settings page
membersSettings; membersSettings;
static Future<void> initialize() async {
final values = await getIt<KeyValueStorage>().getWithFormat<FeatureFlagMap>(
KVKeys.featureFlag,
(value) => Map.from(jsonDecode(value)).map(
(key, value) => MapEntry(
FeatureFlag.values.firstWhere((e) => e.name == key),
value as bool,
),
),
) ??
{};
_values = {
...{for (final flag in FeatureFlag.values) flag: false},
...values,
};
}
static UnmodifiableMapView<FeatureFlag, bool> get data =>
UnmodifiableMapView(_values);
Future<void> turnOn() async {
await update(true);
}
Future<void> turnOff() async {
await update(false);
}
Future<void> update(bool value) async {
_values[this] = value;
await getIt<KeyValueStorage>().set(
KVKeys.featureFlag,
jsonEncode(
_values.map((key, value) => MapEntry(key.name, value)),
),
);
}
static Future<void> clear() async {
_values = {};
await getIt<KeyValueStorage>().remove(KVKeys.featureFlag);
}
bool get isOn { bool get isOn {
if (_values.containsKey(this)) {
return _values[this]!;
}
switch (this) { switch (this) {
case FeatureFlag.collaborativeWorkspace: case FeatureFlag.collaborativeWorkspace:
return false; return false;
@ -22,4 +78,17 @@ enum FeatureFlag {
return false; return false;
} }
} }
String get description {
switch (this) {
case FeatureFlag.collaborativeWorkspace:
return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app';
case FeatureFlag.membersSettings:
return 'if it\'s on, you can see the members settings in the settings page';
}
}
String get key => 'appflowy_feature_flag_${toString()}';
} }
FeatureFlagMap _values = {};

View File

@ -0,0 +1,23 @@
const _trailingZerosPattern = r'^(\d+(?:\.\d*?[1-9](?=0|\b))?)\.?0*$';
final trailingZerosRegex = RegExp(_trailingZerosPattern);
const _hrefPattern =
r'https?://(?:www\.)?[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,}(?:/[^\s]*)?';
final hrefRegex = RegExp(_hrefPattern);
/// This pattern allows for both HTTP and HTTPS Scheme
/// It allows for query parameters
/// It only allows the following image extensions: .png, .jpg, .gif, .webm
///
const _imgUrlPattern =
r'(https?:\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.webm)(\?[^\s[",><]*)?';
final imgUrlRegex = RegExp(_imgUrlPattern);
const _appflowyCloudUrlPattern = r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)';
final appflowyCloudUrlRegex = RegExp(_appflowyCloudUrlPattern);
const _camelCasePattern = '(?<=[a-z])[A-Z]';
final camelCaseRegex = RegExp(_camelCasePattern);
const _macOSVolumesPattern = '^/Volumes/[^/]+';
final macOSVolumesRegex = RegExp(_macOSVolumesPattern);

View File

@ -0,0 +1,19 @@
/// RegExp to match Twelve Hour formats
/// Source: https://stackoverflow.com/a/33906224
///
/// Matches eg: "05:05 PM", "5:50 Pm", "10:59 am", etc.
///
const _twelveHourTimePattern =
r'\b((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))';
final twelveHourTimeRegex = RegExp(_twelveHourTimePattern);
bool isTwelveHourTime(String? time) => twelveHourTimeRegex.hasMatch(time ?? '');
/// RegExp to match Twenty Four Hour formats
/// Source: https://stackoverflow.com/a/7536768
///
/// Matches eg: "0:01", "04:59", "16:30", etc.
///
const _twentyFourHourtimePattern = r'^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$';
final tewentyFourHourTimeRegex = RegExp(_twentyFourHourtimePattern);
bool isTwentyFourHourTime(String? time) =>
tewentyFourHourTimeRegex.hasMatch(time ?? '');

View File

@ -134,7 +134,8 @@ void _resolveCommonService(
getIt.registerFactory<FlowyCacheManager>( getIt.registerFactory<FlowyCacheManager>(
() => FlowyCacheManager() () => FlowyCacheManager()
..registerCache(TemporaryDirectoryCache()) ..registerCache(TemporaryDirectoryCache())
..registerCache(CustomImageCacheManager()), ..registerCache(CustomImageCacheManager())
..registerCache(FeatureFlagCache()),
); );
} }

View File

@ -2,6 +2,7 @@ import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:appflowy/env/cloud_env.dart'; import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/startup/tasks/feature_flag_task.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart'; import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy_backend/appflowy_backend.dart'; import 'package:appflowy_backend/appflowy_backend.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -113,6 +114,7 @@ class FlowyRunner {
// there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored. // there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored.
MemoryLeakDetectorTask(), MemoryLeakDetectorTask(),
const DebugTask(), const DebugTask(),
const FeatureFlagTask(),
// localization // localization
const InitLocalizationTask(), const InitLocalizationTask(),

View File

@ -0,0 +1,21 @@
import 'package:appflowy/shared/feature_flags.dart';
import 'package:flutter/foundation.dart';
import '../startup.dart';
class FeatureFlagTask extends LaunchTask {
const FeatureFlagTask();
@override
Future<void> initialize(LaunchContext context) async {
// the hotkey manager is not supported on mobile
if (!kDebugMode) {
return;
}
await FeatureFlag.initialize();
}
@override
Future<void> dispose() async {}
}

View File

@ -1,7 +1,6 @@
import 'package:appflowy/shared/patterns/common_patterns.dart';
extension GoogleFontsParser on String { extension GoogleFontsParser on String {
String parseFontFamilyName() { String parseFontFamilyName() => replaceAll('_regular', '')
final camelCase = RegExp('(?<=[a-z])[A-Z]'); .replaceAllMapped(camelCaseRegex, (m) => ' ${m.group(0)}');
return replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
} }

View File

@ -2,6 +2,8 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
extension StringExtension on String { extension StringExtension on String {
static const _specialCharacters = r'\/:*?"<>| '; static const _specialCharacters = r'\/:*?"<>| ';
@ -31,8 +33,6 @@ extension StringExtension on String {
return null; return null;
} }
/// Returns if the string is a appflowy cloud url. /// Returns true if the string is a appflowy cloud url.
bool get isAppFlowyCloudUrl { bool get isAppFlowyCloudUrl => appflowyCloudUrlRegex.hasMatch(this);
return RegExp(r'^(https:\/\/)(.*)(\.appflowy\.cloud\/)(.*)').hasMatch(this);
}
} }

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:appflowy/core/config/kv.dart'; import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart'; import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/shared/patterns/common_patterns.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/log.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import '../../../startup/tasks/prelude.dart'; import '../../../startup/tasks/prelude.dart';
@ -26,7 +28,7 @@ class ApplicationDataStorage {
if (Platform.isMacOS) { if (Platform.isMacOS) {
// remove the prefix `/Volumes/*` // remove the prefix `/Volumes/*`
path = path.replaceFirst(RegExp('^/Volumes/[^/]+'), ''); path = path.replaceFirst(macOSVolumesRegex, '');
} else if (Platform.isWindows) { } else if (Platform.isWindows) {
path = path.replaceAll('/', '\\'); path = path.replaceAll('/', '\\');
} }

View File

@ -1,18 +0,0 @@
/// RegExp to match Twelve Hour formats
/// Source: https://stackoverflow.com/a/33906224
///
/// Matches eg: "05:05 PM", "5:50 Pm", "10:59 am", etc.
///
final _twelveHourTimePattern =
RegExp(r'\b((1[0-2]|0?[1-9]):([0-5][0-9]) ([AaPp][Mm]))');
bool isTwelveHourTime(String? time) =>
_twelveHourTimePattern.hasMatch(time ?? '');
/// RegExp to match Twenty Four Hour formats
/// Source: https://stackoverflow.com/a/7536768
///
/// Matches eg: "0:01", "04:59", "16:30", etc.
///
final _twentyFourHourtimePattern = RegExp(r'^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$');
bool isTwentyFourHourTime(String? time) =>
_twentyFourHourtimePattern.hasMatch(time ?? '');

View File

@ -17,6 +17,7 @@ enum SettingsPage {
cloud, cloud,
shortcuts, shortcuts,
member, member,
featureFlags,
} }
class SettingsDialogBloc class SettingsDialogBloc

View File

@ -113,8 +113,7 @@ class WorkspaceMenuItem extends StatelessWidget {
return BlocProvider( return BlocProvider(
create: (_) => create: (_) =>
WorkspaceMemberBloc(userProfile: userProfile, workspace: workspace) WorkspaceMemberBloc(userProfile: userProfile, workspace: workspace)
..add(const WorkspaceMemberEvent.initial()) ..add(const WorkspaceMemberEvent.initial()),
..add(const WorkspaceMemberEvent.getWorkspaceMembers()),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>( child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) { builder: (context, state) {
final members = state.members; final members = state.members;

View File

@ -1,6 +1,7 @@
import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/startup.dart'; import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart'; import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
@ -114,6 +115,8 @@ class SettingsDialog extends StatelessWidget {
return const SettingsCustomizeShortcutsWrapper(); return const SettingsCustomizeShortcutsWrapper();
case SettingsPage.member: case SettingsPage.member:
return WorkspaceMembersPage(userProfile: user); return WorkspaceMembersPage(userProfile: user);
case SettingsPage.featureFlags:
return const FeatureFlagsPage();
default: default:
return const SizedBox.shrink(); return const SizedBox.shrink();
} }

View File

@ -0,0 +1,70 @@
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class FeatureFlagsPage extends StatelessWidget {
const FeatureFlagsPage({
super.key,
});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: SeparatedColumn(
children: [
...FeatureFlag.data.entries.map(
(e) => _FeatureFlagItem(featureFlag: e.key),
),
FlowyTextButton(
'Restart the app to apply changes',
fontSize: 16.0,
fontColor: Colors.red,
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 12.0,
),
onPressed: () async {
await runAppFlowy();
},
),
],
),
);
}
}
class _FeatureFlagItem extends StatefulWidget {
const _FeatureFlagItem({
required this.featureFlag,
});
final FeatureFlag featureFlag;
@override
State<_FeatureFlagItem> createState() => _FeatureFlagItemState();
}
class _FeatureFlagItemState extends State<_FeatureFlagItem> {
@override
Widget build(BuildContext context) {
return ListTile(
title: FlowyText(
widget.featureFlag.name,
fontSize: 16.0,
),
subtitle: FlowyText.small(
widget.featureFlag.description,
maxLines: 3,
),
trailing: Switch(
value: widget.featureFlag.isOn,
onChanged: (value) {
setState(() {
widget.featureFlag.update(value);
});
},
),
);
}
}

View File

@ -38,6 +38,8 @@ class WorkspaceMemberBloc
workspaceId = ''; workspaceId = '';
}); });
} }
add(const WorkspaceMemberEvent.getWorkspaceMembers());
}, },
getWorkspaceMembers: () async { getWorkspaceMembers: () async {
final members = await _getWorkspaceMembers(); final members = await _getWorkspaceMembers();

View File

@ -27,9 +27,7 @@ class WorkspaceMembersPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<WorkspaceMemberBloc>( return BlocProvider<WorkspaceMemberBloc>(
create: (context) => WorkspaceMemberBloc(userProfile: userProfile) create: (context) => WorkspaceMemberBloc(userProfile: userProfile)
..add( ..add(const WorkspaceMemberEvent.initial()),
const WorkspaceMemberEvent.getWorkspaceMembers(),
),
child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>( child: BlocBuilder<WorkspaceMemberBloc, WorkspaceMemberState>(
builder: (context, state) { builder: (context, state) {
return SingleChildScrollView( return SingleChildScrollView(

View File

@ -95,7 +95,7 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
return FlowySettingValueDropDown( return FlowySettingValueDropDown(
popoverKey: ThemeFontFamilySetting.popoverKey, popoverKey: ThemeFontFamilySetting.popoverKey,
popoverController: widget.popoverController, popoverController: widget.popoverController,
currentValue: parseFontFamilyName(widget.currentFontFamily), currentValue: widget.currentFontFamily.parseFontFamilyName(),
onClose: () { onClose: () {
query.value = ''; query.value = '';
widget.onClose?.call(); widget.onClose?.call();
@ -162,18 +162,11 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
); );
} }
String parseFontFamilyName(String fontFamilyName) {
final camelCase = RegExp('(?<=[a-z])[A-Z]');
return fontFamilyName
.replaceAll('_regular', '')
.replaceAllMapped(camelCase, (m) => ' ${m.group(0)}');
}
Widget _fontFamilyItemButton( Widget _fontFamilyItemButton(
BuildContext context, BuildContext context,
TextStyle style, TextStyle style,
) { ) {
final buttonFontFamily = parseFontFamilyName(style.fontFamily!); final buttonFontFamily = style.fontFamily!.parseFontFamilyName();
return Tooltip( return Tooltip(
message: buttonFontFamily, message: buttonFontFamily,
@ -184,21 +177,19 @@ class _FontFamilyDropDownState extends State<FontFamilyDropDown> {
child: FlowyButton( child: FlowyButton(
onHover: (_) => FocusScope.of(context).unfocus(), onHover: (_) => FocusScope.of(context).unfocus(),
text: FlowyText.medium( text: FlowyText.medium(
parseFontFamilyName(style.fontFamily!), buttonFontFamily,
fontFamily: style.fontFamily!, fontFamily: style.fontFamily!,
), ),
rightIcon: rightIcon:
buttonFontFamily == parseFontFamilyName(widget.currentFontFamily) buttonFontFamily == widget.currentFontFamily.parseFontFamilyName()
? const FlowySvg( ? const FlowySvg(FlowySvgs.check_s)
FlowySvgs.check_s,
)
: null, : null,
onTap: () { onTap: () {
if (widget.onFontFamilyChanged != null) { if (widget.onFontFamilyChanged != null) {
widget.onFontFamilyChanged!(style.fontFamily!); widget.onFontFamilyChanged!(style.fontFamily!);
} else { } else {
final fontFamily = style.fontFamily!.parseFontFamilyName(); final fontFamily = style.fontFamily!.parseFontFamilyName();
if (parseFontFamilyName(widget.currentFontFamily) != if (widget.currentFontFamily.parseFontFamilyName() !=
buttonFontFamily) { buttonFontFamily) {
context context
.read<AppearanceSettingsCubit>() .read<AppearanceSettingsCubit>()

View File

@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dar
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart'; import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart'; import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class SettingsMenu extends StatelessWidget { class SettingsMenu extends StatelessWidget {
@ -79,6 +80,15 @@ class SettingsMenu extends StatelessWidget {
icon: Icons.people, icon: Icons.people,
changeSelectedPage: changeSelectedPage, changeSelectedPage: changeSelectedPage,
), ),
if (kDebugMode)
SettingsMenuElement(
// no need to translate this page
page: SettingsPage.featureFlags,
selectedPage: currentPage,
label: 'Feature Flags',
icon: Icons.flag,
changeSelectedPage: changeSelectedPage,
),
], ],
), ),
); );

View File

@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
typedef SeparatorBuilder = Widget Function(); typedef SeparatorBuilder = Widget Function();
Widget _defaultColumnSeparatorBuilder() => const Divider();
Widget _defaultRowSeparatorBuilder() => const VerticalDivider();
class SeparatedColumn extends Column { class SeparatedColumn extends Column {
SeparatedColumn({ SeparatedColumn({
super.key, super.key,
@ -11,7 +14,7 @@ class SeparatedColumn extends Column {
super.textBaseline, super.textBaseline,
super.textDirection, super.textDirection,
super.verticalDirection, super.verticalDirection,
required SeparatorBuilder separatorBuilder, SeparatorBuilder separatorBuilder = _defaultColumnSeparatorBuilder,
required List<Widget> children, required List<Widget> children,
}) : super(children: _insertSeparators(children, separatorBuilder)); }) : super(children: _insertSeparators(children, separatorBuilder));
} }
@ -25,7 +28,7 @@ class SeparatedRow extends Row {
super.textBaseline, super.textBaseline,
super.textDirection, super.textDirection,
super.verticalDirection, super.verticalDirection,
required SeparatorBuilder separatorBuilder, SeparatorBuilder separatorBuilder = _defaultRowSeparatorBuilder,
required List<Widget> children, required List<Widget> children,
}) : super(children: _insertSeparators(children, separatorBuilder)); }) : super(children: _insertSeparators(children, separatorBuilder));
} }

View File

@ -53,17 +53,17 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: d4d35c0 ref: ce391a8
resolved-ref: d4d35c0d103a5d1bddf68181fcfaf9f75b0fccb5 resolved-ref: ce391a8c0f492f7b5fdd8f44bbc89fc68882ff23
url: "https://github.com/AppFlowy-IO/appflowy-editor.git" url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git source: git
version: "2.3.2" version: "2.3.3"
appflowy_editor_plugins: appflowy_editor_plugins:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "0223cca" ref: "8f238f2"
resolved-ref: "0223ccabe74b86092d3f3849b69026c89df3b236" resolved-ref: "8f238f214de72e629fe2d90317518c5a0510cdc5"
url: "https://github.com/LucasXu0/appflowy_editor_plugins" url: "https://github.com/LucasXu0/appflowy_editor_plugins"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@ -50,7 +50,7 @@ dependencies:
appflowy_editor_plugins: appflowy_editor_plugins:
git: git:
url: https://github.com/LucasXu0/appflowy_editor_plugins url: https://github.com/LucasXu0/appflowy_editor_plugins
ref: "0223cca" ref: "8f238f2"
appflowy_popover: appflowy_popover:
path: packages/appflowy_popover path: packages/appflowy_popover
@ -167,7 +167,7 @@ dependency_overrides:
appflowy_editor: appflowy_editor:
git: git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "d4d35c0" ref: "ce391a8"
sheet: sheet:
git: git:

View File

@ -1,7 +1,8 @@
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart'; import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
import 'package:bloc_test/bloc_test.dart'; import 'package:bloc_test/bloc_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart'; import 'util.dart';
void main() { void main() {
@ -17,7 +18,8 @@ void main() {
context = await gridTest.createTestGrid(); context = await gridTest.createTestGrid();
}); });
// The initial number of rows is 3 for each grid. // The initial number of rows is 3 for each grid
// We create one row so we expect 4 rows
blocTest<GridBloc, GridState>( blocTest<GridBloc, GridState>(
"create a row", "create a row",
build: () => GridBloc( build: () => GridBloc(

View File

@ -79,8 +79,8 @@
"yjs": "^13.5.51" "yjs": "^13.5.51"
}, },
"devDependencies": { "devDependencies": {
"@svgr/plugin-svgo": "^8.0.1",
"@tauri-apps/cli": "^1.5.6", "@tauri-apps/cli": "^1.5.6",
"@svgr/plugin-svgo": "^8.0.1",
"@types/google-protobuf": "^3.15.12", "@types/google-protobuf": "^3.15.12",
"@types/is-hotkey": "^0.1.7", "@types/is-hotkey": "^0.1.7",
"@types/jest": "^29.5.3", "@types/jest": "^29.5.3",

View File

@ -0,0 +1,31 @@
const fs = require('fs');
const path = require('path');
if (process.argv.length < 3) {
console.error('Usage: node update-tauri-version.js <version>');
process.exit(1);
}
const newVersion = process.argv[2];
const tauriConfigPath = path.join(__dirname, '../src-tauri', 'tauri.conf.json');
fs.readFile(tauriConfigPath, 'utf8', (err, data) => {
if (err) {
console.error('Error reading tauri.conf.json:', err);
return;
}
const config = JSON.parse(data);
config.package.version = newVersion;
fs.writeFile(tauriConfigPath, JSON.stringify(config, null, 2), 'utf8', (err) => {
if (err) {
console.error('Error writing tauri.conf.json:', err);
return;
}
console.log(`Tauri version updated to ${newVersion} successfully.`);
});
});

View File

@ -162,7 +162,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -714,7 +714,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -1313,7 +1313,7 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -2587,7 +2587,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -2604,7 +2604,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -3059,7 +3059,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -4801,7 +4801,7 @@ dependencies = [
[[package]] [[package]]
name = "realtime-entity" name = "realtime-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -4825,7 +4825,7 @@ dependencies = [
[[package]] [[package]]
name = "realtime-protocol" name = "realtime-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -5497,7 +5497,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -6993,7 +6993,7 @@ checksum = "14247bb57be4f377dfb94c72830b8ce8fc6beac03cf4bf7b9732eadd414123fc"
[[package]] [[package]]
name = "websocket" name = "websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",
@ -7456,7 +7456,7 @@ dependencies = [
[[package]] [[package]]
name = "workspace-template" name = "workspace-template"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",

View File

@ -82,7 +82,7 @@ custom-protocol = ["tauri/custom-protocol"]
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eed20c25197109a3f799a5d18d0e38e9f5529d69" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb79b9f5e80846bd06b30b4f9c04039ce1452582" }
# Please use the following script to update collab. # Please use the following script to update collab.
# Working directory: frontend # Working directory: frontend
# #

View File

@ -60,6 +60,7 @@ function DeleteConfirmDialog({ open, title, onOk, onCancel, onClose, okText, can
<Button <Button
className={'w-full'} className={'w-full'}
variant={'outlined'} variant={'outlined'}
color={'inherit'}
onClick={() => { onClick={() => {
onCancel?.(); onCancel?.();
onClose(); onClose();

View File

@ -67,7 +67,7 @@ function RenameDialog({
</DialogContent> </DialogContent>
<Divider className={'mb-1'} /> <Divider className={'mb-1'} />
<DialogActions className={'mb-1 px-4'}> <DialogActions className={'mb-1 px-4'}>
<Button variant={'outlined'} onClick={onClose}> <Button color={'inherit'} variant={'outlined'} onClick={onClose}>
{t('button.cancel')} {t('button.cancel')}
</Button> </Button>
<Button variant={'contained'} onClick={onDone}> <Button variant={'contained'} onClick={onDone}>

View File

@ -49,6 +49,7 @@ export interface KeyboardNavigationProps<T> {
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
itemClassName?: string; itemClassName?: string;
itemStyle?: React.CSSProperties;
} }
function KeyboardNavigation<T>({ function KeyboardNavigation<T>({
@ -67,6 +68,7 @@ function KeyboardNavigation<T>({
onBlur, onBlur,
onFocus, onFocus,
itemClassName, itemClassName,
itemStyle,
}: KeyboardNavigationProps<T>) { }: KeyboardNavigationProps<T>) {
const { t } = useTranslation(); const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -232,6 +234,7 @@ function KeyboardNavigation<T>({
} }
}} }}
selected={isFocused} selected={isFocused}
style={itemStyle}
className={`ml-0 flex w-full items-center justify-start rounded-none px-2 py-1 text-xs ${ className={`ml-0 flex w-full items-center justify-start rounded-none px-2 py-1 text-xs ${
!isFocused ? 'hover:bg-transparent' : '' !isFocused ? 'hover:bg-transparent' : ''
} ${itemClassName ?? ''}`} } ${itemClassName ?? ''}`}
@ -246,7 +249,7 @@ function KeyboardNavigation<T>({
</div> </div>
); );
}, },
[itemClassName, focusedKey, onConfirm, onFocus] [itemClassName, focusedKey, onConfirm, onFocus, itemStyle]
); );
useEffect(() => { useEffect(() => {
@ -284,10 +287,16 @@ function KeyboardNavigation<T>({
onBlur={(e) => { onBlur={(e) => {
e.stopPropagation(); e.stopPropagation();
const target = e.relatedTarget as HTMLElement;
if (target?.closest('.keyboard-navigation')) {
return;
}
onBlur?.(); onBlur?.();
}} }}
autoFocus={!disableFocus} autoFocus={!disableFocus}
className={'flex w-full flex-col gap-1 outline-none'} className={'keyboard-navigation flex w-full flex-col gap-1 outline-none'}
ref={ref} ref={ref}
> >
{options.length > 0 ? ( {options.length > 0 ? (

View File

@ -22,7 +22,7 @@ function ViewBanner({
<div className={'view-banner flex w-full flex-col'}> <div className={'view-banner flex w-full flex-col'}>
{showCover && cover && <ViewCover cover={cover} onUpdateCover={onUpdateCover} />} {showCover && cover && <ViewCover cover={cover} onUpdateCover={onUpdateCover} />}
<div className={'relative min-h-[65px] px-16 pt-4'}> <div className={`relative min-h-[65px] ${showCover ? 'px-16' : ''} pt-4`}>
<div <div
style={{ style={{
display: icon ? 'flex' : 'none', display: icon ? 'flex' : 'none',

View File

@ -22,6 +22,9 @@ function ViewIcon({ icon, onUpdateIcon }: { icon?: PageIcon; onUpdateIcon: (icon
const onEmojiSelect = useCallback( const onEmojiSelect = useCallback(
(emoji: string) => { (emoji: string) => {
onUpdateIcon(emoji); onUpdateIcon(emoji);
if (!emoji) {
setAnchorPosition(undefined);
}
}, },
[onUpdateIcon] [onUpdateIcon]
); );

View File

@ -1,10 +1,11 @@
import { t } from 'i18next';
import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo'; import { AppflowyLogo } from '../../_shared/svg/AppflowyLogo';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { useLogin } from '$app/components/auth/get_started/useLogin'; import { useLogin } from '$app/components/auth/get_started/useLogin';
import { useTranslation } from 'react-i18next';
export const GetStarted = () => { export const GetStarted = () => {
const { onAutoSignInClick } = useLogin(); const { onAutoSignInClick } = useLogin();
const { t } = useTranslation();
return ( return (
<> <>

View File

@ -195,8 +195,8 @@ export const useConnectDatabase = (viewId: string) => {
return database; return database;
}; };
const DatabaseRenderedContext = createContext<() => void>(() => { const DatabaseRenderedContext = createContext<(viewId: string) => void>(() => {
// do nothing return;
}); });
export const DatabaseRenderedProvider = DatabaseRenderedContext.Provider; export const DatabaseRenderedProvider = DatabaseRenderedContext.Provider;

View File

@ -58,65 +58,43 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
} }
}, [viewId]); }, [viewId]);
const parentId = page?.parentId;
useEffect(() => { useEffect(() => {
void handleGetPage(); void handleGetPage();
void handleResetDatabaseViews(viewId); void handleResetDatabaseViews(viewId);
const unsubscribePromise = subscribeNotifications( const unsubscribePromise = subscribeNotifications({
{ [FolderNotification.DidUpdateView]: (changeset) => {
[FolderNotification.DidUpdateView]: (changeset) => { if (changeset.parent_view_id !== viewId && changeset.id !== viewId) return;
setChildViews((prev) => { setChildViews((prev) => {
const index = prev.findIndex((view) => view.id === changeset.id); const index = prev.findIndex((view) => view.id === changeset.id);
if (index === -1) { if (index === -1) {
return prev; 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;
} }
void handleResetDatabaseViews(viewId); const newViews = [...prev];
},
newViews[index] = {
...newViews[index],
name: changeset.name,
};
return newViews;
});
}, },
{ [FolderNotification.DidUpdateChildViews]: (changeset) => {
id: viewId, if (changeset.parent_view_id !== viewId && changeset.parent_view_id !== parentId) return;
} if (changeset.create_child_views.length === 0 && changeset.delete_child_views.length === 0) {
); return;
}
void handleResetDatabaseViews(viewId);
},
});
return () => void unsubscribePromise.then((unsubscribe) => unsubscribe()); return () => void unsubscribePromise.then((unsubscribe) => unsubscribe());
}, [handleGetPage, handleResetDatabaseViews, viewId]); }, [handleGetPage, handleResetDatabaseViews, viewId, parentId]);
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(() => { const value = useMemo(() => {
return Math.max( return Math.max(
@ -183,7 +161,13 @@ export const Database = forwardRef<HTMLDivElement, Props>(({ selectedViewId, set
index={value} index={value}
> >
{childViews.map((view, index) => ( {childViews.map((view, index) => (
<TabPanel className={'flex h-full w-full flex-col'} key={view.id} index={index} value={value}> <TabPanel
data-view-id={view.id}
className={'flex h-full w-full flex-col'}
key={view.id}
index={index}
value={value}
>
<DatabaseLoader viewId={view.id}> <DatabaseLoader viewId={view.id}>
{selectedViewId === view.id && ( {selectedViewId === view.id && (
<> <>

View File

@ -1,12 +1,12 @@
import { FormEventHandler, useCallback } from 'react'; import { FormEventHandler, useCallback } from 'react';
import { t } from 'i18next';
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
import { useAppDispatch, useAppSelector } from '$app/stores/store'; import { useAppDispatch, useAppSelector } from '$app/stores/store';
import { updatePageName } from '$app_reducers/pages/async_actions'; import { updatePageName } from '$app_reducers/pages/async_actions';
import { useTranslation } from 'react-i18next';
export const DatabaseTitle = () => { export const DatabaseTitle = () => {
const viewId = useViewId(); const viewId = useViewId();
const { t } = useTranslation();
const pageName = useAppSelector((state) => state.pages.pageMap[viewId]?.name || ''); const pageName = useAppSelector((state) => state.pages.pageMap[viewId]?.name || '');
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();

View File

@ -9,12 +9,12 @@ import Popover from '@mui/material/Popover';
const initialAnchorOrigin: PopoverOrigin = { const initialAnchorOrigin: PopoverOrigin = {
vertical: 'bottom', vertical: 'bottom',
horizontal: 'left', horizontal: 'center',
}; };
const initialTransformOrigin: PopoverOrigin = { const initialTransformOrigin: PopoverOrigin = {
vertical: 'top', vertical: 'top',
horizontal: 'left', horizontal: 'center',
}; };
const SelectCellActions = lazy( const SelectCellActions = lazy(
() => import('$app/components/database/components/field_types/select/select_cell_actions/SelectCellActions') () => import('$app/components/database/components/field_types/select/select_cell_actions/SelectCellActions')

View File

@ -18,7 +18,7 @@ function DatabaseSettings(props: Props) {
<div className='flex h-[39px] items-center gap-2 border-b border-line-divider'> <div className='flex h-[39px] items-center gap-2 border-b border-line-divider'>
<FilterSettings {...props} /> <FilterSettings {...props} />
<SortSettings {...props} /> <SortSettings {...props} />
<TextButton color='inherit' onClick={(e) => setSettingAnchorEl(e.currentTarget)}> <TextButton className={'min-w-fit'} color='inherit' onClick={(e) => setSettingAnchorEl(e.currentTarget)}>
{t('settings.title')} {t('settings.title')}
</TextButton> </TextButton>
<SettingsMenu <SettingsMenu

View File

@ -23,7 +23,7 @@ function FilterSettings({ onToggleCollection }: { onToggleCollection: (forceOpen
return ( return (
<> <>
<TextButton onClick={handleClick} color={highlight ? 'primary' : 'inherit'}> <TextButton className={'min-w-fit'} onClick={handleClick} color={highlight ? 'primary' : 'inherit'}>
{t('grid.settings.filter')} {t('grid.settings.filter')}
</TextButton> </TextButton>
<FilterFieldsMenu <FilterFieldsMenu

View File

@ -31,7 +31,7 @@ function SortSettings({ onToggleCollection }: Props) {
return ( return (
<> <>
<TextButton className={'p-1'} color={highlight ? 'primary' : 'inherit'} onClick={handleClick}> <TextButton className={'min-w-fit p-1'} color={highlight ? 'primary' : 'inherit'} onClick={handleClick}>
{t('grid.settings.sort')} {t('grid.settings.sort')}
</TextButton> </TextButton>
<SortFieldsMenu <SortFieldsMenu

View File

@ -5,6 +5,7 @@ import dayjs from 'dayjs';
import { ReactComponent as LeftSvg } from '$app/assets/arrow-left.svg'; import { ReactComponent as LeftSvg } from '$app/assets/arrow-left.svg';
import { ReactComponent as RightSvg } from '$app/assets/arrow-right.svg'; import { ReactComponent as RightSvg } from '$app/assets/arrow-right.svg';
import { IconButton } from '@mui/material'; import { IconButton } from '@mui/material';
import './calendar.scss';
function CustomCalendar({ function CustomCalendar({
handleChange, handleChange,

View File

@ -0,0 +1,82 @@
.react-datepicker__month-container {
width: 100%;
border-radius: 0;
}
.react-datepicker__header {
border-radius: 0;
background: transparent;
border-bottom: 0;
}
.react-datepicker__day-names {
border: none;
}
.react-datepicker__day-name {
color: var(--text-caption);
}
.react-datepicker__month {
border: none;
}
.react-datepicker__day {
border: none;
color: var(--text-title);
border-radius: 100%;
}
.react-datepicker__day:hover {
border-radius: 100%;
background: var(--fill-default);
color: var(--content-on-fill);
}
.react-datepicker__day--outside-month {
color: var(--text-caption);
}
.react-datepicker__day--in-range {
background: var(--fill-hover);
color: var(--content-on-fill);
}
.react-datepicker__day--today {
border: 1px solid var(--fill-default);
color: var(--text-title);
border-radius: 100%;
background: transparent;
font-weight: 500;
}
.react-datepicker__day--today:hover{
background: var(--fill-default);
color: var(--content-on-fill);
}
.react-datepicker__day--in-selecting-range, .react-datepicker__day--today.react-datepicker__day--in-range {
background: var(--fill-hover);
color: var(--content-on-fill);
border-color: transparent;
}
.react-datepicker__day--keyboard-selected {
background: transparent;
}
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected {
&.react-datepicker__day--today {
background: var(--fill-default);
color: var(--content-on-fill);
}
background: var(--fill-default) !important;
color: var(--content-on-fill);
}
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected:hover {
background: var(--fill-default);
color: var(--content-on-fill);
}
.react-swipeable-view-container {
height: 100%;
}

View File

@ -1,5 +1,4 @@
import { FC, useMemo, useRef, useState } from 'react'; import { FC, useMemo, useRef, useState } from 'react';
import { t } from 'i18next';
import { Divider, ListSubheader, MenuItem, MenuList, MenuProps, OutlinedInput } from '@mui/material'; import { Divider, ListSubheader, MenuItem, MenuList, MenuProps, OutlinedInput } from '@mui/material';
import { SelectOptionColorPB } from '@/services/backend'; import { SelectOptionColorPB } from '@/services/backend';
import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg'; import { ReactComponent as DeleteSvg } from '$app/assets/delete.svg';
@ -14,6 +13,7 @@ import {
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
import Popover from '@mui/material/Popover'; import Popover from '@mui/material/Popover';
import debounce from 'lodash-es/debounce'; import debounce from 'lodash-es/debounce';
import { useTranslation } from 'react-i18next';
interface SelectOptionMenuProps { interface SelectOptionMenuProps {
fieldId: string; fieldId: string;
@ -34,6 +34,7 @@ const Colors = [
]; ];
export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, option, MenuProps: menuProps }) => { export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, option, MenuProps: menuProps }) => {
const { t } = useTranslation();
const [tagName, setTagName] = useState(option.name); const [tagName, setTagName] = useState(option.name);
const viewId = useViewId(); const viewId = useViewId();
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
@ -83,6 +84,9 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
horizontal: -32, horizontal: -32,
}} }}
{...menuProps} {...menuProps}
onClick={(e) => {
e.stopPropagation();
}}
onClose={onClose} onClose={onClose}
onMouseDown={(e) => { onMouseDown={(e) => {
const isInput = inputRef.current?.contains(e.target as Node); const isInput = inputRef.current?.contains(e.target as Node);
@ -95,6 +99,9 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
<ListSubheader className='my-2 leading-tight'> <ListSubheader className='my-2 leading-tight'>
<OutlinedInput <OutlinedInput
inputRef={inputRef} inputRef={inputRef}
spellCheck={false}
autoCorrect={'off'}
autoCapitalize={'off'}
value={tagName} value={tagName}
onChange={(e) => { onChange={(e) => {
setTagName(e.target.value); setTagName(e.target.value);
@ -108,6 +115,9 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
onClose(); onClose();
} }
}} }}
onClick={(e) => {
e.stopPropagation();
}}
autoFocus={true} autoFocus={true}
placeholder={t('grid.selectOption.tagName')} placeholder={t('grid.selectOption.tagName')}
size='small' size='small'
@ -139,6 +149,7 @@ export const SelectOptionModifyMenu: FC<SelectOptionMenuProps> = ({ fieldId, opt
}} }}
key={color} key={color}
value={color} value={color}
className={'px-1.5'}
> >
<span className={`mr-2 inline-flex h-4 w-4 rounded-full ${SelectOptionColorMap[color]}`} /> <span className={`mr-2 inline-flex h-4 w-4 rounded-full ${SelectOptionColorMap[color]}`} />
<span className='flex-1'>{t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)}</span> <span className='flex-1'>{t(`grid.selectOption.${SelectOptionColorTextMap[color]}`)}</span>

View File

@ -1,16 +0,0 @@
import { MenuItem, MenuItemProps } from '@mui/material';
import { FC } from 'react';
import { Tag } from '../Tag';
export interface CreateOptionProps {
label: React.ReactNode;
onClick?: MenuItemProps['onClick'];
}
export const CreateOption: FC<CreateOptionProps> = ({ label, onClick }) => {
return (
<MenuItem className='px-2' onClick={onClick}>
<Tag className='ml-2' size='small' label={label} />
</MenuItem>
);
};

View File

@ -1,18 +1,17 @@
import React, { FormEvent, useCallback } from 'react'; import React, { FormEvent, useCallback } from 'react';
import { OutlinedInput } from '@mui/material'; import { OutlinedInput } from '@mui/material';
import { t } from 'i18next'; import { useTranslation } from 'react-i18next';
function SearchInput({ function SearchInput({
setNewOptionName, setNewOptionName,
newOptionName, newOptionName,
onEnter, inputRef,
onEscape,
}: { }: {
newOptionName: string; newOptionName: string;
setNewOptionName: (value: string) => void; setNewOptionName: (value: string) => void;
onEnter: () => void; inputRef?: React.RefObject<HTMLInputElement>;
onEscape?: () => void;
}) { }) {
const { t } = useTranslation();
const handleInput = useCallback( const handleInput = useCallback(
(event: FormEvent) => { (event: FormEvent) => {
const value = (event.target as HTMLInputElement).value; const value = (event.target as HTMLInputElement).value;
@ -27,20 +26,10 @@ function SearchInput({
size='small' size='small'
className={'mx-4'} className={'mx-4'}
autoFocus={true} autoFocus={true}
inputRef={inputRef}
value={newOptionName} value={newOptionName}
onInput={handleInput} onInput={handleInput}
spellCheck={false} spellCheck={false}
onKeyDown={(e) => {
if (e.key === 'Enter') {
onEnter();
}
if (e.key === 'Escape') {
e.stopPropagation();
e.preventDefault();
onEscape?.();
}
}}
placeholder={t('grid.selectOption.searchOrCreateOption')} placeholder={t('grid.selectOption.searchOrCreateOption')}
/> />
); );

View File

@ -1,7 +1,4 @@
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react';
import { MenuItem } from '@mui/material';
import { t } from 'i18next';
import { CreateOption } from '$app/components/database/components/field_types/select/select_cell_actions/CreateOption';
import { SelectOptionItem } from '$app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem'; import { SelectOptionItem } from '$app/components/database/components/field_types/select/select_cell_actions/SelectOptionItem';
import { cellService, SelectCell as SelectCellType, SelectField, SelectTypeOption } from '$app/application/database'; import { cellService, SelectCell as SelectCellType, SelectField, SelectTypeOption } from '$app/application/database';
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
@ -12,6 +9,13 @@ import {
import { FieldType } from '@/services/backend'; import { FieldType } from '@/services/backend';
import { useTypeOption } from '$app/components/database'; import { useTypeOption } from '$app/components/database';
import SearchInput from './SearchInput'; import SearchInput from './SearchInput';
import { useTranslation } from 'react-i18next';
import KeyboardNavigation, {
KeyboardNavigationOption,
} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
import { Tag } from '$app/components/database/components/field_types/select/Tag';
const CREATE_OPTION_KEY = 'createOption';
function SelectCellActions({ function SelectCellActions({
field, field,
@ -24,22 +28,43 @@ function SelectCellActions({
onUpdated?: () => void; onUpdated?: () => void;
onClose?: () => void; onClose?: () => void;
}) { }) {
const { t } = useTranslation();
const rowId = cell?.rowId; const rowId = cell?.rowId;
const viewId = useViewId(); const viewId = useViewId();
const typeOption = useTypeOption<SelectTypeOption>(field.id); const typeOption = useTypeOption<SelectTypeOption>(field.id);
const options = useMemo(() => typeOption.options ?? [], [typeOption.options]); const options = useMemo(() => typeOption.options ?? [], [typeOption.options]);
const scrollRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const selectedOptionIds = useMemo(() => cell?.data?.selectedOptionIds ?? [], [cell]); const selectedOptionIds = useMemo(() => cell?.data?.selectedOptionIds ?? [], [cell]);
const [newOptionName, setNewOptionName] = useState(''); const [newOptionName, setNewOptionName] = useState('');
const filteredOptions = useMemo(
() =>
options.filter((option) => {
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
}),
[options, newOptionName]
);
const shouldCreateOption = !!newOptionName && filteredOptions.length === 0; const filteredOptions: KeyboardNavigationOption[] = useMemo(() => {
const result = options
.filter((option) => {
return option.name.toLowerCase().includes(newOptionName.toLowerCase());
})
.map((option) => ({
key: option.id,
content: (
<SelectOptionItem
isSelected={selectedOptionIds?.includes(option.id)}
fieldId={cell?.fieldId || ''}
option={option}
/>
),
}));
if (result.length === 0) {
result.push({
key: CREATE_OPTION_KEY,
content: <Tag size='small' label={newOptionName} />,
});
}
return result;
}, [newOptionName, options, selectedOptionIds, cell?.fieldId]);
const shouldCreateOption = filteredOptions.length === 1 && filteredOptions[0].key === 'createOption';
const updateCell = useCallback( const updateCell = useCallback(
async (optionIds: string[]) => { async (optionIds: string[]) => {
@ -65,90 +90,67 @@ function SelectCellActions({
return option; return option;
}, [viewId, field.id, newOptionName]); }, [viewId, field.id, newOptionName]);
const handleClickOption = useCallback( const onConfirm = useCallback(
(optionId: string) => { async (key: string) => {
let optionId = key;
if (key === CREATE_OPTION_KEY) {
const option = await createOption();
optionId = option?.id || '';
}
if (!optionId) return;
if (field.type === FieldType.SingleSelect) { if (field.type === FieldType.SingleSelect) {
void updateCell([optionId]); const newOptionIds = [optionId];
if (selectedOptionIds?.includes(optionId)) {
newOptionIds.pop();
}
void updateCell(newOptionIds);
return; return;
} }
const prev = selectedOptionIds;
let newOptionIds = []; let newOptionIds = [];
if (!prev) { if (!selectedOptionIds) {
newOptionIds.push(optionId); newOptionIds.push(optionId);
} else { } else {
const isSelected = prev.includes(optionId); const isSelected = selectedOptionIds.includes(optionId);
if (isSelected) { if (isSelected) {
newOptionIds = prev.filter((id) => id !== optionId); newOptionIds = selectedOptionIds.filter((id) => id !== optionId);
} else { } else {
newOptionIds = [...prev, optionId]; newOptionIds = [...selectedOptionIds, optionId];
} }
} }
void updateCell(newOptionIds); void updateCell(newOptionIds);
}, },
[field.type, selectedOptionIds, updateCell] [createOption, field.type, selectedOptionIds, updateCell]
); );
const handleNewTagClick = useCallback(async () => {
if (!cell || !rowId) return;
const option = await createOption();
if (!option) return;
handleClickOption(option.id);
}, [cell, createOption, handleClickOption, rowId]);
const handleEnter = useCallback(() => {
if (shouldCreateOption) {
void handleNewTagClick();
} else {
if (field.type === FieldType.SingleSelect) {
const firstOption = filteredOptions[0];
if (!firstOption) return;
void updateCell([firstOption.id]);
} else {
void updateCell(filteredOptions.map((option) => option.id));
}
}
setNewOptionName('');
}, [field.type, filteredOptions, handleNewTagClick, shouldCreateOption, updateCell]);
return ( return (
<div className={'flex h-full flex-col overflow-hidden'}> <div className={'flex h-full flex-col overflow-hidden'}>
<SearchInput <SearchInput inputRef={inputRef} setNewOptionName={setNewOptionName} newOptionName={newOptionName} />
onEscape={onClose}
setNewOptionName={setNewOptionName}
newOptionName={newOptionName}
onEnter={handleEnter}
/>
<div className='mx-4 mb-2 mt-4 text-xs'> <div className='mx-4 mb-2 mt-4 text-xs'>
{shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')} {shouldCreateOption ? t('grid.selectOption.createNew') : t('grid.selectOption.orSelectOne')}
</div> </div>
<div className={'mx-1 flex-1 overflow-y-auto overflow-x-hidden'}> <div ref={scrollRef} className={'mx-1 flex-1 overflow-y-auto overflow-x-hidden px-1'}>
{shouldCreateOption ? ( <KeyboardNavigation
<CreateOption label={newOptionName} onClick={handleNewTagClick} /> scrollRef={scrollRef}
) : ( focusRef={inputRef}
<div className={' px-2'}> options={filteredOptions}
{filteredOptions.map((option) => ( disableFocus={true}
<MenuItem className={'px-2'} key={option.id} value={option.id}> onConfirm={onConfirm}
<SelectOptionItem onEscape={onClose}
onClick={() => { itemStyle={{
handleClickOption(option.id); borderRadius: '4px',
}} }}
isSelected={selectedOptionIds?.includes(option.id)} />
fieldId={cell?.fieldId || ''}
option={option}
/>
</MenuItem>
))}
</div>
)}
</div> </div>
</div> </div>
); );

View File

@ -10,10 +10,9 @@ export interface SelectOptionItemProps {
option: SelectOption; option: SelectOption;
fieldId: string; fieldId: string;
isSelected?: boolean; isSelected?: boolean;
onClick?: () => void;
} }
export const SelectOptionItem: FC<SelectOptionItemProps> = ({ onClick, isSelected, fieldId, option }) => { export const SelectOptionItem: FC<SelectOptionItemProps> = ({ isSelected, fieldId, option }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const anchorEl = useRef<HTMLDivElement | null>(null); const anchorEl = useRef<HTMLDivElement | null>(null);
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@ -25,7 +24,6 @@ export const SelectOptionItem: FC<SelectOptionItemProps> = ({ onClick, isSelecte
return ( return (
<> <>
<div <div
onClick={onClick}
ref={anchorEl} ref={anchorEl}
className={'flex w-full items-center justify-between'} className={'flex w-full items-center justify-between'}
onMouseEnter={() => setHovered(true)} onMouseEnter={() => setHovered(true)}

View File

@ -15,12 +15,12 @@ export interface FieldProps {
const initialAnchorOrigin: PopoverOrigin = { const initialAnchorOrigin: PopoverOrigin = {
vertical: 'bottom', vertical: 'bottom',
horizontal: 'left', horizontal: 'right',
}; };
const initialTransformOrigin: PopoverOrigin = { const initialTransformOrigin: PopoverOrigin = {
vertical: 'top', vertical: 'top',
horizontal: 'left', horizontal: 'center',
}; };
export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOpened }) => { export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOpened }) => {
@ -54,7 +54,7 @@ export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOp
}, [menuOpened]); }, [menuOpened]);
const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ const { paperHeight, paperWidth, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({
initialPaperWidth: 369, initialPaperWidth: 300,
initialPaperHeight: 400, initialPaperHeight: 400,
anchorPosition, anchorPosition,
initialAnchorOrigin, initialAnchorOrigin,
@ -81,7 +81,7 @@ export const Property: FC<FieldProps> = ({ field, onCloseMenu, className, menuOp
PaperProps={{ PaperProps={{
style: { style: {
maxHeight: paperHeight, maxHeight: paperHeight,
maxWidth: paperWidth, width: paperWidth,
height: 'auto', height: 'auto',
}, },
className: 'flex h-full flex-col overflow-hidden', className: 'flex h-full flex-col overflow-hidden',

View File

@ -1,4 +1,3 @@
import { t } from 'i18next';
import { FC, useMemo, useRef, useState } from 'react'; import { FC, useMemo, useRef, useState } from 'react';
import { SortConditionPB } from '@/services/backend'; import { SortConditionPB } from '@/services/backend';
import KeyboardNavigation, { import KeyboardNavigation, {
@ -6,11 +5,13 @@ import KeyboardNavigation, {
} from '$app/components/_shared/keyboard_navigation/KeyboardNavigation'; } from '$app/components/_shared/keyboard_navigation/KeyboardNavigation';
import { Popover } from '@mui/material'; import { Popover } from '@mui/material';
import { ReactComponent as DropDownSvg } from '$app/assets/more.svg'; import { ReactComponent as DropDownSvg } from '$app/assets/more.svg';
import { useTranslation } from 'react-i18next';
export const SortConditionSelect: FC<{ export const SortConditionSelect: FC<{
onChange?: (value: SortConditionPB) => void; onChange?: (value: SortConditionPB) => void;
value?: SortConditionPB; value?: SortConditionPB;
}> = ({ onChange, value }) => { }> = ({ onChange, value }) => {
const { t } = useTranslation();
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClose = () => { const handleClose = () => {
@ -28,7 +29,7 @@ export const SortConditionSelect: FC<{
content: t('grid.sort.descending'), content: t('grid.sort.descending'),
}, },
]; ];
}, []); }, [t]);
const onConfirm = (optionKey: SortConditionPB) => { const onConfirm = (optionKey: SortConditionPB) => {
onChange?.(optionKey); onChange?.(optionKey);

View File

@ -84,6 +84,7 @@ function ViewActions({ view, pageId, ...props }: { pageId: string; view: Page }
updatePageName({ updatePageName({
id: viewId, id: viewId,
name: val, name: val,
immediate: true,
}) })
); );
setOpenRenameDialog(false); setOpenRenameDialog(false);

View File

@ -2,6 +2,7 @@ import React from 'react';
import { useDatabaseVisibilityRows } from '$app/components/database'; import { useDatabaseVisibilityRows } from '$app/components/database';
import { Field } from '$app/application/database'; import { Field } from '$app/application/database';
import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants'; import { DEFAULT_FIELD_WIDTH, GRID_ACTIONS_WIDTH } from '$app/components/database/grid/constants';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
field: Field; field: Field;
@ -13,6 +14,7 @@ export function GridCalculate({ field, index }: Props) {
const rowMetas = useDatabaseVisibilityRows(); const rowMetas = useDatabaseVisibilityRows();
const count = rowMetas.length; const count = rowMetas.length;
const width = index === 0 ? GRID_ACTIONS_WIDTH : field.width ?? DEFAULT_FIELD_WIDTH; const width = index === 0 ? GRID_ACTIONS_WIDTH : field.width ?? DEFAULT_FIELD_WIDTH;
const { t } = useTranslation();
return ( return (
<div <div
@ -23,7 +25,7 @@ export function GridCalculate({ field, index }: Props) {
> >
{field.isPrimary ? ( {field.isPrimary ? (
<> <>
<span className={'mr-2 text-text-caption'}>Count</span> <span className={'mr-2 text-text-caption'}>{t('grid.calculationTypeLabel.count')}</span>
<span>{count}</span> <span>{count}</span>
</> </>
) : null} ) : null}

View File

@ -133,6 +133,10 @@ export const GridField: FC<GridFieldProps> = memo(
className='relative flex h-full w-full items-center rounded-none px-0' className='relative flex h-full w-full items-center rounded-none px-0'
disableRipple disableRipple
onContextMenu={(event) => { onContextMenu={(event) => {
if (propertyMenuOpened || menuOpened || open) {
return;
}
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
handleClick(); handleClick();

View File

@ -1,8 +1,8 @@
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { rowService } from '$app/application/database'; import { rowService } from '$app/application/database';
import { useViewId } from '$app/hooks'; import { useViewId } from '$app/hooks';
import { t } from 'i18next';
import { ReactComponent as AddSvg } from '$app/assets/add.svg'; import { ReactComponent as AddSvg } from '$app/assets/add.svg';
import { useTranslation } from 'react-i18next';
interface Props { interface Props {
index: number; index: number;
@ -15,6 +15,7 @@ const CSS_HIGHLIGHT_PROPERTY = 'bg-content-blue-50';
function GridNewRow({ index, groupId, getContainerRef }: Props) { function GridNewRow({ index, groupId, getContainerRef }: Props) {
const viewId = useViewId(); const viewId = useViewId();
const { t } = useTranslation();
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
void rowService.createRow(viewId, { void rowService.createRow(viewId, {
groupId, groupId,

View File

@ -10,6 +10,7 @@ import { useGridColumn, useGridRow } from './GridTable.hooks';
import GridStickyHeader from '$app/components/database/grid/grid_sticky_header/GridStickyHeader'; import GridStickyHeader from '$app/components/database/grid/grid_sticky_header/GridStickyHeader';
import GridTableOverlay from '$app/components/database/grid/grid_overlay/GridTableOverlay'; import GridTableOverlay from '$app/components/database/grid/grid_overlay/GridTableOverlay';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import { useViewId } from '$app/hooks';
export interface GridTableProps { export interface GridTableProps {
onEditRecord: (rowId: string) => void; onEditRecord: (rowId: string) => void;
@ -30,6 +31,7 @@ export const GridTable: FC<GridTableProps> = React.memo(({ onEditRecord }) => {
columns, columns,
ref as React.MutableRefObject<Grid<GridColumn[] | { columns: GridColumn[]; renderRows: RenderRow[] }> | null> ref as React.MutableRefObject<Grid<GridColumn[] | { columns: GridColumn[]; renderRows: RenderRow[] }> | null>
); );
const viewId = useViewId();
const { rowHeight } = useGridRow(); const { rowHeight } = useGridRow();
const onRendered = useDatabaseRendered(); const onRendered = useDatabaseRendered();
@ -139,7 +141,7 @@ export const GridTable: FC<GridTableProps> = React.memo(({ onEditRecord }) => {
className={'grid-scroll-container'} className={'grid-scroll-container'}
outerRef={(el) => { outerRef={(el) => {
scrollElementRef.current = el; scrollElementRef.current = el;
onRendered(); onRendered(viewId);
}} }}
innerRef={containerRef} innerRef={containerRef}
> >

View File

@ -3,8 +3,19 @@ import { ViewLayoutPB } from '@/services/backend';
export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) { export function useLoadDatabaseList({ searchText, layout }: { searchText: string; layout: ViewLayoutPB }) {
const list = useAppSelector((state) => { const list = useAppSelector((state) => {
const workspaces = state.workspace.workspaces.map((item) => item.id) ?? [];
return Object.values(state.pages.pageMap).filter((page) => { return Object.values(state.pages.pageMap).filter((page) => {
if (page.layout !== layout) return false; if (page.layout !== layout) return false;
const parentId = page.parentId;
if (!parentId) return false;
const parent = state.pages.pageMap[parentId];
const parentLayout = parent?.layout;
if (!workspaces.includes(parentId) && parentLayout !== ViewLayoutPB.Document) return false;
return page.name.toLowerCase().includes(searchText.toLowerCase()); return page.name.toLowerCase().includes(searchText.toLowerCase());
}); });
}); });

View File

@ -7,17 +7,19 @@ function GridView({ viewId }: { viewId: string }) {
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [rendered, setRendered] = useState(false); const [rendered, setRendered] = useState<{ viewId: string; rendered: boolean } | undefined>(undefined);
// delegate wheel event to layout when grid is scrolled to top or bottom // delegate wheel event to layout when grid is scrolled to top or bottom
useEffect(() => { useEffect(() => {
const element = ref.current; const element = ref.current;
if (!element) { const viewId = rendered?.viewId;
if (!viewId || !element) {
return; return;
} }
const gridScroller = element.querySelector('.grid-scroll-container') as HTMLDivElement; const gridScroller = element.querySelector(`[data-view-id="${viewId}"] .grid-scroll-container`) as HTMLDivElement;
const scrollLayout = gridScroller?.closest('.appflowy-scroll-container') as HTMLDivElement; const scrollLayout = gridScroller?.closest('.appflowy-scroll-container') as HTMLDivElement;
@ -29,7 +31,7 @@ function GridView({ viewId }: { viewId: string }) {
const deltaY = event.deltaY; const deltaY = event.deltaY;
const deltaX = event.deltaX; const deltaX = event.deltaX;
if (deltaX > 10) { if (Math.abs(deltaX) > 8) {
return; return;
} }
@ -50,8 +52,11 @@ function GridView({ viewId }: { viewId: string }) {
}; };
}, [rendered]); }, [rendered]);
const onRendered = useCallback(() => { const onRendered = useCallback((viewId: string) => {
setRendered(true); setRendered({
viewId,
rendered: true,
});
}, []); }, []);
return ( return (

View File

@ -133,9 +133,9 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul
<div ref={scrollRef} className={'mt-1 flex w-full flex-col items-start'}> <div ref={scrollRef} className={'mt-1 flex w-full flex-col items-start'}>
{isActivated && ( {isActivated && (
<KeyboardNavigation <KeyboardNavigation
options={editOptions}
disableFocus={!focusMenu} disableFocus={!focusMenu}
scrollRef={scrollRef} scrollRef={scrollRef}
options={editOptions}
onConfirm={onConfirm} onConfirm={onConfirm}
onFocus={() => { onFocus={() => {
setFocusMenu(true); setFocusMenu(true);
@ -143,8 +143,8 @@ function LinkEditContent({ onClose, defaultHref }: { onClose: () => void; defaul
onBlur={() => { onBlur={() => {
setFocusMenu(false); setFocusMenu(false);
}} }}
onEscape={onClose}
disableSelect={!focusMenu} disableSelect={!focusMenu}
onEscape={onClose}
onKeyDown={(e) => { onKeyDown={(e) => {
e.stopPropagation(); e.stopPropagation();
if (isHotkey('Tab', e)) { if (isHotkey('Tab', e)) {

View File

@ -40,7 +40,7 @@ export function LinkEditPopover({
initialAnchorOrigin, initialAnchorOrigin,
initialTransformOrigin, initialTransformOrigin,
initialPaperWidth: 340, initialPaperWidth: 340,
initialPaperHeight: 180, initialPaperHeight: 200,
}); });
return ( return (

View File

@ -160,7 +160,7 @@ export function ColorPicker({ onEscape, onChange, disableFocus }: ColorPickerPro
}, [renderColorItem, t]); }, [renderColorItem, t]);
return ( return (
<div ref={ref} className={'flex h-full max-h-[360px] w-full flex-col overflow-y-auto'}> <div ref={ref} className={'flex h-full max-h-[420px] w-full flex-col overflow-y-auto'}>
<KeyboardNavigation <KeyboardNavigation
disableFocus={disableFocus} disableFocus={disableFocus}
onPressLeft={onEscape} onPressLeft={onEscape}

View File

@ -14,9 +14,13 @@ const initialOrigin: {
anchorOrigin?: PopoverOrigin; anchorOrigin?: PopoverOrigin;
} = { } = {
anchorOrigin: { anchorOrigin: {
vertical: 'top', vertical: 'center',
horizontal: 'right', horizontal: 'right',
}, },
transformOrigin: {
vertical: 'center',
horizontal: 'left',
},
}; };
export function Color({ export function Color({

View File

@ -12,7 +12,7 @@ const ActionButton = forwardRef<
} & IconButtonProps } & IconButtonProps
>(({ tooltip, onClick, disabled, children, active, className, ...props }, ref) => { >(({ tooltip, onClick, disabled, children, active, className, ...props }, ref) => {
return ( return (
<Tooltip placement={'top'} title={tooltip}> <Tooltip disableInteractive={true} placement={'top'} title={tooltip}>
<IconButton <IconButton
ref={ref} ref={ref}
onClick={onClick} onClick={onClick}

View File

@ -47,7 +47,7 @@ function ColorPopover({
const { paperHeight, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({ const { paperHeight, transformOrigin, anchorOrigin, isEntered } = usePopoverAutoPosition({
initialPaperWidth: 200, initialPaperWidth: 200,
initialPaperHeight: 360, initialPaperHeight: 420,
anchorEl, anchorEl,
initialAnchorOrigin: initialOrigin.anchorOrigin, initialAnchorOrigin: initialOrigin.anchorOrigin,
initialTransformOrigin: initialOrigin.transformOrigin, initialTransformOrigin: initialOrigin.transformOrigin,

View File

@ -112,13 +112,14 @@ export function withPasted(editor: ReactEditor) {
if (isText && parent) { if (isText && parent) {
const [parentNode, parentPath] = parent as NodeEntry<Element>; const [parentNode, parentPath] = parent as NodeEntry<Element>;
const pastedNodeIsPage = parentNode.type === EditorNodeType.Page; const pastedNodeIsPage = parentNode.type === EditorNodeType.Page;
const pastedNodeIsNotList = !LIST_TYPES.includes(parentNode.type as EditorNodeType);
const clonedFragment = transFragment(editor, fragment); const clonedFragment = transFragment(editor, fragment);
const [firstNode, ...otherNodes] = clonedFragment; const [firstNode, ...otherNodes] = clonedFragment;
const lastNode = getLastNode(otherNodes[otherNodes.length - 1]); const lastNode = getLastNode(otherNodes[otherNodes.length - 1]);
const firstIsEmbed = editor.isEmbed(firstNode); const firstIsEmbed = editor.isEmbed(firstNode);
const insertNodes: Element[] = [...otherNodes]; const insertNodes: Element[] = [...otherNodes];
const needMoveChildren = parentNode.children.length > 1 && !pastedNodeIsPage; const needMoveChildren = parentNode.children.length > 1 && !pastedNodeIsPage && !pastedNodeIsNotList;
let moveStartIndex = 0; let moveStartIndex = 0;
if (firstIsEmbed) { if (firstIsEmbed) {
@ -138,7 +139,7 @@ export function withPasted(editor: ReactEditor) {
}); });
if (children.length > 0) { if (children.length > 0) {
if (pastedNodeIsPage) { if (pastedNodeIsPage || pastedNodeIsNotList) {
// lift the children of the first fragment node to current node // lift the children of the first fragment node to current node
insertNodes.unshift(...children); insertNodes.unshift(...children);
} else { } else {

View File

@ -1,4 +1,4 @@
import React, { useCallback, useMemo } from 'react'; import React, { useCallback } from 'react';
import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcrumb.hooks'; import { useLoadExpandedPages } from '$app/components/layout/bread_crumb/Breadcrumb.hooks';
import Breadcrumbs from '@mui/material/Breadcrumbs'; import Breadcrumbs from '@mui/material/Breadcrumbs';
import Link from '@mui/material/Link'; import Link from '@mui/material/Link';
@ -13,7 +13,6 @@ function Breadcrumb() {
const { isTrash, pagePath, currentPage } = useLoadExpandedPages(); const { isTrash, pagePath, currentPage } = useLoadExpandedPages();
const navigate = useNavigate(); const navigate = useNavigate();
const parentPages = useMemo(() => pagePath.slice(1, -1).filter(Boolean) as Page[], [pagePath]);
const navigateToPage = useCallback( const navigateToPage = useCallback(
(page: Page) => { (page: Page) => {
const pageType = pageTypeMap[page.layout]; const pageType = pageTypeMap[page.layout];
@ -33,26 +32,32 @@ function Breadcrumb() {
return ( return (
<Breadcrumbs aria-label='breadcrumb'> <Breadcrumbs aria-label='breadcrumb'>
{parentPages?.map((page: Page) => ( {pagePath?.map((page: Page, index) => {
<Link if (index === pagePath.length - 1) {
key={page.id} return (
className={'flex cursor-pointer select-none gap-1'} <div key={page.id} className={'flex select-none gap-1 text-text-title'}>
underline='hover' <div className={'select-none'}>{getPageIcon(page)}</div>
color='inherit' {page.name || t('menuAppHeader.defaultNewPageName')}
onClick={() => { </div>
navigateToPage(page); );
}} }
>
<div>{getPageIcon(page)}</div>
{page.name || t('document.title.placeholder')} return (
</Link> <Link
))} key={page.id}
className={'flex cursor-pointer select-none gap-1'}
underline='hover'
color='inherit'
onClick={() => {
navigateToPage(page);
}}
>
<div>{getPageIcon(page)}</div>
<div className={'flex select-none gap-1 text-text-title'}> {page.name || t('document.title.placeholder')}
<div className={'select-none'}>{getPageIcon(currentPage)}</div> </Link>
{currentPage.name || t('menuAppHeader.defaultNewPageName')} );
</div> })}
</Breadcrumbs> </Breadcrumbs>
); );
} }

View File

@ -1,72 +1,34 @@
import { useAppSelector } from '$app/stores/store'; import { useAppSelector } from '$app/stores/store';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useMemo } from 'react';
import { useParams, useLocation } from 'react-router-dom'; import { useParams, useLocation } from 'react-router-dom';
import { Page } from '$app_reducers/pages/slice'; import { Page } from '$app_reducers/pages/slice';
import { getPage } from '$app/application/folder/page.service';
export function useLoadExpandedPages() { export function useLoadExpandedPages() {
const params = useParams(); const params = useParams();
const location = useLocation(); const location = useLocation();
const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]); const isTrash = useMemo(() => location.pathname.includes('trash'), [location.pathname]);
const currentPageId = params.id; const currentPageId = params.id;
const pageMap = useAppSelector((state) => state.pages.pageMap); const currentPage = useAppSelector((state) => (currentPageId ? state.pages.pageMap[currentPageId] : undefined));
const currentPage = currentPageId ? pageMap[currentPageId] : null;
const [pagePath, setPagePath] = useState< const pagePath = useAppSelector((state) => {
( const result: Page[] = [];
| Page
| {
name: string;
}
)[]
>([]);
const loadPagePath = useCallback( if (!currentPage) return result;
async (pageId: string) => {
let page = pageMap[pageId];
if (!page) { const findParent = (page: Page) => {
try { if (!page.parentId) return;
page = await getPage(pageId); const parent = state.pages.pageMap[page.parentId];
} catch (e) {
// do nothing
}
if (!page) { if (parent) {
return; result.unshift(parent);
} findParent(parent);
} }
};
setPagePath((prev) => { findParent(currentPage);
return [page, ...prev]; result.push(currentPage);
}); return result;
});
if (page.parentId) {
await loadPagePath(page.parentId);
}
},
[pageMap]
);
useEffect(() => {
setPagePath([]);
if (!currentPageId) {
return;
}
void loadPagePath(currentPageId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPageId]);
useEffect(() => {
setPagePath((prev) => {
return prev.map((page, index) => {
if (!page) return page;
if (index === 0) return page;
return 'id' in page && page.id ? pageMap[page.id] : page;
});
});
}, [pageMap]);
return { return {
pagePath, pagePath,

View File

@ -1,7 +1,34 @@
* {
margin: 0;
padding: 0; .sketch-picker {
background-color: var(--bg-body) !important;
border-color: transparent !important;
box-shadow: none !important;
} }
.sketch-picker .flexbox-fix {
border-color: var(--line-divider) !important;
}
.sketch-picker [id^='rc-editable-input'] {
background-color: var(--bg-body) !important;
border-color: var(--line-divider) !important;
color: var(--text-title) !important;
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
}
.appflowy-date-picker-calendar {
width: 100%;
}
.grid-sticky-header::-webkit-scrollbar {
width: 0;
height: 0;
}
.grid-scroll-container::-webkit-scrollbar {
width: 0;
height: 0;
}
.appflowy-scroll-container { .appflowy-scroll-container {
&::-webkit-scrollbar { &::-webkit-scrollbar {

View File

@ -12,7 +12,16 @@ function NestedPage({ pageId }: { pageId: string }) {
const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId); const { toggleCollapsed, collapsed, childPages } = useLoadChildPages(pageId);
const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId); const { onAddPage, onPageClick, onDeletePage, onDuplicatePage, onRenamePage } = usePageActions(pageId);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const page = useAppSelector((state) => state.pages.pageMap[pageId]); const { page, parentLayout } = useAppSelector((state) => {
const page = state.pages.pageMap[pageId];
const parent = state.pages.pageMap[page?.parentId || ''];
return {
page,
parentLayout: parent?.layout,
};
});
const disableChildren = useAppSelector((state) => { const disableChildren = useAppSelector((state) => {
if (!page) return true; if (!page) return true;
const layout = state.pages.pageMap[page.parentId]?.layout; const layout = state.pages.pageMap[page.parentId]?.layout;
@ -65,6 +74,9 @@ function NestedPage({ pageId }: { pageId: string }) {
} }
}, [dropPosition, isDragging, isDraggingOver, page?.layout]); }, [dropPosition, isDragging, isDraggingOver, page?.layout]);
// Only allow dragging if the parent layout is undefined or a document
const draggable = parentLayout === undefined || parentLayout === ViewLayoutPB.Document;
return ( return (
<div <div
className={className} className={className}
@ -73,7 +85,7 @@ function NestedPage({ pageId }: { pageId: string }) {
onDragOver={onDragOver} onDragOver={onDragOver}
onDragEnd={onDragEnd} onDragEnd={onDragEnd}
onDrop={onDrop} onDrop={onDrop}
draggable={true} draggable={draggable}
data-drop-enabled={page?.layout === ViewLayoutPB.Document} data-drop-enabled={page?.layout === ViewLayoutPB.Document}
data-dragging={isDragging} data-dragging={isDragging}
data-page-id={pageId} data-page-id={pageId}

View File

@ -2,6 +2,7 @@ import { createAsyncThunk } from '@reduxjs/toolkit';
import { RootState } from '$app/stores/store'; import { RootState } from '$app/stores/store';
import { pagesActions } from '$app_reducers/pages/slice'; import { pagesActions } from '$app_reducers/pages/slice';
import { movePage, updatePage } from '$app/application/folder/page.service'; import { movePage, updatePage } from '$app/application/folder/page.service';
import debounce from 'lodash-es/debounce';
export const movePageThunk = createAsyncThunk( export const movePageThunk = createAsyncThunk(
'pages/movePage', 'pages/movePage',
@ -61,12 +62,14 @@ export const movePageThunk = createAsyncThunk(
} }
); );
const debounceUpdateName = debounce(updatePage, 1000);
export const updatePageName = createAsyncThunk( export const updatePageName = createAsyncThunk(
'pages/updateName', 'pages/updateName',
async (payload: { id: string; name: string }, thunkAPI) => { async (payload: { id: string; name: string; immediate?: boolean }, thunkAPI) => {
const { dispatch, getState } = thunkAPI; const { dispatch, getState } = thunkAPI;
const { pageMap } = (getState() as RootState).pages; const { pageMap } = (getState() as RootState).pages;
const { id, name } = payload; const { id, name, immediate } = payload;
const page = pageMap[id]; const page = pageMap[id];
if (name === page.name) return; if (name === page.name) return;
@ -78,9 +81,13 @@ export const updatePageName = createAsyncThunk(
}) })
); );
await updatePage({ if (immediate) {
id, await updatePage({ id, name });
name, } else {
}); await debounceUpdateName({
id,
name,
});
}
} }
); );

View File

@ -1,5 +1,10 @@
@import './variables/index.css'; @import './variables/index.css';
* {
margin: 0;
padding: 0;
}
/* stop body from scrolling */ /* stop body from scrolling */
html, html,
body { body {
@ -53,114 +58,3 @@ th {
@apply text-left font-normal; @apply text-left font-normal;
} }
.sketch-picker {
background-color: var(--bg-body) !important;
border-color: transparent !important;
box-shadow: none !important;
}
.sketch-picker .flexbox-fix {
border-color: var(--line-divider) !important;
}
.sketch-picker [id^='rc-editable-input'] {
background-color: var(--bg-body) !important;
border-color: var(--line-divider) !important;
color: var(--text-title) !important;
box-shadow: var(--line-border) 0px 0px 0px 1px inset !important;
}
.appflowy-date-picker-calendar {
width: 100%;
}
.react-datepicker__month-container {
width: 100%;
border-radius: 0;
}
.react-datepicker__header {
border-radius: 0;
background: transparent;
border-bottom: 0;
}
.react-datepicker__day-names {
border: none;
}
.react-datepicker__day-name {
color: var(--text-caption);
}
.react-datepicker__month {
border: none;
}
.react-datepicker__day {
border: none;
color: var(--text-title);
border-radius: 100%;
}
.react-datepicker__day:hover {
border-radius: 100%;
background: var(--fill-default);
color: var(--content-on-fill);
}
.react-datepicker__day--outside-month {
color: var(--text-caption);
}
.react-datepicker__day--in-range {
background: var(--fill-hover);
color: var(--content-on-fill);
}
.react-datepicker__day--today {
border: 1px solid var(--fill-default);
color: var(--text-title);
border-radius: 100%;
background: transparent;
font-weight: 500;
}
.react-datepicker__day--today:hover{
background: var(--fill-default);
color: var(--content-on-fill);
}
.react-datepicker__day--in-selecting-range, .react-datepicker__day--today.react-datepicker__day--in-range {
background: var(--fill-hover);
color: var(--content-on-fill);
border-color: transparent;
}
.react-datepicker__day--keyboard-selected {
background: transparent;
}
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected {
&.react-datepicker__day--today {
background: var(--fill-default);
color: var(--content-on-fill);
}
background: var(--fill-default) !important;
color: var(--content-on-fill);
}
.react-datepicker__day--range-start, .react-datepicker__day--range-end, .react-datepicker__day--selected:hover {
background: var(--fill-default);
color: var(--content-on-fill);
}
.react-swipeable-view-container {
height: 100%;
}
.grid-sticky-header::-webkit-scrollbar {
width: 0;
height: 0;
}
.grid-scroll-container::-webkit-scrollbar {
width: 0;
height: 0;
}

View File

@ -221,7 +221,7 @@ checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]] [[package]]
name = "app-error" name = "app-error"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -545,7 +545,7 @@ dependencies = [
[[package]] [[package]]
name = "client-api" name = "client-api"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"again", "again",
"anyhow", "anyhow",
@ -946,7 +946,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]] [[package]]
name = "database-entity" name = "database-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -1700,7 +1700,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue" name = "gotrue"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-util", "futures-util",
@ -1717,7 +1717,7 @@ dependencies = [
[[package]] [[package]]
name = "gotrue-entity" name = "gotrue-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -2051,7 +2051,7 @@ dependencies = [
[[package]] [[package]]
name = "infra" name = "infra"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"reqwest", "reqwest",
@ -3295,7 +3295,7 @@ dependencies = [
[[package]] [[package]]
name = "realtime-entity" name = "realtime-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -3319,7 +3319,7 @@ dependencies = [
[[package]] [[package]]
name = "realtime-protocol" name = "realtime-protocol"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bincode", "bincode",
@ -3772,7 +3772,7 @@ dependencies = [
[[package]] [[package]]
name = "shared-entity" name = "shared-entity"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"app-error", "app-error",
@ -4714,7 +4714,7 @@ checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10"
[[package]] [[package]]
name = "websocket" name = "websocket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eed20c25197109a3f799a5d18d0e38e9f5529d69#eed20c25197109a3f799a5d18d0e38e9f5529d69" source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eb79b9f5e80846bd06b30b4f9c04039ce1452582#eb79b9f5e80846bd06b30b4f9c04039ce1452582"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-util", "futures-util",

View File

@ -55,7 +55,11 @@ codegen-units = 1
# Run the script: # Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id # scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️ # ⚠️⚠️⚠️️
<<<<<<< HEAD
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eed20c25197109a3f799a5d18d0e38e9f5529d69" } client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eed20c25197109a3f799a5d18d0e38e9f5529d69" }
=======
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eb79b9f5e80846bd06b30b4f9c04039ce1452582" }
>>>>>>> main
# Please use the following script to update collab. # Please use the following script to update collab.
# Working directory: frontend # Working directory: frontend
# #

Some files were not shown because too many files have changed in this diff Show More