diff --git a/.github/actions/flutter_build/action.yml b/.github/actions/flutter_build/action.yml
index cb7b325e82..81b2845949 100644
--- a/.github/actions/flutter_build/action.yml
+++ b/.github/actions/flutter_build/action.yml
@@ -37,13 +37,6 @@ runs:
override: true
profile: minimal
- - name: Export pub environment variables and add to PATH
- run: |
- if [ "$RUNNER_OS" == "Windows" ]; then
- echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
- fi
- shell: bash
-
- name: Install flutter
id: flutter
uses: subosito/flutter-action@v2
diff --git a/.github/workflows/android_ci.yaml b/.github/workflows/android_ci.yaml.bak
similarity index 100%
rename from .github/workflows/android_ci.yaml
rename to .github/workflows/android_ci.yaml.bak
diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml
index 5755fb7276..2c2143c1fe 100644
--- a/.github/workflows/docker_ci.yml
+++ b/.github/workflows/docker_ci.yml
@@ -7,19 +7,13 @@ on:
- release/*
paths:
- frontend/**
-
pull_request:
branches:
- main
- release/*
paths:
- frontend/**
- types:
- - opened
- - synchronize
- - reopened
- - unlocked
- - ready_for_review
+ types: [opened, synchronize, reopened, unlocked, ready_for_review]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
@@ -33,6 +27,15 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
+ - name: Set up Docker Compose
+ run: |
+ docker-compose --version || {
+ echo "Docker Compose not found, installing..."
+ sudo curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
+ sudo chmod +x /usr/local/bin/docker-compose
+ docker-compose --version
+ }
+
- name: Build the app
shell: bash
run: |
@@ -45,4 +48,4 @@ jobs:
else \
echo "$line"; \
fi; \
- done \
+ done
diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml
index a5cb5f8f96..ffa1d309a5 100644
--- a/.github/workflows/flutter_ci.yaml
+++ b/.github/workflows/flutter_ci.yaml
@@ -7,6 +7,7 @@ on:
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
+ - ".github/actions/flutter_build/**"
- "frontend/rust-lib/**"
- "frontend/appflowy_flutter/**"
- "frontend/resources/**"
@@ -17,6 +18,7 @@ on:
- "release/*"
paths:
- ".github/workflows/flutter_ci.yaml"
+ - ".github/actions/flutter_build/**"
- "frontend/rust-lib/**"
- "frontend/appflowy_flutter/**"
- "frontend/resources/**"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 237f81d125..f8fc9cac64 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -54,13 +54,6 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v4
- - name: Export pub environment variable on Windows
- run: |
- if [ "$RUNNER_OS" == "Windows" ]; then
- echo "PUB_CACHE=$LOCALAPPDATA\\Pub\\Cache" >> $GITHUB_ENV
- fi
- shell: bash
-
- name: Install flutter
uses: subosito/flutter-action@v2
with:
@@ -336,6 +329,7 @@ jobs:
LINUX_PACKAGE_TMP_RPM_NAME: AppFlowy-${{ github.ref_name }}-2.x86_64.rpm
LINUX_PACKAGE_TMP_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-x86_64.AppImage
LINUX_PACKAGE_APPIMAGE_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.AppImage
+ LINUX_PACKAGE_ZIP_NAME: AppFlowy-${{ github.ref_name }}-linux-x86_64.tar.gz
strategy:
fail-fast: false
@@ -412,7 +406,8 @@ jobs:
continue-on-error: true
run: |
sh scripts/linux_distribution/appimage/build_appimage.sh ${{ github.ref_name }}
- cp -r ${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
+ cd ..
+ cp -r frontend/${{ env.LINUX_PACKAGE_TMP_APPIMAGE_NAME }} ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_APPIMAGE_NAME }}
- name: Upload Asset
id: upload-release-asset
@@ -422,7 +417,7 @@ jobs:
with:
upload_url: ${{ needs.create-release.outputs.upload_url }}
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_ZIP_NAME }}
- asset_name: ${{ env.LINUX_ZIP_NAME }}
+ asset_name: ${{ env.LINUX_PACKAGE_ZIP_NAME }}
asset_content_type: application/octet-stream
- name: Upload Debian package
diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml
index 4b30b0043a..7251939de6 100644
--- a/.github/workflows/rust_ci.yaml
+++ b/.github/workflows/rust_ci.yaml
@@ -93,7 +93,8 @@ jobs:
af_cloud_test_base_url: http://localhost
af_cloud_test_ws_url: ws://localhost/ws/v1
af_cloud_test_gotrue_url: http://localhost/gotrue
- run: cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture
+ run: |
+ DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="rev-sqlite,dart" -- --nocapture
- name: rustfmt rust-lib
run: cargo fmt --all -- --check
diff --git a/.github/workflows/tauri2_ci.yaml b/.github/workflows/tauri2_ci.yaml
new file mode 100644
index 0000000000..06ce87d2fe
--- /dev/null
+++ b/.github/workflows/tauri2_ci.yaml
@@ -0,0 +1,113 @@
+name: Tauri2-CI
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/tauri2_ci.yaml"
+ - "frontend/rust-lib/**"
+ - "frontend/appflowy_web_app/**"
+ - "frontend/resources/**"
+
+env:
+ NODE_VERSION: "18.16.0"
+ PNPM_VERSION: "8.5.0"
+ RUST_TOOLCHAIN: "1.75"
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ tauri-build:
+ if: github.event.pull_request.draft != true
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [ ubuntu-20.04 ]
+
+ runs-on: ${{ matrix.platform }}
+
+ env:
+ CI: true
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Maximize build space (ubuntu only)
+ if: matrix.platform == 'ubuntu-20.04'
+ run: |
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf /opt/ghc
+ sudo rm -rf "/usr/local/share/boost"
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ sudo docker image prune --all --force
+ sudo rm -rf /opt/hostedtoolcache/codeQL
+ sudo rm -rf ${GITHUB_WORKSPACE}/.git
+ sudo rm -rf $ANDROID_HOME/ndk
+
+ - name: setup node
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+
+ - name: setup pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: ${{ env.PNPM_VERSION }}
+
+ - name: Install Rust toolchain
+ id: rust_toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: ${{ env.RUST_TOOLCHAIN }}
+ override: true
+ profile: minimal
+
+ - name: Rust cache
+ uses: swatinem/rust-cache@v2
+ with:
+ workspaces: "./frontend/appflowy_web_app/src-tauri -> target"
+
+ - name: Node_modules cache
+ uses: actions/cache@v2
+ with:
+ path: frontend/appflowy_web_app/node_modules
+ key: node-modules-${{ runner.os }}
+
+ - name: install dependencies (windows only)
+ if: matrix.platform == 'windows-latest'
+ working-directory: frontend
+ run: |
+ cargo install --force duckscript_cli
+ vcpkg integrate install
+
+ - name: install dependencies (ubuntu only)
+ if: matrix.platform == 'ubuntu-20.04'
+ working-directory: frontend
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
+
+ - name: install cargo-make
+ working-directory: frontend
+ run: |
+ cargo install --force cargo-make
+ cargo make appflowy-tauri-deps-tools
+
+ - name: install frontend dependencies
+ working-directory: frontend/appflowy_web_app
+ run: |
+ mkdir dist
+ pnpm install
+ cd src-tauri && cargo build
+
+ - name: test and lint
+ working-directory: frontend/appflowy_web_app
+ run: |
+ pnpm run lint:tauri
+
+ - uses: tauri-apps/tauri-action@v0
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tauriScript: pnpm tauri
+ projectPath: frontend/appflowy_web_app
+ args: "--debug"
\ No newline at end of file
diff --git a/.github/workflows/tauri_ci.yaml b/.github/workflows/tauri_ci.yaml
index 8d99091aab..462bebb8dd 100644
--- a/.github/workflows/tauri_ci.yaml
+++ b/.github/workflows/tauri_ci.yaml
@@ -22,7 +22,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- platform: [ubuntu-latest]
+ platform: [ubuntu-20.04]
runs-on: ${{ matrix.platform }}
@@ -32,7 +32,7 @@ jobs:
- uses: actions/checkout@v4
- name: Maximize build space (ubuntu only)
- if: matrix.platform == 'ubuntu-latest'
+ if: matrix.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
@@ -80,7 +80,7 @@ jobs:
vcpkg integrate install
- name: install dependencies (ubuntu only)
- if: matrix.platform == 'ubuntu-latest'
+ if: matrix.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
@@ -110,4 +110,5 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tauriScript: pnpm tauri
- projectPath: frontend/appflowy_tauri
\ No newline at end of file
+ projectPath: frontend/appflowy_tauri
+ args: "--debug"
\ No newline at end of file
diff --git a/.github/workflows/tauri_release.yml b/.github/workflows/tauri_release.yml
index e031e65ccd..2e4be46dbe 100644
--- a/.github/workflows/tauri_release.yml
+++ b/.github/workflows/tauri_release.yml
@@ -31,7 +31,7 @@ jobs:
- platform: macos-latest
args: "--target x86_64-apple-darwin"
target: "macos-x86_64"
- - platform: ubuntu-latest
+ - platform: ubuntu-20.04
args: "--target x86_64-unknown-linux-gnu"
target: "linux-x86_64"
@@ -46,7 +46,7 @@ jobs:
ref: ${{ github.event.inputs.branch }}
- name: Maximize build space (ubuntu only)
- if: matrix.settings.platform == 'ubuntu-latest'
+ if: matrix.settings.platform == 'ubuntu-20.04'
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
@@ -88,7 +88,7 @@ jobs:
vcpkg integrate install
- name: install dependencies (ubuntu only)
- if: matrix.settings.platform == 'ubuntu-latest'
+ if: matrix.settings.platform == 'ubuntu-20.04'
working-directory: frontend
run: |
sudo apt-get update
@@ -140,14 +140,14 @@ jobs:
- name: Upload Deb package(ubuntu only)
uses: actions/upload-artifact@v4
- if: matrix.settings.platform == 'ubuntu-latest'
+ if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.deb
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
- name: Upload AppImage package(ubuntu only)
uses: actions/upload-artifact@v4
- if: matrix.settings.platform == 'ubuntu-latest'
+ if: matrix.settings.platform == 'ubuntu-20.04'
with:
name: ${{ env.PACKAGE_PREFIX }}.AppImage
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
diff --git a/.github/workflows/web2_ci.yaml b/.github/workflows/web2_ci.yaml
new file mode 100644
index 0000000000..31d6f5d8b3
--- /dev/null
+++ b/.github/workflows/web2_ci.yaml
@@ -0,0 +1,61 @@
+name: Web2-CI
+on:
+ pull_request:
+ paths:
+ - ".github/workflows/web2_ci.yaml"
+ - "frontend/appflowy_web_app/**"
+ - "frontend/resources/**"
+env:
+ NODE_VERSION: "18.16.0"
+ PNPM_VERSION: "8.5.0"
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+jobs:
+ web-build:
+ if: github.event.pull_request.draft != true
+ strategy:
+ fail-fast: false
+ matrix:
+ platform: [ ubuntu-latest ]
+
+ runs-on: ${{ matrix.platform }}
+
+ steps:
+ - uses: actions/checkout@v4
+ - name: Maximize build space (ubuntu only)
+ if: matrix.platform == 'ubuntu-latest'
+ run: |
+ sudo rm -rf /usr/share/dotnet
+ sudo rm -rf /opt/ghc
+ sudo rm -rf "/usr/local/share/boost"
+ sudo rm -rf "$AGENT_TOOLSDIRECTORY"
+ sudo docker image prune --all --force
+ sudo rm -rf /opt/hostedtoolcache/codeQL
+ sudo rm -rf ${GITHUB_WORKSPACE}/.git
+ sudo rm -rf $ANDROID_HOME/ndk
+ - name: setup node
+ uses: actions/setup-node@v3
+ with:
+ node-version: ${{ env.NODE_VERSION }}
+ - name: setup pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: ${{ env.PNPM_VERSION }}
+ - name: Node_modules cache
+ uses: actions/cache@v2
+ with:
+ path: frontend/appflowy_web_app/node_modules
+ key: node-modules-${{ runner.os }}
+ - name: install frontend dependencies
+ working-directory: frontend/appflowy_web_app
+ run: |
+ pnpm install
+ - name: test and lint
+ working-directory: frontend/appflowy_web_app
+ run: |
+ pnpm run lint
+ - name: build
+ working-directory: frontend/appflowy_web_app
+ run: |
+ pnpm run build
\ No newline at end of file
diff --git a/.github/workflows/web_ci.yaml b/.github/workflows/web_ci.yaml
index 079a176772..2e92b33226 100644
--- a/.github/workflows/web_ci.yaml
+++ b/.github/workflows/web_ci.yaml
@@ -1,13 +1,12 @@
name: WEB-CI
on:
- pull_request:
- branches:
- - "main"
- paths:
- - ".github/workflows/web_ci.yaml"
- - "frontend/rust-lib/**"
- - "frontend/appflowy_web/**"
+ workflow_dispatch:
+ inputs:
+ build:
+ description: 'Build the web app'
+ required: true
+ default: 'true'
env:
CARGO_TERM_COLOR: always
@@ -22,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- platform: [ubuntu-latest]
+ platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
diff --git a/.gitignore b/.gitignore
index f1f08e6128..33de28002a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,3 +40,5 @@ frontend/package
frontend/*.deb
**/Cargo.toml.bak
+
+**/.cargo/**
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4e59da2ad1..5b040c4607 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,44 @@
# Release Notes
+## Version 0.5.4 - 04/08/2024
+### New Features
+- TBD
+### Bug Fixes
+- TBD
+
+## Version 0.5.3 - 03/21/2024
+### New Features
+- Added build support for 32-bit Android devices
+- Introduced filters for KanBan boards for enhanced organization
+- Introduced the new "Relations" column type in Grids
+- Expanded language support with the addition of Greek
+- Enhanced toolbar design for Mobile devices
+- Introduced a command palette feature with initial support for page search
+### Bug Fixes
+- Rectified the issue of incomplete row data in Grids when adding new rows with active filters
+- Enhanced the logic governing the filtering of number and select/multi-select fields for improved accuracy
+- Implemented UI refinements on both Desktop and Mobile platforms, enriching the overall user experience of AppFlowy
+
+## Version 0.5.2 - 03/13/2024
+### Bug Fixes
+- Import csv file.
+
+## Version 0.5.1 - 03/11/2024
+### New Features
+- Introduced support for performing generic calculations on databases.
+- Implemented functionality for easily duplicating calendar events.
+- Added the ability to duplicate fields with cell data, facilitating smoother data management.
+- Now supports customizing font styles and colors prior to typing.
+- Enhanced the checklist user experience with the integration of keyboard shortcuts.
+- Improved the dark mode experience on mobile devices.
+### Bug Fixes
+- Fixed an issue with some pages failing to sync properly.
+- Fixed an issue where links without the http(s) scheme could not be opened, ensuring consistent link functionality.
+- Fixed an issue that prevented numbers from being inserted before heading blocks.
+- Fixed the inline page reference update mechanism to accurately reflect workspace changes.
+- Fixed an issue that made it difficult to resize images in certain cases.
+- Enhanced image loading reliability by clearing the image cache when images fail to load.
+- Resolved a problem preventing the launching of URLs on some Linux distributions.
+
## Version 0.5.0 - 02/26/2024
### New Features
- Added support for scaling text on mobile platforms for better readability.
diff --git a/README.md b/README.md
index e08d894389..580fa98a48 100644
--- a/README.md
+++ b/README.md
@@ -31,15 +31,15 @@ You are in charge of your data and customizations.
## User Installation
-* [Windows/Mac/Linux](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/mac-windows-linux-packages)
-* [Docker](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/installing-with-docker)
-* [Source](https://appflowy.gitbook.io/docs/essential-documentation/install-appflowy/installation-methods/from-source)
+- [Windows/Mac/Linux](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/mac-windows-linux-packages)
+- [Docker](https://docs.appflowy.io/docs/appflowy/install-appflowy/installation-methods/installing-with-docker)
+- [Source](https://docs.appflowy.io/docs/documentation/appflowy/from-source)
## Built With
-* [Flutter](https://flutter.dev/)
+- [Flutter](https://flutter.dev/)
-* [Rust](https://www.rust-lang.org/)
+- [Rust](https://www.rust-lang.org/)
## Stay Up-to-Date
@@ -51,8 +51,8 @@ Please view the [documentation](https://docs.appflowy.io/docs/documentation/appf
## Roadmap
-* [AppFlowy Roadmap ReadMe](https://appflowy.gitbook.io/docs/essential-documentation/roadmap)
-* [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
+- [AppFlowy Roadmap ReadMe](https://docs.appflowy.io/docs/appflowy/roadmap)
+- [AppFlowy Public Roadmap](https://github.com/orgs/AppFlowy-IO/projects/5/views/12)
If you'd like to propose a feature, submit a feature request [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=feature_request.yaml&title=%5BFR%5D+)
If you'd like to report a bug, submit a bug report [here](https://github.com/AppFlowy-IO/AppFlowy/issues/new?assignees=&labels=&template=bug_report.yaml&title=%5BBug%5D+)
@@ -63,19 +63,17 @@ Please see the [changelog](https://www.appflowy.io/whatsnew) for more details ab
## Contributing
-Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/contributing-to-appflowy) for details.
+Contributions make the open-source community a fantastic place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. Please look at [Contributing to AppFlowy](https://docs.appflowy.io/docs/documentation/software-contributions/contributing-to-appflowy) for details.
If your Pull Request is accepted as it fixes a bug, adds functionality, or makes AppFlowy's codebase significantly easier to use or understand, **Congratulations!** If your administrative and managerial work behind the scenes sustains the community, **Congratulations!** You are now an official contributor to AppFlowy. Get in touch with us ([link](https://tally.so/r/mKP5z3)) to receive the very special Contributor T-shirt!
Proudly wear your T-shirt and show it to us by tagging [@appflowy](https://twitter.com/appflowy) on Twitter.
-
## Translations 🌎🗺
[![translation badge](https://inlang.com/badge?url=github.com/AppFlowy-IO/AppFlowy)](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy?ref=badge)
To add translations, you can manually edit the JSON translation files in `/frontend/resources/translations`, use the [inlang online editor](https://inlang.com/editor/github.com/AppFlowy-IO/AppFlowy), or run `npx inlang machine translate` to add missing translations.
-
## Join the community to build AppFlowy together
@@ -92,14 +90,14 @@ When a customer's evolving core needs are not satisfied, they either switch to a
All these restrictions necessitate our mission - to make it possible for anyone to create apps that suit their needs well.
-* To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
-* To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
+- To individuals, we would like to offer Notion's functionality, data security, and cross-platform native experience.
+- To enterprises and hackers, AppFlowy is dedicated to offering building blocks and collaboration infra services to enable you to make apps on your own. Moreover, you have 100% control of your data. You can design and modify AppFlowy your way, with a single codebase written in Flutter and Rust supporting multiple platforms armed with long-term maintainability.
We decided to achieve this mission by upholding the three most fundamental values:
-* Data privacy first
-* Reliable native experience
-* Community-driven extensibility
+- Data privacy first
+- Reliable native experience
+- Community-driven extensibility
We do not claim to outperform Notion in terms of functionality and design, at least for now. Besides, our priority doesn't lie in more functionality at the moment. Instead, we would like to cultivate a community to democratize the knowledge and wheels of making complex workplace management tools while enabling people and businesses to create beautiful things on their own by equipping them with a versatile toolbox of building blocks.
@@ -111,6 +109,6 @@ Distributed under the AGPLv3 License. See [`LICENSE.md`](https://github.com/AppF
Special thanks to these amazing projects which help power AppFlowy.IO:
-* [flutter-quill](https://github.com/singerdmx/flutter-quill)
-* [cargo-make](https://github.com/sagiegurari/cargo-make)
-* [contrib.rocks](https://contrib.rocks)
+- [flutter-quill](https://github.com/singerdmx/flutter-quill)
+- [cargo-make](https://github.com/sagiegurari/cargo-make)
+- [contrib.rocks](https://contrib.rocks)
diff --git a/frontend/.vscode/launch.json b/frontend/.vscode/launch.json
index 1bc6978a44..72d398e0fa 100644
--- a/frontend/.vscode/launch.json
+++ b/frontend/.vscode/launch.json
@@ -115,9 +115,12 @@
},
{
"name": "AF-desktop: Debug Rust",
- "request": "attach",
"type": "lldb",
+ "request": "attach",
"pid": "${command:pickMyProcess}"
+ // To launch the application directly, use the following configuration:
+ // "request": "launch",
+ // "program": "[YOUR_APPLICATION_PATH]",
},
{
// https://tauri.app/v1/guides/debugging/vs-code
diff --git a/frontend/.vscode/tasks.json b/frontend/.vscode/tasks.json
index 4253215e27..d940eef0a8 100644
--- a/frontend/.vscode/tasks.json
+++ b/frontend/.vscode/tasks.json
@@ -257,7 +257,7 @@
"label": "AF: Tauri UI Dev",
"type": "shell",
"isBackground": true,
- "command": "pnpm run sync:i18n && pnpm run dev",
+ "command": "pnpm sync:i18n && pnpm run dev",
"options": {
"cwd": "${workspaceFolder}/appflowy_tauri"
}
diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml
index 3c8164c477..7157639104 100644
--- a/frontend/Makefile.toml
+++ b/frontend/Makefile.toml
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
-APPFLOWY_VERSION = "0.5.1"
+APPFLOWY_VERSION = "0.5.4"
FLUTTER_DESKTOP_FEATURES = "dart,rev-sqlite"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -50,7 +50,8 @@ APP_ENVIRONMENT = "local"
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
-WEB_LIB_PATH= "appflowy_web/wasm-libs/af-wasm"
+WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
+TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend"
# Test default config
TEST_CRATE_TYPE = "cdylib"
TEST_LIB_EXT = "dylib"
@@ -226,9 +227,8 @@ script = ['''
echo FEATURES: ${FLUTTER_DESKTOP_FEATURES}
echo PRODUCT_EXT: ${PRODUCT_EXT}
echo APP_ENVIRONMENT: ${APP_ENVIRONMENT}
- echo ${platforms}
- echo ${BUILD_ARCHS}
- echo ${BUILD_VERSION}
+ echo BUILD_ARCHS: ${BUILD_ARCHS}
+ echo BUILD_VERSION: ${BUILD_VERSION}
''']
script_runner = "@shell"
diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle
index be3e3dc25c..d14c7016c1 100644
--- a/frontend/appflowy_flutter/android/app/build.gradle
+++ b/frontend/appflowy_flutter/android/app/build.gradle
@@ -52,7 +52,7 @@ android {
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
- minSdkVersion 29
+ minSdkVersion 23
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
diff --git a/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt b/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt
index 38b0aa5ca7..455c5081b6 100644
--- a/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt
+++ b/frontend/appflowy_flutter/android/app/src/main/CMakeLists.txt
@@ -11,6 +11,12 @@ file(COPY
DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/arm64-v8a
)
+# armeabi-v7a
+file(COPY
+ ${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/armeabi-v7a/libc++_shared.so
+ DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/jniLibs/armeabi-v7a
+)
+
# x86_64
file(COPY
${ANDROID_NDK}/sources/cxx-stl/llvm-libc++/libs/x86_64/libc++_shared.so
diff --git a/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md b/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md
new file mode 100644
index 0000000000..5998220774
--- /dev/null
+++ b/frontend/appflowy_flutter/assets/test/workspaces/markdowns/markdown_with_table.md
@@ -0,0 +1,11 @@
+# AppFlowy Test Markdown import with table
+
+# Table
+
+| S.No. | Column 2 |
+| --- | --- |
+| 1. | row 1 |
+| 2. | row 2 |
+| 3. | row 3 |
+| 4. | row 4 |
+| 5. | row 5 |
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart
index 6f58ba6354..1e555b1667 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart
@@ -1,6 +1,7 @@
// ignore_for_file: unused_import
import 'dart:io';
+import 'dart:ui';
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -14,8 +15,9 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
-import 'package:path/path.dart' as p;
import 'package:integration_test/integration_test.dart';
+import 'package:path/path.dart' as p;
+
import '../shared/dir.dart';
import '../shared/mock/mock_file_picker.dart';
import '../shared/util.dart';
diff --git a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart
index 2435d15c3d..c66cdd5cc1 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/cloud_runner.dart
@@ -1,9 +1,12 @@
import 'anon_user_continue_test.dart' as anon_user_continue_test;
import 'appflowy_cloud_auth_test.dart' as appflowy_cloud_auth_test;
-import 'collaborative_workspace_test.dart' as collaboration_workspace_test;
import 'empty_test.dart' as preset_af_cloud_env_test;
// import 'document_sync_test.dart' as document_sync_test;
import 'user_setting_sync_test.dart' as user_sync_test;
+import 'workspace/change_name_and_icon_test.dart'
+ as change_workspace_name_and_icon_test;
+import 'workspace/collaborative_workspace_test.dart'
+ as collaboration_workspace_test;
Future main() async {
preset_af_cloud_env_test.main();
@@ -16,5 +19,7 @@ Future main() async {
anon_user_continue_test.main();
+ // workspace
collaboration_workspace_test.main();
+ change_workspace_name_and_icon_test.main();
}
diff --git a/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart
new file mode 100644
index 0000000000..5e0122c5ef
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/change_name_and_icon_test.dart
@@ -0,0 +1,81 @@
+// ignore_for_file: unused_import
+
+import 'package:appflowy/env/cloud_env.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/shared/feature_flags.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/workspace/application/settings/prelude.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/uuid.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:path/path.dart' as p;
+
+import '../../shared/mock/mock_file_picker.dart';
+import '../../shared/util.dart';
+import '../../shared/workspace.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ const icon = '😄';
+ const name = 'AppFlowy';
+ final email = '${uuid()}@appflowy.io';
+
+ testWidgets('change name and icon', (tester) async {
+ // only run the test when the feature flag is on
+ if (!FeatureFlag.collaborativeWorkspace.isOn) {
+ return;
+ }
+
+ await tester.initializeAppFlowy(
+ cloudType: AuthenticatorType.appflowyCloudSelfHost,
+ email: email, // use the same email to check the next test
+ );
+
+ await tester.tapGoogleLoginInButton();
+ await tester.expectToSeeHomePageWithGetStartedPage();
+
+ var workspaceIcon = tester.widget(
+ find.byType(WorkspaceIcon),
+ );
+ expect(workspaceIcon.workspace.icon, '');
+
+ await tester.openWorkspaceMenu();
+ await tester.changeWorkspaceIcon(icon);
+ await tester.changeWorkspaceName(name);
+
+ workspaceIcon = tester.widget(
+ find.byType(WorkspaceIcon),
+ );
+ expect(workspaceIcon.workspace.icon, icon);
+ expect(find.findTextInFlowyText(name), findsOneWidget);
+ });
+
+ testWidgets('verify the result again after relaunching', (tester) async {
+ // only run the test when the feature flag is on
+ if (!FeatureFlag.collaborativeWorkspace.isOn) {
+ return;
+ }
+
+ await tester.initializeAppFlowy(
+ cloudType: AuthenticatorType.appflowyCloudSelfHost,
+ email: email, // use the same email to check the next test
+ );
+
+ await tester.tapGoogleLoginInButton();
+ await tester.expectToSeeHomePageWithGetStartedPage();
+
+ // check the result again
+ final workspaceIcon = tester.widget(
+ find.byType(WorkspaceIcon),
+ );
+ expect(workspaceIcon.workspace.icon, icon);
+ expect(workspaceIcon.workspace.name, name);
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart
similarity index 90%
rename from frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart
rename to frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart
index 56d9f2bb2c..d1c13eeb47 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/collaborative_workspace_test.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/workspace/collaborative_workspace_test.dart
@@ -23,11 +23,11 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
-import '../shared/database_test_op.dart';
-import '../shared/dir.dart';
-import '../shared/emoji.dart';
-import '../shared/mock/mock_file_picker.dart';
-import '../shared/util.dart';
+import '../../shared/database_test_op.dart';
+import '../../shared/dir.dart';
+import '../../shared/emoji.dart';
+import '../../shared/mock/mock_file_picker.dart';
+import '../../shared/util.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
@@ -35,14 +35,14 @@ void main() {
final email = '${uuid()}@appflowy.io';
group('collaborative workspace', () {
- // only run the test when the feature flag is on
- if (!FeatureFlag.collaborativeWorkspace.isOn) {
- return;
- }
-
// combine the create and delete workspace test to reduce the time
testWidgets('create a new workspace, open it and then delete it',
(tester) async {
+ // only run the test when the feature flag is on
+ if (!FeatureFlag.collaborativeWorkspace.isOn) {
+ return;
+ }
+
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
email: email,
@@ -68,8 +68,9 @@ void main() {
);
// open the newly created workspace
- await tester.tapButton(items.last);
+ await tester.tapButton(items.last, milliseconds: 1000);
success = find.text(LocaleKeys.workspace_openSuccess.tr());
+ await tester.pumpUntilFound(success);
expect(success, findsOneWidget);
await tester.pumpUntilNotFound(success);
diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart
index 30f79e1ac8..60d7ee8423 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/select/select_option.dart';
+import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -327,69 +327,70 @@ void main() {
);
});
- testWidgets('last modified and created at field type options',
- (tester) async {
- await tester.initializeAppFlowy();
- await tester.tapGoButton();
+ // Disable this test because it fails on CI randomly
+ // testWidgets('last modified and created at field type options',
+ // (tester) async {
+ // await tester.initializeAppFlowy();
+ // await tester.tapGoButton();
- await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
- final created = DateTime.now();
+ // await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
+ // final created = DateTime.now();
- // create a created at field
- await tester.tapNewPropertyButton();
- await tester.renameField(FieldType.CreatedTime.i18n);
- await tester.tapSwitchFieldTypeButton();
- await tester.selectFieldType(FieldType.CreatedTime);
- await tester.dismissFieldEditor();
+ // // create a created at field
+ // await tester.tapNewPropertyButton();
+ // await tester.renameField(FieldType.CreatedTime.i18n);
+ // await tester.tapSwitchFieldTypeButton();
+ // await tester.selectFieldType(FieldType.CreatedTime);
+ // await tester.dismissFieldEditor();
- // create a last modified field
- await tester.tapNewPropertyButton();
- await tester.renameField(FieldType.LastEditedTime.i18n);
- await tester.tapSwitchFieldTypeButton();
+ // // create a last modified field
+ // await tester.tapNewPropertyButton();
+ // await tester.renameField(FieldType.LastEditedTime.i18n);
+ // await tester.tapSwitchFieldTypeButton();
- // get time just before modifying
- final modified = DateTime.now();
+ // // get time just before modifying
+ // final modified = DateTime.now();
- // create a last modified field (cont'd)
- await tester.selectFieldType(FieldType.LastEditedTime);
- await tester.dismissFieldEditor();
+ // // create a last modified field (cont'd)
+ // await tester.selectFieldType(FieldType.LastEditedTime);
+ // await tester.dismissFieldEditor();
- tester.assertCellContent(
- rowIndex: 0,
- fieldType: FieldType.CreatedTime,
- content: DateFormat('MMM dd, y HH:mm').format(created),
- );
- tester.assertCellContent(
- rowIndex: 0,
- fieldType: FieldType.LastEditedTime,
- content: DateFormat('MMM dd, y HH:mm').format(modified),
- );
+ // tester.assertCellContent(
+ // rowIndex: 0,
+ // fieldType: FieldType.CreatedTime,
+ // content: DateFormat('MMM dd, y HH:mm').format(created),
+ // );
+ // tester.assertCellContent(
+ // rowIndex: 0,
+ // fieldType: FieldType.LastEditedTime,
+ // content: DateFormat('MMM dd, y HH:mm').format(modified),
+ // );
- // open field editor and change date & time format
- await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n);
- await tester.tapEditFieldButton();
- await tester.changeDateFormat();
- await tester.changeTimeFormat();
- await tester.dismissFieldEditor();
+ // // open field editor and change date & time format
+ // await tester.tapGridFieldWithName(FieldType.LastEditedTime.i18n);
+ // await tester.tapEditFieldButton();
+ // await tester.changeDateFormat();
+ // await tester.changeTimeFormat();
+ // await tester.dismissFieldEditor();
- // open field editor and change date & time format
- await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n);
- await tester.tapEditFieldButton();
- await tester.changeDateFormat();
- await tester.changeTimeFormat();
- await tester.dismissFieldEditor();
+ // // open field editor and change date & time format
+ // await tester.tapGridFieldWithName(FieldType.CreatedTime.i18n);
+ // await tester.tapEditFieldButton();
+ // await tester.changeDateFormat();
+ // await tester.changeTimeFormat();
+ // await tester.dismissFieldEditor();
- // assert format has been changed
- tester.assertCellContent(
- rowIndex: 0,
- fieldType: FieldType.CreatedTime,
- content: DateFormat('dd/MM/y hh:mm a').format(created),
- );
- tester.assertCellContent(
- rowIndex: 0,
- fieldType: FieldType.LastEditedTime,
- content: DateFormat('dd/MM/y hh:mm a').format(modified),
- );
- });
+ // // assert format has been changed
+ // tester.assertCellContent(
+ // rowIndex: 0,
+ // fieldType: FieldType.CreatedTime,
+ // content: DateFormat('dd/MM/y hh:mm a').format(created),
+ // );
+ // tester.assertCellContent(
+ // rowIndex: 0,
+ // fieldType: FieldType.LastEditedTime,
+ // content: DateFormat('dd/MM/y hh:mm a').format(modified),
+ // );
+ // });
});
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart
index 4dfc64ff62..b6db3e1a62 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_filter_test.dart
@@ -103,8 +103,8 @@ void main() {
// select the option 's4'
await tester.tapOptionFilterWithName('s4');
- // The row with 's4' or 's5' should be shown.
- await tester.assertNumberOfRowsInGridPage(2);
+ // The row with 's4' should be shown.
+ await tester.assertNumberOfRowsInGridPage(1);
await tester.pumpAndSettle();
});
diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart
index 7568a81def..9ff563604d 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_expand_test.dart
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
-import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/personal_folder.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -13,10 +13,10 @@ void main() {
group('sidebar expand test', () {
bool isExpanded({required FolderCategoryType type}) {
- if (type == FolderCategoryType.personal) {
+ if (type == FolderCategoryType.private) {
return find
.descendant(
- of: find.byType(PersonalFolder),
+ of: find.byType(PrivateSectionFolder),
matching: find.byType(ViewItem),
)
.evaluate()
@@ -30,19 +30,19 @@ void main() {
await tester.tapGoButton();
// first time is expanded
- expect(isExpanded(type: FolderCategoryType.personal), true);
+ expect(isExpanded(type: FolderCategoryType.private), true);
// collapse the personal folder
await tester.tapButton(
- find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
+ find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
);
- expect(isExpanded(type: FolderCategoryType.personal), false);
+ expect(isExpanded(type: FolderCategoryType.private), false);
// expand the personal folder
await tester.tapButton(
- find.byTooltip(LocaleKeys.sideBar_clickToHidePersonal.tr()),
+ find.byTooltip(LocaleKeys.sideBar_clickToHidePrivate.tr()),
);
- expect(isExpanded(type: FolderCategoryType.personal), true);
+ expect(isExpanded(type: FolderCategoryType.private), true);
});
});
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart
index aa4f151ab8..9ccd06d526 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_favorites_test.dart
@@ -1,5 +1,5 @@
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
-import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/favorite_folder.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/folder/_favorite_folder.dart';
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
diff --git a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart
index bf199036a8..35bcf599ab 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/sidebar/sidebar_test_runner.dart
@@ -1,6 +1,5 @@
import 'package:integration_test/integration_test.dart';
-import 'sidebar_expand_test.dart' as sidebar_expanded_test;
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
import 'sidebar_icon_test.dart' as sidebar_icon_test;
import 'sidebar_test.dart' as sidebar_test;
@@ -10,7 +9,7 @@ void startTesting() {
// Sidebar integration tests
sidebar_test.main();
- sidebar_expanded_test.main();
+ // sidebar_expanded_test.main();
sidebar_favorite_test.main();
sidebar_icon_test.main();
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart
index 58d8cc75be..8f356b8406 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/import_files_test.dart
@@ -1,5 +1,6 @@
import 'dart:io';
+import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -44,5 +45,44 @@ void main() {
tester.expectToSeePageName('test1');
tester.expectToSeePageName('test2');
});
+
+ testWidgets('import markdown file with table', (tester) async {
+ final context = await tester.initializeAppFlowy();
+ await tester.tapGoButton();
+
+ // expect to see a getting started page
+ tester.expectToSeePageName(gettingStarted);
+
+ await tester.tapAddViewButton();
+ await tester.tapImportButton();
+
+ const testFileName = 'markdown_with_table.md';
+ final paths = [];
+ final str = await rootBundle.loadString(
+ 'assets/test/workspaces/markdowns/$testFileName',
+ );
+ final path = p.join(context.applicationDataDirectory, testFileName);
+ paths.add(path);
+ File(path).writeAsStringSync(str);
+ // mock get files
+ mockPickFilePaths(
+ paths: paths,
+ );
+
+ await tester.tapTextAndMarkdownButton();
+
+ tester.expectToSeePageName('markdown_with_table');
+
+ // expect to see all content of markdown file along with table
+ await tester.openPage('markdown_with_table');
+
+ final importedPageEditorState = tester.editor.getCurrentEditorState();
+ expect(importedPageEditorState.getNodeAtPath([0])!.type,
+ HeadingBlockKeys.type,);
+ expect(importedPageEditorState.getNodeAtPath([2])!.type,
+ HeadingBlockKeys.type,);
+ expect(importedPageEditorState.getNodeAtPath([4])!.type,
+ TableBlockKeys.type,);
+ });
});
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart
index e2a343d4f1..137b6e5701 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/switch_folder_test.dart
@@ -5,9 +5,7 @@ import 'package:appflowy/startup/tasks/prelude.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
-import 'package:path/path.dart' as p;
-import '../../shared/mock/mock_file_picker.dart';
import '../../shared/util.dart';
void main() {
@@ -18,80 +16,80 @@ void main() {
return;
}
- testWidgets('switch to B from A, then switch to A again', (tester) async {
- const userA = 'UserA';
- const userB = 'UserB';
+ // testWidgets('switch to B from A, then switch to A again', (tester) async {
+ // const userA = 'UserA';
+ // const userB = 'UserB';
- final initialPath = p.join(userA, appFlowyDataFolder);
- final context = await tester.initializeAppFlowy(
- pathExtension: initialPath,
- );
- // remove the last extension
- final rootPath = context.applicationDataDirectory.replaceFirst(
- initialPath,
- '',
- );
+ // final initialPath = p.join(userA, appFlowyDataFolder);
+ // final context = await tester.initializeAppFlowy(
+ // pathExtension: initialPath,
+ // );
+ // // remove the last extension
+ // final rootPath = context.applicationDataDirectory.replaceFirst(
+ // initialPath,
+ // '',
+ // );
- await tester.tapGoButton();
- await tester.expectToSeeHomePageWithGetStartedPage();
+ // await tester.tapGoButton();
+ // await tester.expectToSeeHomePageWithGetStartedPage();
- // switch to user B
- {
- // set user name for userA
- await tester.openSettings();
- await tester.openSettingsPage(SettingsPage.user);
- await tester.enterUserName(userA);
+ // // switch to user B
+ // {
+ // // set user name for userA
+ // await tester.openSettings();
+ // await tester.openSettingsPage(SettingsPage.user);
+ // await tester.enterUserName(userA);
- await tester.openSettingsPage(SettingsPage.files);
- await tester.pumpAndSettle();
+ // await tester.openSettingsPage(SettingsPage.files);
+ // await tester.pumpAndSettle();
- // mock the file_picker result
- await mockGetDirectoryPath(
- p.join(rootPath, userB),
- );
- await tester.tapCustomLocationButton();
- await tester.pumpAndSettle();
- await tester.expectToSeeHomePageWithGetStartedPage();
+ // // mock the file_picker result
+ // await mockGetDirectoryPath(
+ // p.join(rootPath, userB),
+ // );
+ // await tester.tapCustomLocationButton();
+ // await tester.pumpAndSettle();
+ // await tester.expectToSeeHomePageWithGetStartedPage();
- // set user name for userB
- await tester.openSettings();
- await tester.openSettingsPage(SettingsPage.user);
- await tester.enterUserName(userB);
- }
+ // // set user name for userB
+ // await tester.openSettings();
+ // await tester.openSettingsPage(SettingsPage.user);
+ // await tester.enterUserName(userB);
+ // }
- // switch to the userA
- {
- await tester.openSettingsPage(SettingsPage.files);
- await tester.pumpAndSettle();
+ // // switch to the userA
+ // {
+ // await tester.openSettingsPage(SettingsPage.files);
+ // await tester.pumpAndSettle();
- // mock the file_picker result
- await mockGetDirectoryPath(
- p.join(rootPath, userA),
- );
- await tester.tapCustomLocationButton();
+ // // mock the file_picker result
+ // await mockGetDirectoryPath(
+ // p.join(rootPath, userA),
+ // );
+ // await tester.tapCustomLocationButton();
- await tester.pumpAndSettle();
- await tester.expectToSeeHomePageWithGetStartedPage();
- tester.expectToSeeUserName(userA);
- }
+ // await tester.pumpAndSettle();
+ // await tester.expectToSeeHomePageWithGetStartedPage();
+ // tester.expectToSeeUserName(userA);
+ // }
- // switch to the userB again
- {
- await tester.openSettings();
- await tester.openSettingsPage(SettingsPage.files);
- await tester.pumpAndSettle();
+ // // switch to the userB again
+ // {
+ // await tester.openSettings();
+ // await tester.openSettingsPage(SettingsPage.files);
+ // await tester.pumpAndSettle();
- // mock the file_picker result
- await mockGetDirectoryPath(
- p.join(rootPath, userB),
- );
- await tester.tapCustomLocationButton();
+ // // mock the file_picker result
+ // await mockGetDirectoryPath(
+ // p.join(rootPath, userB),
+ // );
+ // await tester.tapCustomLocationButton();
- await tester.pumpAndSettle();
- await tester.expectToSeeHomePageWithGetStartedPage();
- tester.expectToSeeUserName(userB);
- }
- });
+ // await tester.pumpAndSettle();
+ // await tester.expectToSeeHomePageWithGetStartedPage();
+ // tester.expectToSeeUserName(userB);
+ // }
+ // });
testWidgets('reset to default location', (tester) async {
await tester.initializeAppFlowy();
diff --git a/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart
new file mode 100644
index 0000000000..e75031c955
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/mobile/home_page/create_new_page_test.dart
@@ -0,0 +1,49 @@
+// ignore_for_file: unused_import
+
+import 'dart:io';
+
+import 'package:appflowy/env/cloud_env.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
+import 'package:appflowy/mobile/presentation/home/home.dart';
+import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
+import 'package:appflowy/plugins/document/document_page.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
+import 'package:appflowy/workspace/application/settings/prelude.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/uuid.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:path/path.dart' as p;
+
+import '../../shared/dir.dart';
+import '../../shared/mock/mock_file_picker.dart';
+import '../../shared/util.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('create new page', () {
+ testWidgets('create document', (tester) async {
+ await tester.initializeAppFlowy(
+ cloudType: AuthenticatorType.local,
+ );
+
+ // click the anonymousSignInButton
+ final anonymousSignInButton = find.byType(SignInAnonymousButton);
+ expect(anonymousSignInButton, findsOneWidget);
+ await tester.tapButton(anonymousSignInButton);
+
+ // tap the create page button
+ final createPageButton = find.byKey(mobileCreateNewPageButtonKey);
+ await tester.tapButton(createPageButton);
+ expect(find.byType(MobileDocumentScreen), findsOneWidget);
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart
index f90b151372..9039c843aa 100644
--- a/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart
+++ b/frontend/appflowy_flutter/integration_test/mobile/sign_in/anonymous_sign_in_test.dart
@@ -28,9 +28,7 @@ void main() {
group('anonymous sign in on mobile', () {
testWidgets('anon user and then sign in', (tester) async {
- await tester.initializeAppFlowy(
- cloudType: AuthenticatorType.local,
- );
+ await tester.initializeAppFlowy();
// click the anonymousSignInButton
final anonymousSignInButton = find.byType(SignInAnonymousButton);
diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart
index 50eb062c08..00255f7b3e 100644
--- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart
@@ -1,5 +1,7 @@
import 'dart:io';
+import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
+import 'package:appflowy/plugins/database/widgets/field/field_type_list.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -29,10 +31,8 @@ import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/discl
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/footer/grid_footer.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_editor.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_list.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/number.dart';
+import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
+import 'package:appflowy/plugins/database/widgets/field/type_option_editor/number.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/row/row.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/create_sort_list.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/sort/order_panel.dart';
@@ -54,7 +54,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
-import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_editor.dart';
+import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
import 'package:appflowy/plugins/database/widgets/row/accessory/cell_accessory.dart';
diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart
new file mode 100644
index 0000000000..5137944364
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart
@@ -0,0 +1,58 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/base/icon/icon_picker.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/sidebar_workspace.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'base.dart';
+
+extension AppFlowyWorkspace on WidgetTester {
+ /// Open workspace menu
+ Future openWorkspaceMenu() async {
+ final workspaceWrapper = find.byType(SidebarSwitchWorkspaceButton);
+ expect(workspaceWrapper, findsOneWidget);
+ await tapButton(workspaceWrapper);
+ final workspaceMenu = find.byType(WorkspacesMenu);
+ expect(workspaceMenu, findsOneWidget);
+ }
+
+ /// Open a workspace
+ Future openWorkspace(String name) async {
+ final workspace = find.descendant(
+ of: find.byType(WorkspaceMenuItem),
+ matching: find.findTextInFlowyText(name),
+ );
+ expect(workspace, findsOneWidget);
+ await tapButton(workspace);
+ }
+
+ Future changeWorkspaceName(String name) async {
+ final moreButton = find.descendant(
+ of: find.byType(WorkspaceMenuItem),
+ matching: find.byType(WorkspaceMoreActionList),
+ );
+ expect(moreButton, findsOneWidget);
+ await tapButton(moreButton);
+ await tapButton(find.findTextInFlowyText(LocaleKeys.button_rename.tr()));
+ final input = find.byType(TextFormField);
+ expect(input, findsOneWidget);
+ await enterText(input, name);
+ await tapButton(find.text(LocaleKeys.button_ok.tr()));
+ }
+
+ Future changeWorkspaceIcon(String icon) async {
+ final iconButton = find.descendant(
+ of: find.byType(WorkspaceMenuItem),
+ matching: find.byType(WorkspaceIcon),
+ );
+ expect(iconButton, findsOneWidget);
+ await tapButton(iconButton);
+ final iconPicker = find.byType(FlowyIconPicker);
+ expect(iconPicker, findsOneWidget);
+ await tapButton(find.findTextInFlowyText(icon));
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart
index 94103ffef8..50fdebd203 100644
--- a/frontend/appflowy_flutter/lib/core/config/kv_keys.dart
+++ b/frontend/appflowy_flutter/lib/core/config/kv_keys.dart
@@ -64,4 +64,9 @@ class KVKeys {
/// The value is a json string with the following format:
/// {'feature_flag_1': true, 'feature_flag_2': false}
static const String featureFlag = 'featureFlag';
+
+ /// The key for saving the last opened workspace id
+ ///
+ /// The workspace id is a string.
+ static const String lastOpenedWorkspaceId = 'lastOpenedWorkspaceId';
}
diff --git a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
index c01d72a7fd..e8c9be51d5 100644
--- a/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
+++ b/frontend/appflowy_flutter/lib/core/helpers/url_launcher.dart
@@ -27,8 +27,7 @@ Future afLaunchUrl(
);
} on PlatformException catch (e) {
Log.error('Failed to open uri: $e');
- } finally {
- result = false;
+ return false;
}
// if the uri is not a valid url, try to launch it with http scheme
diff --git a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart
index 4dcaf3fa23..259ab09745 100644
--- a/frontend/appflowy_flutter/lib/core/notification/document_notification.dart
+++ b/frontend/appflowy_flutter/lib/core/notification/document_notification.dart
@@ -5,6 +5,9 @@ import 'package:appflowy_backend/protobuf/flowy-document/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
+// This value should be the same as the DOCUMENT_OBSERVABLE_SOURCE value
+const String _source = 'Document';
+
typedef DocumentNotificationCallback = void Function(
DocumentNotification,
FlowyResult,
@@ -16,7 +19,8 @@ class DocumentNotificationParser
super.id,
required super.callback,
}) : super(
- tyParser: (ty) => DocumentNotification.valueOf(ty),
+ tyParser: (ty, source) =>
+ source == _source ? DocumentNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
diff --git a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart
index 46cba8cbfe..e7304ac14b 100644
--- a/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart
+++ b/frontend/appflowy_flutter/lib/core/notification/folder_notification.dart
@@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
+// This value should be the same as the FOLDER_OBSERVABLE_SOURCE value
+const String _source = 'Workspace';
+
// Folder
typedef FolderNotificationCallback = void Function(
FolderNotification,
@@ -21,7 +24,8 @@ class FolderNotificationParser
super.id,
required super.callback,
}) : super(
- tyParser: (ty) => FolderNotification.valueOf(ty),
+ tyParser: (ty, source) =>
+ source == _source ? FolderNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
diff --git a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart
index 4d67f0bbb0..38676e384c 100644
--- a/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart
+++ b/frontend/appflowy_flutter/lib/core/notification/grid_notification.dart
@@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
+// This value should be the same as the DATABASE_OBSERVABLE_SOURCE value
+const String _source = 'Database';
+
// DatabasePB
typedef DatabaseNotificationCallback = void Function(
DatabaseNotification,
@@ -21,7 +24,8 @@ class DatabaseNotificationParser
super.id,
required super.callback,
}) : super(
- tyParser: (ty) => DatabaseNotification.valueOf(ty),
+ tyParser: (ty, source) =>
+ source == _source ? DatabaseNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
diff --git a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart
index 9aba14cd27..e6ed20fab0 100644
--- a/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart
+++ b/frontend/appflowy_flutter/lib/core/notification/notification_helper.dart
@@ -14,7 +14,7 @@ class NotificationParser {
String? id;
void Function(T, FlowyResult) callback;
E Function(Uint8List) errorParser;
- T? Function(int) tyParser;
+ T? Function(int, String) tyParser;
void parse(SubscribeObject subject) {
if (id != null) {
@@ -23,7 +23,7 @@ class NotificationParser {
}
}
- final ty = tyParser(subject.ty);
+ final ty = tyParser(subject.ty, subject.source);
if (ty == null) {
return;
}
diff --git a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart
index 741f26967c..36c7638df5 100644
--- a/frontend/appflowy_flutter/lib/core/notification/user_notification.dart
+++ b/frontend/appflowy_flutter/lib/core/notification/user_notification.dart
@@ -9,6 +9,9 @@ import 'package:appflowy_result/appflowy_result.dart';
import 'notification_helper.dart';
+// This value should be the same as the USER_OBSERVABLE_SOURCE value
+const String _source = 'User';
+
// User
typedef UserNotificationCallback = void Function(
UserNotification,
@@ -21,7 +24,8 @@ class UserNotificationParser
required String super.id,
required super.callback,
}) : super(
- tyParser: (ty) => UserNotification.valueOf(ty),
+ tyParser: (ty, source) =>
+ source == _source ? UserNotification.valueOf(ty) : null,
errorParser: (bytes) => FlowyError.fromBuffer(bytes),
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
index db4540f3c0..3a2c9a83fa 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
@@ -26,7 +26,7 @@ extension on ViewPB {
String get routeName {
switch (layout) {
case ViewLayoutPB.Document:
- return MobileEditorScreen.routeName;
+ return MobileDocumentScreen.routeName;
case ViewLayoutPB.Grid:
return MobileGridScreen.routeName;
case ViewLayoutPB.Calendar:
@@ -42,8 +42,8 @@ extension on ViewPB {
switch (layout) {
case ViewLayoutPB.Document:
return {
- MobileEditorScreen.viewId: id,
- MobileEditorScreen.viewTitle: name,
+ MobileDocumentScreen.viewId: id,
+ MobileDocumentScreen.viewTitle: name,
};
case ViewLayoutPB.Grid:
return {
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart
index 2a35ab24cc..2341513c25 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar_actions.dart
@@ -77,7 +77,7 @@ class AppBarDoneButton extends StatelessWidget {
Widget build(BuildContext context) {
return AppBarButton(
onTap: onTap,
- padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
+ padding: const EdgeInsets.all(12),
child: FlowyText(
LocaleKeys.button_done.tr(),
color: Theme.of(context).colorScheme.primary,
@@ -93,7 +93,7 @@ class AppBarSaveButton extends StatelessWidget {
super.key,
required this.onTap,
this.enable = true,
- this.padding = const EdgeInsets.fromLTRB(12, 12, 8, 12),
+ this.padding = const EdgeInsets.all(12),
});
final VoidCallback onTap;
@@ -165,7 +165,7 @@ class AppBarMoreButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return AppBarButton(
- padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
+ padding: const EdgeInsets.all(12),
onTap: () => onTap(context),
child: const FlowySvg(FlowySvgs.three_dots_s),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
index 5ed50a2f1a..701b356283 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
@@ -4,7 +4,10 @@ import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
-import 'package:appflowy/plugins/document/document_page.dart';
+import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
+import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
+import 'package:appflowy/plugins/shared/sync_indicator.dart';
+import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
@@ -70,7 +73,21 @@ class _MobileViewPageState extends State {
} else {
body = state.data!.fold((view) {
viewPB = view;
- actions.add(_buildAppBarMoreButton(view));
+ actions.addAll([
+ if (FeatureFlag.syncDocument.isOn) ...[
+ DocumentCollaborators(
+ width: 60,
+ height: 44,
+ fontSize: 14,
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ view: view,
+ ),
+ const HSpace(16.0),
+ DocumentSyncIndicator(view: view),
+ const HSpace(8.0),
+ ],
+ _buildAppBarMoreButton(view),
+ ]);
final plugin = view.plugin(arguments: widget.arguments ?? const {})
..init();
return plugin.widgetBuilder.buildWidget(shrinkWrap: false);
@@ -144,6 +161,8 @@ class _MobileViewPageState extends State {
Widget _buildAppBarMoreButton(ViewPB view) {
return AppBarMoreButton(
onTap: (context) {
+ EditorNotification.exitEditing().post();
+
showMobileBottomSheet(
context,
showDragHandle: true,
@@ -183,14 +202,12 @@ class _MobileViewPageState extends State {
context.read().add(FavoriteEvent.toggle(view));
break;
case MobileViewBottomSheetBodyAction.undo:
- context.dispatchNotification(
- const EditorNotification(type: EditorNotificationType.redo),
- );
+ EditorNotification.undo().post();
context.pop();
break;
case MobileViewBottomSheetBodyAction.redo:
+ EditorNotification.redo().post();
context.pop();
- context.dispatchNotification(EditorNotification.redo());
break;
case MobileViewBottomSheetBodyAction.helpCenter:
// unimplemented
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart
new file mode 100644
index 0000000000..c9d8a48e6b
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart
@@ -0,0 +1,82 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class BottomSheetCloseButton extends StatelessWidget {
+ const BottomSheetCloseButton({
+ super.key,
+ this.onTap,
+ });
+
+ final VoidCallback? onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap ?? () => Navigator.pop(context),
+ child: const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16.0),
+ child: SizedBox(
+ width: 18,
+ height: 18,
+ child: FlowySvg(
+ FlowySvgs.m_bottom_sheet_close_m,
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class BottomSheetDoneButton extends StatelessWidget {
+ const BottomSheetDoneButton({
+ super.key,
+ this.onDone,
+ });
+
+ final VoidCallback? onDone;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onDone ?? () => Navigator.pop(context),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12.0),
+ child: FlowyText(
+ LocaleKeys.button_done.tr(),
+ color: Theme.of(context).colorScheme.primary,
+ fontWeight: FontWeight.w500,
+ textAlign: TextAlign.right,
+ ),
+ ),
+ );
+ }
+}
+
+class BottomSheetBackButton extends StatelessWidget {
+ const BottomSheetBackButton({
+ super.key,
+ this.onTap,
+ });
+
+ final VoidCallback? onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onTap: onTap ?? () => Navigator.pop(context),
+ child: const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 16.0),
+ child: SizedBox(
+ width: 18,
+ height: 18,
+ child: FlowySvg(
+ FlowySvgs.m_app_bar_back_s,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart
index e09b13268c..e1bc32a6f0 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_header.dart
@@ -1,6 +1,4 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
-import 'package:easy_localization/easy_localization.dart';
+import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -26,7 +24,7 @@ class BottomSheetHeader extends StatelessWidget {
left: 0,
child: Align(
alignment: Alignment.centerLeft,
- child: AppBarCloseButton(
+ child: BottomSheetCloseButton(
onTap: onClose,
),
),
@@ -41,19 +39,8 @@ class BottomSheetHeader extends StatelessWidget {
if (onDone != null)
Align(
alignment: Alignment.centerRight,
- child: FlowyButton(
- useIntrinsicWidth: true,
- margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
- decoration: const BoxDecoration(
- borderRadius: BorderRadius.all(Radius.circular(10)),
- color: Color(0xFF00BCF0),
- ),
- text: FlowyText.medium(
- LocaleKeys.button_done.tr(),
- color: Colors.white,
- fontSize: 16.0,
- ),
- onTap: onDone,
+ child: BottomSheetDoneButton(
+ onDone: onDone,
),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart
index 0b0ce92b34..d4f49cb9a9 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart
@@ -52,7 +52,7 @@ class _MobileBottomSheetRenameWidgetState
height: 42.0,
child: FlowyTextField(
controller: controller,
- textInputAction: TextInputAction.done,
+ keyboardType: TextInputType.text,
onSubmitted: (text) => widget.onRename(text),
),
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
index a240f32178..8a51a08176 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
@@ -1,4 +1,4 @@
-import 'package:appflowy/mobile/presentation/base/app_bar_actions.dart';
+import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart' hide WidgetBuilder;
import 'package:flutter/material.dart';
@@ -195,12 +195,12 @@ class BottomSheetHeader extends StatelessWidget {
if (showBackButton)
const Align(
alignment: Alignment.centerLeft,
- child: AppBarBackButton(),
+ child: BottomSheetBackButton(),
),
if (showCloseButton)
const Align(
alignment: Alignment.centerLeft,
- child: AppBarCloseButton(),
+ child: BottomSheetCloseButton(),
),
Align(
child: FlowyText(
@@ -212,8 +212,8 @@ class BottomSheetHeader extends StatelessWidget {
if (showDoneButton)
Align(
alignment: Alignment.centerRight,
- child: AppBarDoneButton(
- onTap: () => Navigator.pop(context),
+ child: BottomSheetDoneButton(
+ onDone: () => Navigator.pop(context),
),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart
index 078da42b9d..549a7f4a8a 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart
@@ -199,7 +199,7 @@ class MobileHiddenGroup extends StatelessWidget {
children: [
Expanded(
child: Text(
- group.groupName,
+ context.read().generateGroupNameFromGroup(group),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart
index 95c9fd0ee0..26c9d462a6 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_full_field_editor.dart
@@ -11,7 +11,7 @@ import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/base/drag_handler.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/date/date_time_format.dart';
+import 'package:appflowy/plugins/database/widgets/field/type_option_editor/date/date_time_format.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart
index fe482f57c4..14c4e022ae 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart
@@ -2,8 +2,8 @@ import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flutter/material.dart';
-class MobileEditorScreen extends StatelessWidget {
- const MobileEditorScreen({
+class MobileDocumentScreen extends StatelessWidget {
+ const MobileDocumentScreen({
super.key,
required this.id,
this.title,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart
index d7d9b7993f..9d8cbf608e 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_folder.dart
@@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
-import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
+import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
+import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -16,22 +16,22 @@ class MobileFavoritePageFolder extends StatelessWidget {
const MobileFavoritePageFolder({
super.key,
required this.userProfile,
- required this.workspaceSetting,
+ required this.workspaceId,
});
final UserProfilePB userProfile;
- final WorkspaceSettingPB workspaceSetting;
+ final String workspaceId;
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider(
- create: (_) => SidebarRootViewsBloc()
+ create: (_) => SidebarSectionsBloc()
..add(
- SidebarRootViewsEvent.initial(
+ SidebarSectionsEvent.initial(
userProfile,
- workspaceSetting.workspaceId,
+ workspaceId,
),
),
),
@@ -39,45 +39,52 @@ class MobileFavoritePageFolder extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
- child: MultiBlocListener(
- listeners: [
- BlocListener(
- listenWhen: (p, c) =>
- p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
- listener: (context, state) =>
- context.pushView(state.lastCreatedRootView!),
- ),
- ],
- child: Builder(
- builder: (context) {
- final favoriteState = context.watch().state;
- if (favoriteState.views.isEmpty) {
- return FlowyMobileStateContainer.info(
- emoji: '😁',
- title: LocaleKeys.favorite_noFavorite.tr(),
- description: LocaleKeys.favorite_noFavoriteHintText.tr(),
+ child: BlocListener(
+ listener: (context, state) {
+ context.read().add(
+ const FavoriteEvent.initial(),
);
- }
- return Scrollbar(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: SlidableAutoCloseBehavior(
- child: Column(
- children: [
- MobileFavoriteFolder(
- showHeader: false,
- forceExpanded: true,
- views: favoriteState.views,
- ),
- const VSpace(100.0),
- ],
+ },
+ child: MultiBlocListener(
+ listeners: [
+ BlocListener(
+ listenWhen: (p, c) =>
+ p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
+ listener: (context, state) =>
+ context.pushView(state.lastCreatedRootView!),
+ ),
+ ],
+ child: Builder(
+ builder: (context) {
+ final favoriteState = context.watch().state;
+ if (favoriteState.views.isEmpty) {
+ return FlowyMobileStateContainer.info(
+ emoji: '😁',
+ title: LocaleKeys.favorite_noFavorite.tr(),
+ description: LocaleKeys.favorite_noFavoriteHintText.tr(),
+ );
+ }
+ return Scrollbar(
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SlidableAutoCloseBehavior(
+ child: Column(
+ children: [
+ MobileFavoriteFolder(
+ showHeader: false,
+ forceExpanded: true,
+ views: favoriteState.views,
+ ),
+ const VSpace(100.0),
+ ],
+ ),
),
),
),
- ),
- );
- },
+ );
+ },
+ ),
),
),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart
index d6e9a18272..7afc740b45 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/favorite/mobile_favorite_page.dart
@@ -4,11 +4,13 @@ import 'package:appflowy/mobile/presentation/favorite/mobile_favorite_folder.dar
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/workspace/application/user/prelude.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
class MobileFavoriteScreen extends StatelessWidget {
const MobileFavoriteScreen({
@@ -50,9 +52,23 @@ class MobileFavoriteScreen extends StatelessWidget {
return Scaffold(
body: SafeArea(
- child: MobileFavoritePage(
- userProfile: userProfile,
- workspaceSetting: workspaceSetting,
+ child: BlocProvider(
+ create: (_) => UserWorkspaceBloc(userProfile: userProfile)
+ ..add(
+ const UserWorkspaceEvent.initial(),
+ ),
+ child: BlocBuilder(
+ buildWhen: (previous, current) =>
+ previous.currentWorkspace?.workspaceId !=
+ current.currentWorkspace?.workspaceId,
+ builder: (context, state) {
+ return MobileFavoritePage(
+ userProfile: userProfile,
+ workspaceId: state.currentWorkspace?.workspaceId ??
+ workspaceSetting.workspaceId,
+ );
+ },
+ ),
),
),
);
@@ -65,11 +81,11 @@ class MobileFavoritePage extends StatelessWidget {
const MobileFavoritePage({
super.key,
required this.userProfile,
- required this.workspaceSetting,
+ required this.workspaceId,
});
final UserProfilePB userProfile;
- final WorkspaceSettingPB workspaceSetting;
+ final String workspaceId;
@override
Widget build(BuildContext context) {
@@ -92,7 +108,7 @@ class MobileFavoritePage extends StatelessWidget {
Expanded(
child: MobileFavoritePageFolder(
userProfile: userProfile,
- workspaceSetting: workspaceSetting,
+ workspaceId: workspaceId,
),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
index cc42b3c9b9..f062bccffd 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
@@ -1,24 +1,28 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
-import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart';
+import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
-import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
-import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
+import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
+import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
+import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
+// Contains Public And Private Sections
class MobileFolders extends StatelessWidget {
const MobileFolders({
super.key,
required this.user,
- required this.workspaceSetting,
+ required this.workspaceId,
required this.showFavorite,
});
final UserProfilePB user;
- final WorkspaceSettingPB workspaceSetting;
+ final String workspaceId;
final bool showFavorite;
@override
@@ -26,11 +30,11 @@ class MobileFolders extends StatelessWidget {
return MultiBlocProvider(
providers: [
BlocProvider(
- create: (_) => SidebarRootViewsBloc()
+ create: (_) => SidebarSectionsBloc()
..add(
- SidebarRootViewsEvent.initial(
+ SidebarSectionsEvent.initial(
user,
- workspaceSetting.workspaceId,
+ workspaceId,
),
),
),
@@ -38,24 +42,51 @@ class MobileFolders extends StatelessWidget {
create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
),
],
- child: MultiBlocListener(
- listeners: [
- BlocListener(
- listenWhen: (p, c) =>
- p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
- listener: (context, state) =>
- context.pushView(state.lastCreatedRootView!),
- ),
- ],
- child: Builder(
- builder: (context) {
- final menuState = context.watch().state;
+ child: BlocListener(
+ listener: (context, state) {
+ context.read().add(
+ SidebarSectionsEvent.initial(
+ user,
+ state.currentWorkspace?.workspaceId ?? workspaceId,
+ ),
+ );
+ },
+ child: BlocConsumer(
+ listenWhen: (p, c) =>
+ p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
+ listener: (context, state) {
+ final lastCreatedRootView = state.lastCreatedRootView;
+ if (lastCreatedRootView != null) {
+ context.pushView(lastCreatedRootView);
+ }
+ },
+ builder: (context, state) {
+ final isCollaborativeWorkspace =
+ context.read().state.isCollabWorkspaceOn;
return SlidableAutoCloseBehavior(
child: Column(
children: [
- MobilePersonalFolder(
- views: menuState.views,
- ),
+ ...isCollaborativeWorkspace
+ ? [
+ MobileSectionFolder(
+ title: LocaleKeys.sideBar_public.tr(),
+ categoryType: FolderCategoryType.public,
+ views: state.section.publicViews,
+ ),
+ const VSpace(8.0),
+ MobileSectionFolder(
+ title: LocaleKeys.sideBar_private.tr(),
+ categoryType: FolderCategoryType.private,
+ views: state.section.privateViews,
+ ),
+ ]
+ : [
+ MobileSectionFolder(
+ title: LocaleKeys.sideBar_personal.tr(),
+ categoryType: FolderCategoryType.public,
+ views: state.section.publicViews,
+ ),
+ ],
const VSpace(8.0),
],
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
index cab7da1fc7..69759fc508 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
@@ -8,6 +8,7 @@ import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
@@ -15,6 +16,7 @@ import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
@@ -82,55 +84,73 @@ class MobileHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return Column(
- children: [
- // Header
- Padding(
- padding: EdgeInsets.only(
- left: 16,
- right: 16,
- top: Platform.isAndroid ? 8.0 : 0.0,
- ),
- child: MobileHomePageHeader(
- userProfile: userProfile,
- ),
+ return BlocProvider(
+ create: (_) => UserWorkspaceBloc(userProfile: userProfile)
+ ..add(
+ const UserWorkspaceEvent.initial(),
),
- const Divider(),
-
- // Folder
- Expanded(
- child: Scrollbar(
- child: SingleChildScrollView(
- child: Padding(
- padding: const EdgeInsets.symmetric(vertical: 8.0),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisSize: MainAxisSize.min,
- children: [
- // Recent files
- const MobileRecentFolder(),
-
- // Folders
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 24),
- child: MobileFolders(
- user: userProfile,
- workspaceSetting: workspaceSetting,
- showFavorite: false,
- ),
- ),
- const SizedBox(height: 8),
- const Padding(
- padding: EdgeInsets.symmetric(horizontal: 24),
- child: _TrashButton(),
- ),
- ],
+ child: BlocBuilder(
+ buildWhen: (previous, current) =>
+ previous.currentWorkspace?.workspaceId !=
+ current.currentWorkspace?.workspaceId,
+ builder: (context, state) {
+ if (state.currentWorkspace == null) {
+ return const SizedBox.shrink();
+ }
+ return Column(
+ children: [
+ // Header
+ Padding(
+ padding: EdgeInsets.only(
+ left: 16,
+ right: 16,
+ top: Platform.isAndroid ? 8.0 : 0.0,
+ ),
+ child: MobileHomePageHeader(
+ userProfile: userProfile,
),
),
- ),
- ),
- ),
- ],
+ const Divider(),
+
+ // Folder
+ Expanded(
+ child: Scrollbar(
+ child: SingleChildScrollView(
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // Recent files
+ const MobileRecentFolder(),
+
+ // Folders
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: MobileFolders(
+ user: userProfile,
+ workspaceId:
+ state.currentWorkspace?.workspaceId ??
+ workspaceSetting.workspaceId,
+ showFavorite: false,
+ ),
+ ),
+ const SizedBox(height: 8),
+ const Padding(
+ padding: EdgeInsets.symmetric(horizontal: 24),
+ child: _TrashButton(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
);
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
index 157f878b75..c6782c0ce1 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
@@ -1,12 +1,16 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
+import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
+import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_user_view.dart';
-import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
@@ -14,7 +18,10 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
class MobileHomePageHeader extends StatelessWidget {
- const MobileHomePageHeader({super.key, required this.userProfile});
+ const MobileHomePageHeader({
+ super.key,
+ required this.userProfile,
+ });
final UserProfilePB userProfile;
@@ -25,33 +32,22 @@ class MobileHomePageHeader extends StatelessWidget {
..add(const SettingsUserEvent.initial()),
child: BlocBuilder(
builder: (context, state) {
- final userIcon = state.userProfile.iconUrl;
+ final isCollaborativeWorkspace =
+ context.read().state.isCollabWorkspaceOn;
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 52),
child: Row(
+ mainAxisSize: MainAxisSize.min,
children: [
- _UserIcon(userIcon: userIcon),
- const HSpace(12),
Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- const FlowyText.medium('AppFlowy', fontSize: 18),
- const VSpace(4),
- FlowyText.regular(
- userProfile.email.isNotEmpty
- ? state.userProfile.email
- : state.userProfile.name,
- fontSize: 12,
- color: Theme.of(context).colorScheme.onSurface,
- overflow: TextOverflow.ellipsis,
- ),
- ],
- ),
+ child: isCollaborativeWorkspace
+ ? _MobileWorkspace(userProfile: userProfile)
+ : _MobileUser(userProfile: userProfile),
),
IconButton(
- onPressed: () =>
- context.push(MobileHomeSettingPage.routeName),
+ onPressed: () => context.push(
+ MobileHomeSettingPage.routeName,
+ ),
icon: const FlowySvg(FlowySvgs.m_setting_m),
),
],
@@ -63,6 +59,154 @@ class MobileHomePageHeader extends StatelessWidget {
}
}
+class _MobileUser extends StatelessWidget {
+ const _MobileUser({
+ required this.userProfile,
+ });
+
+ final UserProfilePB userProfile;
+
+ @override
+ Widget build(BuildContext context) {
+ final userIcon = userProfile.iconUrl;
+ return Row(
+ children: [
+ _UserIcon(userIcon: userIcon),
+ const HSpace(12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const FlowyText.medium('AppFlowy', fontSize: 18),
+ const VSpace(4),
+ FlowyText.regular(
+ userProfile.email.isNotEmpty
+ ? userProfile.email
+ : userProfile.name,
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.onSurface,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _MobileWorkspace extends StatelessWidget {
+ const _MobileWorkspace({
+ required this.userProfile,
+ });
+
+ final UserProfilePB userProfile;
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) {
+ final currentWorkspace = state.currentWorkspace;
+ if (currentWorkspace == null) {
+ return const SizedBox.shrink();
+ }
+ return GestureDetector(
+ onTap: () {
+ context.read().add(
+ const UserWorkspaceEvent.fetchWorkspaces(),
+ );
+ _showSwitchWorkspacesBottomSheet(context);
+ },
+ child: Row(
+ children: [
+ const HSpace(2.0),
+ SizedBox.square(
+ dimension: 34.0,
+ child: WorkspaceIcon(
+ workspace: currentWorkspace,
+ iconSize: 26,
+ enableEdit: false,
+ ),
+ ),
+ const HSpace(8),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ FlowyText.medium(
+ currentWorkspace.name,
+ fontSize: 16.0,
+ overflow: TextOverflow.ellipsis,
+ ),
+ const HSpace(4.0),
+ const FlowySvg(FlowySvgs.list_dropdown_s),
+ ],
+ ),
+ FlowyText.medium(
+ userProfile.email.isNotEmpty
+ ? userProfile.email
+ : userProfile.name,
+ overflow: TextOverflow.ellipsis,
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.onSurface,
+ ),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+ },
+ );
+ }
+
+ void _showSwitchWorkspacesBottomSheet(
+ BuildContext context,
+ ) {
+ showMobileBottomSheet(
+ context,
+ showDivider: false,
+ showHeader: true,
+ showDragHandle: true,
+ title: LocaleKeys.workspace_menuTitle.tr(),
+ builder: (_) {
+ return BlocProvider.value(
+ value: context.read(),
+ child: BlocBuilder(
+ builder: (context, state) {
+ final currentWorkspace = state.currentWorkspace;
+ final workspaces = state.workspaces;
+ if (currentWorkspace == null || workspaces.isEmpty) {
+ return const SizedBox.shrink();
+ }
+ return MobileWorkspaceMenu(
+ userProfile: userProfile,
+ currentWorkspace: currentWorkspace,
+ workspaces: workspaces,
+ onWorkspaceSelected: (workspace) {
+ context.pop();
+
+ if (workspace == currentWorkspace) {
+ return;
+ }
+
+ context.read().add(
+ UserWorkspaceEvent.openWorkspace(
+ workspace.workspaceId,
+ ),
+ );
+ },
+ );
+ },
+ ),
+ );
+ },
+ );
+ }
+}
+
class _UserIcon extends StatelessWidget {
const _UserIcon({
required this.userIcon,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart
index 95e4f6e11a..1cf3b5515b 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_home_recent_views.dart
@@ -1,11 +1,16 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/mobile_recent_view.dart';
+import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/workspace/application/recent/prelude.dart';
+import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
class MobileRecentFolder extends StatefulWidget {
const MobileRecentFolder({super.key});
@@ -22,31 +27,38 @@ class _MobileRecentFolderState extends State {
..add(
const RecentViewsEvent.initial(),
),
- child: BlocBuilder(
- builder: (context, state) {
- final ids = {};
-
- List recentViews = state.views.reversed.toList();
- recentViews.retainWhere((element) => ids.add(element.id));
-
- // only keep the first 20 items.
- recentViews = recentViews.take(20).toList();
-
- if (recentViews.isEmpty) {
- return const SizedBox.shrink();
- }
-
- return Column(
- children: [
- _RecentViews(
- key: ValueKey(recentViews),
- // the recent views are in reverse order
- recentViews: recentViews,
- ),
- const VSpace(12.0),
- ],
- );
+ child: BlocListener(
+ listener: (context, state) {
+ context.read().add(
+ const RecentViewsEvent.fetchRecentViews(),
+ );
},
+ child: BlocBuilder(
+ builder: (context, state) {
+ final ids = {};
+
+ List recentViews = state.views.reversed.toList();
+ recentViews.retainWhere((element) => ids.add(element.id));
+
+ // only keep the first 20 items.
+ recentViews = recentViews.take(20).toList();
+
+ if (recentViews.isEmpty) {
+ return const SizedBox.shrink();
+ }
+
+ return Column(
+ children: [
+ _RecentViews(
+ key: ValueKey(recentViews),
+ // the recent views are in reverse order
+ recentViews: recentViews,
+ ),
+ const VSpace(12.0),
+ ],
+ );
+ },
+ ),
),
);
}
@@ -68,11 +80,70 @@ class _RecentViews extends StatelessWidget {
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
- child: FlowyText.semibold(
- LocaleKeys.sideBar_recent.tr(),
- fontSize: 20.0,
+ child: GestureDetector(
+ child: FlowyText.semibold(
+ LocaleKeys.sideBar_recent.tr(),
+ fontSize: 20.0,
+ ),
+ onTap: () {
+ showMobileBottomSheet(
+ context,
+ showDivider: false,
+ showDragHandle: true,
+ backgroundColor: Theme.of(context).colorScheme.background,
+ builder: (_) {
+ return Column(
+ children: [
+ FlowyOptionTile.text(
+ text: LocaleKeys.button_clear.tr(),
+ leftIcon: FlowySvg(
+ FlowySvgs.m_delete_s,
+ color: Theme.of(context).colorScheme.error,
+ ),
+ textColor: Theme.of(context).colorScheme.error,
+ onTap: () {
+ context.read().add(
+ RecentViewsEvent.removeRecentViews(
+ recentViews.map((e) => e.id).toList(),
+ ),
+ );
+ context.pop();
+ },
+ ),
+ ],
+ );
+ },
+ );
+ },
),
),
+ // Row(
+ // mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ // children: [
+ // Padding(
+ // padding: const EdgeInsets.symmetric(horizontal: 24),
+ // child: FlowyText.semibold(
+ // LocaleKeys.sideBar_recent.tr(),
+ // fontSize: 20.0,
+ // ),
+ // ),
+ // if (kDebugMode)
+ // Padding(
+ // padding: const EdgeInsets.only(right: 16.0),
+ // child: FlowyButton(
+ // useIntrinsicWidth: true,
+ // text: FlowyText(LocaleKeys.button_clear.tr()),
+ // onTap: () {
+ // context.read().add(
+ // RecentViewsEvent.removeRecentViews(
+ // recentViews.map((e) => e.id).toList(),
+ // ),
+ // );
+ // },
+ // ),
+ // ),
+ // ],
+ // ),
SingleChildScrollView(
key: const PageStorageKey('recent_views_page_storage_key'),
scrollDirection: Axis.horizontal,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart
index b315cb5e52..aa938160ae 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/mobile_recent_view.dart
@@ -2,10 +2,10 @@ import 'dart:io';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
+import 'package:appflowy/plugins/document/application/doc_listener.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
-import 'package:appflowy/workspace/application/doc/doc_listener.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
@@ -53,7 +53,7 @@ class _MobileRecentViewState extends State {
documentListener = DocumentListener(id: view.id)
..start(
- didReceiveUpdate: (document) {
+ onDocEventUpdate: (document) {
setState(() {
view = view;
});
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart
similarity index 64%
rename from frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart
rename to frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart
index a5b04a7093..58e34c63d1 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart
@@ -1,26 +1,33 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart';
-import 'package:appflowy/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart';
+import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
+import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-class MobilePersonalFolder extends StatelessWidget {
- const MobilePersonalFolder({
+class MobileSectionFolder extends StatelessWidget {
+ const MobileSectionFolder({
super.key,
+ required this.title,
required this.views,
+ required this.categoryType,
});
+ final String title;
final List views;
+ final FolderCategoryType categoryType;
@override
Widget build(BuildContext context) {
return BlocProvider(
- create: (context) => FolderBloc(type: FolderCategoryType.personal)
+ create: (context) => FolderBloc(type: categoryType)
..add(
const FolderEvent.initial(),
),
@@ -28,14 +35,25 @@ class MobilePersonalFolder extends StatelessWidget {
builder: (context, state) {
return Column(
children: [
- MobilePersonalFolderHeader(
+ MobileSectionFolderHeader(
+ title: title,
isExpanded: context.read().state.isExpanded,
onPressed: () => context
.read()
.add(const FolderEvent.expandOrUnExpand()),
- onAdded: () => context.read().add(
- const FolderEvent.expandOrUnExpand(isExpanded: true),
- ),
+ onAdded: () {
+ context.read().add(
+ SidebarSectionsEvent.createRootViewInSection(
+ name:
+ LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ index: 0,
+ viewSection: categoryType.toViewSectionPB,
+ ),
+ );
+ context.read().add(
+ const FolderEvent.expandOrUnExpand(isExpanded: true),
+ );
+ },
),
const VSpace(8.0),
const Divider(
@@ -45,9 +63,9 @@ class MobilePersonalFolder extends StatelessWidget {
...views.map(
(view) => MobileViewItem(
key: ValueKey(
- '${FolderCategoryType.personal.name} ${view.id}',
+ '${FolderCategoryType.private.name} ${view.id}',
),
- categoryType: FolderCategoryType.personal,
+ categoryType: categoryType,
isFirstChild: view.id == views.first.id,
view: view,
level: 0,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart
similarity index 65%
rename from frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart
rename to frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart
index 6e77f86454..3ba15df25d 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/personal_folder/mobile_home_personal_folder_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart
@@ -1,30 +1,30 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
-import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-class MobilePersonalFolderHeader extends StatefulWidget {
- const MobilePersonalFolderHeader({
+@visibleForTesting
+const Key mobileCreateNewPageButtonKey = Key('mobileCreateNewPageButtonKey');
+
+class MobileSectionFolderHeader extends StatefulWidget {
+ const MobileSectionFolderHeader({
super.key,
+ required this.title,
required this.onPressed,
required this.onAdded,
required this.isExpanded,
});
+ final String title;
final VoidCallback onPressed;
final VoidCallback onAdded;
final bool isExpanded;
@override
- State createState() =>
- _MobilePersonalFolderHeaderState();
+ State createState() =>
+ _MobileSectionFolderHeaderState();
}
-class _MobilePersonalFolderHeaderState
- extends State {
+class _MobileSectionFolderHeaderState extends State {
double _turns = 0;
@override
@@ -35,7 +35,7 @@ class _MobilePersonalFolderHeaderState
Expanded(
child: FlowyButton(
text: FlowyText.semibold(
- LocaleKeys.sideBar_personal.tr(),
+ widget.title,
fontSize: 20.0,
),
margin: const EdgeInsets.symmetric(vertical: 8),
@@ -58,6 +58,7 @@ class _MobilePersonalFolderHeaderState
),
),
FlowyIconButton(
+ key: mobileCreateNewPageButtonKey,
hoverColor: Theme.of(context).colorScheme.secondaryContainer,
iconPadding: const EdgeInsets.all(2),
height: iconSize,
@@ -66,14 +67,7 @@ class _MobilePersonalFolderHeaderState
FlowySvgs.add_s,
size: Size.square(iconSize),
),
- onPressed: () {
- context.read().add(
- SidebarRootViewsEvent.createRootView(
- LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
- index: 0,
- ),
- );
- },
+ onPressed: widget.onAdded,
),
],
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart
new file mode 100644
index 0000000000..a41e8aba97
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart
@@ -0,0 +1,119 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
+import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_bloc.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+// Only works on mobile.
+class MobileWorkspaceMenu extends StatelessWidget {
+ const MobileWorkspaceMenu({
+ super.key,
+ required this.userProfile,
+ required this.currentWorkspace,
+ required this.workspaces,
+ required this.onWorkspaceSelected,
+ });
+
+ final UserProfilePB userProfile;
+ final UserWorkspacePB currentWorkspace;
+ final List workspaces;
+ final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
+
+ @override
+ Widget build(BuildContext context) {
+ final List children = [];
+ for (var i = 0; i < workspaces.length; i++) {
+ final workspace = workspaces[i];
+ children.add(
+ _WorkspaceMenuItem(
+ userProfile: userProfile,
+ workspace: workspace,
+ showTopBorder: i == 0,
+ currentWorkspace: currentWorkspace,
+ onWorkspaceSelected: onWorkspaceSelected,
+ ),
+ );
+ }
+ return Column(
+ children: children,
+ );
+ }
+}
+
+class _WorkspaceMenuItem extends StatelessWidget {
+ const _WorkspaceMenuItem({
+ required this.userProfile,
+ required this.workspace,
+ required this.showTopBorder,
+ required this.currentWorkspace,
+ required this.onWorkspaceSelected,
+ });
+
+ final UserProfilePB userProfile;
+ final UserWorkspacePB workspace;
+ final bool showTopBorder;
+ final UserWorkspacePB currentWorkspace;
+ final void Function(UserWorkspacePB workspace) onWorkspaceSelected;
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider(
+ create: (_) => WorkspaceMemberBloc(
+ userProfile: userProfile,
+ workspace: workspace,
+ )..add(const WorkspaceMemberEvent.initial()),
+ child: BlocBuilder(
+ builder: (context, state) {
+ final members = state.members;
+ return FlowyOptionTile.text(
+ content: Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(left: 12),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FlowyText(
+ workspace.name,
+ fontSize: 14,
+ fontWeight: FontWeight.w500,
+ ),
+ FlowyText(
+ state.isLoading
+ ? ''
+ : LocaleKeys.settings_appearance_members_membersCount
+ .plural(
+ members.length,
+ ),
+ fontSize: 10.0,
+ color: Theme.of(context).hintColor,
+ ),
+ ],
+ ),
+ ),
+ ),
+ height: 60,
+ showTopBorder: showTopBorder,
+ leftIcon: WorkspaceIcon(
+ enableEdit: false,
+ iconSize: 26,
+ workspace: workspace,
+ ),
+ trailing: workspace.workspaceId == currentWorkspace.workspaceId
+ ? const FlowySvg(
+ FlowySvgs.m_blue_check_s,
+ blendMode: null,
+ )
+ : null,
+ onTap: () => onWorkspaceSelected(workspace),
+ );
+ },
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart
index f87ef8645f..64e3e8824d 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_page.dart
@@ -4,7 +4,7 @@ import 'package:appflowy/mobile/presentation/notifications/widgets/mobile_notifi
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/notification_filter/notification_filter_bloc.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
-import 'package:appflowy/workspace/application/menu/sidebar_root_views_bloc.dart';
+import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/notifications/reminder_extension.dart';
import 'package:appflowy/workspace/presentation/notifications/widgets/inbox_action_bar.dart';
@@ -80,15 +80,15 @@ class _NotificationScreenContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
- create: (_) => SidebarRootViewsBloc()
+ create: (_) => SidebarSectionsBloc()
..add(
- SidebarRootViewsEvent.initial(
+ SidebarSectionsEvent.initial(
userProfile,
workspaceSetting.workspaceId,
),
),
- child: BlocBuilder(
- builder: (context, menuState) =>
+ child: BlocBuilder(
+ builder: (context, sectionState) =>
BlocBuilder(
builder: (context, filterState) =>
BlocBuilder(
@@ -122,7 +122,7 @@ class _NotificationScreenContent extends StatelessWidget {
NotificationsView(
shownReminders: pastReminders,
reminderBloc: reminderBloc,
- views: menuState.views,
+ views: sectionState.section.publicViews,
onAction: _onAction,
onDelete: _onDelete,
onReadChanged: _onReadChanged,
@@ -134,7 +134,7 @@ class _NotificationScreenContent extends StatelessWidget {
NotificationsView(
shownReminders: upcomingReminders,
reminderBloc: reminderBloc,
- views: menuState.views,
+ views: sectionState.section.publicViews,
isUpcoming: true,
onAction: _onAction,
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart
index 6f6c3adf49..44eba47bcb 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/page_item/mobile_view_item.dart
@@ -406,6 +406,10 @@ class _SingleMobileInnerViewItemState extends State {
ViewEvent.createView(
LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
layout,
+ section:
+ widget.categoryType != FolderCategoryType.favorite
+ ? widget.categoryType.toViewSectionPB
+ : null,
),
);
},
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart
index 66d25c58c6..337ce2549d 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/about/about_setting_group.dart
@@ -1,10 +1,12 @@
-import 'package:flutter/material.dart';
-
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/device_info_task.dart';
+import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/mobile_feature_flag_screen.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
import '../widgets/widgets.dart';
@@ -32,10 +34,20 @@ class AboutSettingGroup extends StatelessWidget {
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
),
+ if (kDebugMode)
+ MobileSettingItem(
+ name: 'Feature Flags',
+ trailing: const Icon(
+ Icons.chevron_right,
+ ),
+ onTap: () {
+ context.push(FeatureFlagScreen.routeName);
+ },
+ ),
MobileSettingItem(
name: LocaleKeys.settings_mobile_version.tr(),
trailing: FlowyText(
- '${DeviceOrApplicationInfoTask.applicationVersion} (${DeviceOrApplicationInfoTask.buildNumber})',
+ '${ApplicationInfo.applicationVersion} (${ApplicationInfo.buildNumber})',
color: Theme.of(context).colorScheme.onSurface,
),
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart
index 840306f34a..ebc58290b9 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart
@@ -5,7 +5,6 @@ import 'package:appflowy/workspace/application/settings/appflowy_cloud_urls_bloc
import 'package:appflowy_backend/log.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
-import 'package:go_router/go_router.dart';
class SelfHostUrlBottomSheet extends StatefulWidget {
const SelfHostUrlBottomSheet({
@@ -38,32 +37,9 @@ class _SelfHostUrlBottomSheetState extends State {
@override
Widget build(BuildContext context) {
- final theme = Theme.of(context);
-
return Column(
mainAxisSize: MainAxisSize.min,
children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- LocaleKeys.editor_urlHint.tr(),
- style: theme.textTheme.labelSmall,
- ),
- IconButton(
- icon: Icon(
- Icons.close,
- color: theme.hintColor,
- ),
- onPressed: () {
- context.pop();
- },
- ),
- ],
- ),
- const SizedBox(
- height: 16,
- ),
Form(
key: _formKey,
child: TextFormField(
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart
index 060dacedf0..095214d6ef 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/self_host_setting_group.dart
@@ -36,6 +36,14 @@ class _SelfHostSettingGroupState extends State {
onTap: () {
showMobileBottomSheet(
context,
+ showHeader: true,
+ title: LocaleKeys.editor_urlHint.tr(),
+ showCloseButton: true,
+ showDivider: false,
+ padding: const EdgeInsets.symmetric(
+ horizontal: 16.0,
+ vertical: 8.0,
+ ),
builder: (_) {
return SelfHostUrlBottomSheet(
url: url,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart
index cdb54ec122..6dc45c1c40 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/setting/widgets/mobile_setting_item_widget.dart
@@ -43,6 +43,7 @@ class MobileSettingItem extends StatelessWidget {
trailing: trailing,
onTap: onTap,
visualDensity: VisualDensity.compact,
+ contentPadding: const EdgeInsets.only(left: 8.0),
),
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart
index ceca40d019..4f76003e23 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/widgets/flowy_option_tile.dart
@@ -36,26 +36,31 @@ class FlowyOptionTile extends StatelessWidget {
this.content,
this.backgroundColor,
this.fontFamily,
+ this.height,
});
factory FlowyOptionTile.text({
- required String text,
+ String? text,
+ Widget? content,
Color? textColor,
bool showTopBorder = true,
bool showBottomBorder = true,
Widget? leftIcon,
Widget? trailing,
VoidCallback? onTap,
+ double? height,
}) {
return FlowyOptionTile._(
type: FlowyOptionTileType.text,
text: text,
+ content: content,
textColor: textColor,
onTap: onTap,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
leading: leftIcon,
trailing: trailing,
+ height: height,
);
}
@@ -174,6 +179,8 @@ class FlowyOptionTile extends StatelessWidget {
final Color? backgroundColor;
final String? fontFamily;
+ final double? height;
+
@override
Widget build(BuildContext context) {
final leadingWidget = _buildLeading();
@@ -182,16 +189,19 @@ class FlowyOptionTile extends StatelessWidget {
color: backgroundColor,
showTopBorder: showTopBorder,
showBottomBorder: showBottomBorder,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0),
- child: Row(
- children: [
- if (leadingWidget != null) leadingWidget,
- if (content != null) content!,
- if (content == null) _buildText(),
- if (content == null) _buildTextField(),
- if (trailing != null) trailing!,
- ],
+ child: SizedBox(
+ height: height,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Row(
+ children: [
+ if (leadingWidget != null) leadingWidget,
+ if (content != null) content!,
+ if (content == null) _buildText(),
+ if (content == null) _buildTextField(),
+ if (trailing != null) trailing!,
+ ],
+ ),
),
),
);
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart
index 2e56e1691a..39528a97c2 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/relation_cell_bloc.dart
@@ -1,10 +1,14 @@
import 'dart:async';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database/application/field/type_option/relation_type_option_cubit.dart';
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
+import 'package:appflowy/plugins/database/domain/field_service.dart';
+import 'package:appflowy/workspace/application/view/view_service.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:collection/collection.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -35,12 +39,14 @@ class RelationCellBloc extends Bloc {
(event, emit) async {
await event.when(
didUpdateCell: (RelationCellDataPB? cellData) async {
- if (cellData == null || cellData.rowIds.isEmpty) {
+ if (cellData == null ||
+ cellData.rowIds.isEmpty ||
+ state.relatedDatabaseMeta == null) {
emit(state.copyWith(rows: const []));
return;
}
final payload = RepeatedRowIdPB(
- databaseId: state.relatedDatabaseId,
+ databaseId: state.relatedDatabaseMeta!.databaseId,
rowIds: cellData.rowIds,
);
final result =
@@ -54,8 +60,16 @@ class RelationCellBloc extends Bloc {
);
emit(state.copyWith(rows: rows));
},
- didUpdateRelationDatabaseId: (databaseId) {
- emit(state.copyWith(relatedDatabaseId: databaseId));
+ didUpdateRelationTypeOption: (typeOption) async {
+ if (typeOption.databaseId.isEmpty) {
+ return;
+ }
+ final meta = await _loadDatabaseMeta(typeOption.databaseId);
+ emit(state.copyWith(relatedDatabaseMeta: meta));
+ _loadCellData();
+ },
+ selectDatabaseId: (databaseId) async {
+ await _updateTypeOption(databaseId);
},
selectRow: (rowId) async {
await _handleSelectRow(rowId);
@@ -73,29 +87,30 @@ class RelationCellBloc extends Bloc {
}
},
onCellFieldChanged: (field) {
- if (!isClosed) {
- // hack: SingleFieldListener receives notification before
- // FieldController's copy is updated.
- Future.delayed(const Duration(milliseconds: 50), () {
+ // hack: SingleFieldListener receives notification before
+ // FieldController's copy is updated.
+ Future.delayed(const Duration(milliseconds: 50), () {
+ if (!isClosed) {
final RelationTypeOptionPB typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser());
- add(
- RelationCellEvent.didUpdateRelationDatabaseId(
- typeOption.databaseId,
- ),
- );
- });
- }
+ add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
+ }
+ });
},
);
}
void _init() {
- final RelationTypeOptionPB typeOption =
+ final typeOption =
cellController.getTypeOption(RelationTypeOptionDataParser());
- add(RelationCellEvent.didUpdateRelationDatabaseId(typeOption.databaseId));
+ add(RelationCellEvent.didUpdateRelationTypeOption(typeOption));
+ }
+
+ void _loadCellData() {
final cellData = cellController.getCellData();
- add(RelationCellEvent.didUpdateCell(cellData));
+ if (!isClosed) {
+ add(RelationCellEvent.didUpdateCell(cellData));
+ }
}
Future _handleSelectRow(String rowId) async {
@@ -115,25 +130,66 @@ class RelationCellBloc extends Bloc {
final result = await DatabaseEventUpdateRelationCell(payload).send();
result.fold((l) => null, (err) => Log.error(err));
}
+
+ Future _loadDatabaseMeta(String databaseId) async {
+ final getDatabaseResult = await DatabaseEventGetDatabases().send();
+ final databaseMeta = getDatabaseResult.fold(
+ (s) => s.items.firstWhereOrNull(
+ (metaPB) => metaPB.databaseId == databaseId,
+ ),
+ (f) => null,
+ );
+ if (databaseMeta != null) {
+ final result =
+ await ViewBackendService.getView(databaseMeta.inlineViewId);
+ return result.fold(
+ (s) => DatabaseMeta(
+ databaseId: databaseId,
+ inlineViewId: databaseMeta.inlineViewId,
+ databaseName: s.name,
+ ),
+ (f) => null,
+ );
+ }
+ return null;
+ }
+
+ Future _updateTypeOption(String databaseId) async {
+ final newDateTypeOption = RelationTypeOptionPB(
+ databaseId: databaseId,
+ );
+
+ final result = await FieldBackendService.updateFieldTypeOption(
+ viewId: cellController.viewId,
+ fieldId: cellController.fieldInfo.id,
+ typeOptionData: newDateTypeOption.writeToBuffer(),
+ );
+ result.fold((s) => null, (err) => Log.error(err));
+ }
}
@freezed
class RelationCellEvent with _$RelationCellEvent {
- const factory RelationCellEvent.didUpdateRelationDatabaseId(
- String databaseId,
- ) = _DidUpdateRelationDatabaseId;
+ const factory RelationCellEvent.didUpdateRelationTypeOption(
+ RelationTypeOptionPB typeOption,
+ ) = _DidUpdateRelationTypeOption;
const factory RelationCellEvent.didUpdateCell(RelationCellDataPB? data) =
_DidUpdateCell;
+ const factory RelationCellEvent.selectDatabaseId(
+ String databaseId,
+ ) = _SelectDatabaseId;
const factory RelationCellEvent.selectRow(String rowId) = _SelectRowId;
}
@freezed
class RelationCellState with _$RelationCellState {
const factory RelationCellState({
- required String relatedDatabaseId,
+ required DatabaseMeta? relatedDatabaseMeta,
required List rows,
}) = _RelationCellState;
- factory RelationCellState.initial() =>
- const RelationCellState(relatedDatabaseId: "", rows: []);
+ factory RelationCellState.initial() => const RelationCellState(
+ relatedDatabaseMeta: null,
+ rows: [],
+ );
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart
new file mode 100644
index 0000000000..8e9e068920
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_cell_editor_bloc.dart
@@ -0,0 +1,433 @@
+import 'dart:async';
+
+import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
+import 'package:appflowy/plugins/database/application/field/type_option/select_type_option_actions.dart';
+import 'package:appflowy/plugins/database/domain/field_service.dart';
+import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'select_option_cell_editor_bloc.freezed.dart';
+
+const String createSelectOptionSuggestionId =
+ "create_select_option_suggestion_id";
+
+class SelectOptionCellEditorBloc
+ extends Bloc {
+ SelectOptionCellEditorBloc({required this.cellController})
+ : _selectOptionService = SelectOptionCellBackendService(
+ viewId: cellController.viewId,
+ fieldId: cellController.fieldId,
+ rowId: cellController.rowId,
+ ),
+ _typeOptionAction = cellController.fieldType == FieldType.SingleSelect
+ ? SingleSelectAction(
+ viewId: cellController.viewId,
+ fieldId: cellController.fieldId,
+ onTypeOptionUpdated: (typeOptionData) =>
+ FieldBackendService.updateFieldTypeOption(
+ viewId: cellController.viewId,
+ fieldId: cellController.fieldId,
+ typeOptionData: typeOptionData,
+ ),
+ )
+ : MultiSelectAction(
+ viewId: cellController.viewId,
+ fieldId: cellController.fieldId,
+ onTypeOptionUpdated: (typeOptionData) =>
+ FieldBackendService.updateFieldTypeOption(
+ viewId: cellController.viewId,
+ fieldId: cellController.fieldId,
+ typeOptionData: typeOptionData,
+ ),
+ ),
+ super(SelectOptionCellEditorState.initial(cellController)) {
+ _dispatch();
+ _startListening();
+ _loadOptions();
+ }
+
+ final SelectOptionCellBackendService _selectOptionService;
+ final ISelectOptionAction _typeOptionAction;
+ final SelectOptionCellController cellController;
+
+ VoidCallback? _onCellChangedFn;
+
+ final List allOptions = [];
+ String filter = "";
+
+ void _dispatch() {
+ on(
+ (event, emit) async {
+ await event.when(
+ didReceiveOptions: (options, selectedOptions) {
+ final result = _getVisibleOptions(options);
+ allOptions
+ ..clear()
+ ..addAll(options);
+ emit(
+ state.copyWith(
+ options: result.options,
+ createSelectOptionSuggestion:
+ result.createSelectOptionSuggestion,
+ selectedOptions: selectedOptions,
+ ),
+ );
+ },
+ createOption: () async {
+ if (state.createSelectOptionSuggestion == null) {
+ return;
+ }
+ filter = "";
+ await _createOption(
+ name: state.createSelectOptionSuggestion!.name,
+ color: state.createSelectOptionSuggestion!.color,
+ );
+ emit(state.copyWith(clearFilter: true));
+ },
+ deleteOption: (option) async {
+ await _deleteOption([option]);
+ },
+ deleteAllOptions: () async {
+ if (allOptions.isNotEmpty) {
+ await _deleteOption(allOptions);
+ }
+ },
+ updateOption: (option) async {
+ await _updateOption(option);
+ },
+ selectOption: (optionId) async {
+ await _selectOptionService.select(optionIds: [optionId]);
+ },
+ unSelectOption: (optionId) async {
+ await _selectOptionService.unSelect(optionIds: [optionId]);
+ },
+ unSelectLastOption: () async {
+ if (state.selectedOptions.isEmpty) {
+ return;
+ }
+ final lastSelectedOptionId = state.selectedOptions.last.id;
+ await _selectOptionService
+ .unSelect(optionIds: [lastSelectedOptionId]);
+ },
+ submitTextField: () {
+ _submitTextFieldValue(emit);
+ },
+ selectMultipleOptions: (optionNames, remainder) {
+ if (optionNames.isNotEmpty) {
+ _selectMultipleOptions(optionNames);
+ }
+ _filterOption(remainder, emit);
+ },
+ reorderOption: (fromOptionId, toOptionId) {
+ final options = _typeOptionAction.reorderOption(
+ allOptions,
+ fromOptionId,
+ toOptionId,
+ );
+ allOptions
+ ..clear()
+ ..addAll(options);
+ final result = _getVisibleOptions(options);
+ emit(state.copyWith(options: result.options));
+ },
+ filterOption: (filterText) {
+ _filterOption(filterText, emit);
+ },
+ focusPreviousOption: () {
+ _focusOption(true, emit);
+ },
+ focusNextOption: () {
+ _focusOption(false, emit);
+ },
+ updateFocusedOption: (optionId) {
+ emit(state.copyWith(focusedOptionId: optionId));
+ },
+ resetClearFilterFlag: () {
+ emit(state.copyWith(clearFilter: false));
+ },
+ );
+ },
+ );
+ }
+
+ @override
+ Future close() async {
+ if (_onCellChangedFn != null) {
+ cellController.removeListener(_onCellChangedFn!);
+ _onCellChangedFn = null;
+ }
+ return super.close();
+ }
+
+ Future _createOption({
+ required String name,
+ required SelectOptionColorPB color,
+ }) async {
+ final result = await _selectOptionService.create(
+ name: name,
+ color: color,
+ );
+ result.fold((l) => {}, (err) => Log.error(err));
+ }
+
+ Future _deleteOption(List options) async {
+ final result = await _selectOptionService.delete(options: options);
+ result.fold((l) => null, (err) => Log.error(err));
+ }
+
+ Future _updateOption(SelectOptionPB option) async {
+ final result = await _selectOptionService.update(
+ option: option,
+ );
+
+ result.fold((l) => null, (err) => Log.error(err));
+ }
+
+ void _submitTextFieldValue(Emitter emit) {
+ if (state.focusedOptionId == null) {
+ return;
+ }
+
+ final focusedOptionId = state.focusedOptionId!;
+
+ if (focusedOptionId == createSelectOptionSuggestionId) {
+ filter = "";
+ _createOption(
+ name: state.createSelectOptionSuggestion!.name,
+ color: state.createSelectOptionSuggestion!.color,
+ );
+ emit(
+ state.copyWith(
+ createSelectOptionSuggestion: null,
+ clearFilter: true,
+ ),
+ );
+ } else if (!state.selectedOptions
+ .any((option) => option.id == focusedOptionId)) {
+ _selectOptionService.select(optionIds: [focusedOptionId]);
+ }
+ }
+
+ void _selectMultipleOptions(List optionNames) {
+ final optionIds = optionNames
+ .map(
+ (name) => allOptions.firstWhereOrNull(
+ (option) => option.name.toLowerCase() == name.toLowerCase(),
+ ),
+ )
+ .nonNulls
+ .map((option) => option.id)
+ .toList();
+
+ _selectOptionService.select(optionIds: optionIds);
+ }
+
+ void _filterOption(
+ String filterText,
+ Emitter emit,
+ ) {
+ filter = filterText;
+ final _MakeOptionResult result = _getVisibleOptions(
+ allOptions,
+ );
+ final focusedOptionId = result.options.isEmpty
+ ? result.createSelectOptionSuggestion == null
+ ? null
+ : createSelectOptionSuggestionId
+ : result.options.any((option) => option.id == state.focusedOptionId)
+ ? state.focusedOptionId
+ : result.options.first.id;
+ emit(
+ state.copyWith(
+ options: result.options,
+ createSelectOptionSuggestion: result.createSelectOptionSuggestion,
+ focusedOptionId: focusedOptionId,
+ ),
+ );
+ }
+
+ Future _loadOptions() async {
+ final result = await _selectOptionService.getCellData();
+ if (isClosed) {
+ Log.warn("Unexpecteded closing the bloc");
+ return;
+ }
+
+ return result.fold(
+ (data) => add(
+ SelectOptionCellEditorEvent.didReceiveOptions(
+ data.options,
+ data.selectOptions,
+ ),
+ ),
+ (err) {
+ Log.error(err);
+ return null;
+ },
+ );
+ }
+
+ _MakeOptionResult _getVisibleOptions(
+ List allOptions,
+ ) {
+ final List options = List.from(allOptions);
+ String newOptionName = filter;
+
+ if (filter.isNotEmpty) {
+ options.retainWhere((option) {
+ final name = option.name.toLowerCase();
+ final lFilter = filter.toLowerCase();
+
+ if (name == lFilter) {
+ newOptionName = "";
+ }
+
+ return name.contains(lFilter);
+ });
+ }
+
+ return _MakeOptionResult(
+ options: options,
+ createSelectOptionSuggestion: newOptionName.isEmpty
+ ? null
+ : CreateSelectOptionSuggestion(
+ name: newOptionName,
+ color: newSelectOptionColor(allOptions),
+ ),
+ );
+ }
+
+ void _focusOption(bool previous, Emitter emit) {
+ if (state.options.isEmpty && state.createSelectOptionSuggestion == null) {
+ return;
+ }
+
+ final optionIds = [
+ ...state.options.map((e) => e.id),
+ if (state.createSelectOptionSuggestion != null)
+ createSelectOptionSuggestionId,
+ ];
+
+ if (state.focusedOptionId == null) {
+ emit(
+ state.copyWith(
+ focusedOptionId: previous ? optionIds.last : optionIds.first,
+ ),
+ );
+ return;
+ }
+
+ final currentIndex =
+ optionIds.indexWhere((id) => id == state.focusedOptionId);
+
+ final newIndex = currentIndex == -1
+ ? 0
+ : (currentIndex + (previous ? -1 : 1)) % optionIds.length;
+
+ emit(state.copyWith(focusedOptionId: optionIds[newIndex]));
+ }
+
+ void _startListening() {
+ _onCellChangedFn = cellController.addListener(
+ onCellChanged: (selectOptionContext) {
+ _loadOptions();
+ },
+ onCellFieldChanged: (field) {
+ _loadOptions();
+ },
+ );
+ }
+}
+
+@freezed
+class SelectOptionCellEditorEvent with _$SelectOptionCellEditorEvent {
+ const factory SelectOptionCellEditorEvent.didReceiveOptions(
+ List options,
+ List selectedOptions,
+ ) = _DidReceiveOptions;
+ const factory SelectOptionCellEditorEvent.createOption() = _CreateOption;
+ const factory SelectOptionCellEditorEvent.selectOption(String optionId) =
+ _SelectOption;
+ const factory SelectOptionCellEditorEvent.unSelectOption(String optionId) =
+ _UnSelectOption;
+ const factory SelectOptionCellEditorEvent.unSelectLastOption() =
+ _UnSelectLastOption;
+ const factory SelectOptionCellEditorEvent.updateOption(
+ SelectOptionPB option,
+ ) = _UpdateOption;
+ const factory SelectOptionCellEditorEvent.deleteOption(
+ SelectOptionPB option,
+ ) = _DeleteOption;
+ const factory SelectOptionCellEditorEvent.deleteAllOptions() =
+ _DeleteAllOptions;
+ const factory SelectOptionCellEditorEvent.reorderOption(
+ String fromOptionId,
+ String toOptionId,
+ ) = _ReorderOption;
+ const factory SelectOptionCellEditorEvent.filterOption(String filterText) =
+ _SelectOptionFilter;
+ const factory SelectOptionCellEditorEvent.submitTextField() =
+ _SubmitTextField;
+ const factory SelectOptionCellEditorEvent.selectMultipleOptions(
+ List optionNames,
+ String remainder,
+ ) = _SelectMultipleOptions;
+ const factory SelectOptionCellEditorEvent.focusPreviousOption() =
+ _FocusPreviousOption;
+ const factory SelectOptionCellEditorEvent.focusNextOption() =
+ _FocusNextOption;
+ const factory SelectOptionCellEditorEvent.updateFocusedOption(
+ String? optionId,
+ ) = _UpdateFocusedOption;
+ const factory SelectOptionCellEditorEvent.resetClearFilterFlag() =
+ _ResetClearFilterFlag;
+}
+
+@freezed
+class SelectOptionCellEditorState with _$SelectOptionCellEditorState {
+ const factory SelectOptionCellEditorState({
+ required List options,
+ required List selectedOptions,
+ required CreateSelectOptionSuggestion? createSelectOptionSuggestion,
+ required String? focusedOptionId,
+ required bool clearFilter,
+ }) = _SelectOptionEditorState;
+
+ factory SelectOptionCellEditorState.initial(
+ SelectOptionCellController context,
+ ) {
+ final data = context.getCellData(loadIfNotExist: false);
+ return SelectOptionCellEditorState(
+ options: data?.options ?? [],
+ selectedOptions: data?.selectOptions ?? [],
+ createSelectOptionSuggestion: null,
+ focusedOptionId: null,
+ clearFilter: false,
+ );
+ }
+}
+
+class _MakeOptionResult {
+ _MakeOptionResult({
+ required this.options,
+ required this.createSelectOptionSuggestion,
+ });
+
+ List options;
+ CreateSelectOptionSuggestion? createSelectOptionSuggestion;
+}
+
+class CreateSelectOptionSuggestion {
+ CreateSelectOptionSuggestion({
+ required this.name,
+ required this.color,
+ });
+
+ final String name;
+ final SelectOptionColorPB color;
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_editor_bloc.dart
deleted file mode 100644
index 224d916719..0000000000
--- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/select_option_editor_bloc.dart
+++ /dev/null
@@ -1,318 +0,0 @@
-import 'dart:async';
-
-import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
-import 'package:appflowy/plugins/database/domain/select_option_cell_service.dart';
-import 'package:appflowy_backend/log.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
-import 'package:flutter/widgets.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import 'package:freezed_annotation/freezed_annotation.dart';
-
-part 'select_option_editor_bloc.freezed.dart';
-
-class SelectOptionCellEditorBloc
- extends Bloc {
- SelectOptionCellEditorBloc({required this.cellController})
- : _selectOptionService = SelectOptionCellBackendService(
- viewId: cellController.viewId,
- fieldId: cellController.fieldId,
- rowId: cellController.rowId,
- ),
- super(SelectOptionEditorState.initial(cellController)) {
- _dispatch();
- }
-
- final SelectOptionCellBackendService _selectOptionService;
- final SelectOptionCellController cellController;
-
- VoidCallback? _onCellChangedFn;
-
- void _dispatch() {
- on(
- (event, emit) async {
- await event.when(
- initial: () async {
- _startListening();
- await _loadOptions();
- },
- didReceiveOptions: (options, selectedOptions) {
- final result = _makeOptions(state.filter, options);
- emit(
- state.copyWith(
- allOptions: options,
- options: result.options,
- createOption: result.createOption,
- selectedOptions: selectedOptions,
- ),
- );
- },
- newOption: (optionName) async {
- await _createOption(optionName);
- emit(
- state.copyWith(
- filter: null,
- ),
- );
- },
- deleteOption: (option) async {
- await _deleteOption([option]);
- },
- deleteAllOptions: () async {
- if (state.allOptions.isNotEmpty) {
- await _deleteOption(state.allOptions);
- }
- },
- updateOption: (option) async {
- await _updateOption(option);
- },
- selectOption: (optionId) async {
- await _selectOptionService.select(optionIds: [optionId]);
- final selectedOption = [
- ...state.selectedOptions,
- state.options.firstWhere(
- (element) => element.id == optionId,
- ),
- ];
- emit(
- state.copyWith(
- selectedOptions: selectedOption,
- ),
- );
- },
- unSelectOption: (optionId) async {
- await _selectOptionService.unSelect(optionIds: [optionId]);
- final selectedOptions = [...state.selectedOptions]
- ..removeWhere((e) => e.id == optionId);
- emit(
- state.copyWith(
- selectedOptions: selectedOptions,
- ),
- );
- },
- trySelectOption: (optionName) {
- _trySelectOption(optionName, emit);
- },
- selectMultipleOptions: (optionNames, remainder) {
- if (optionNames.isNotEmpty) {
- _selectMultipleOptions(optionNames);
- }
- _filterOption(remainder, emit);
- },
- filterOption: (optionName) {
- _filterOption(optionName, emit);
- },
- );
- },
- );
- }
-
- @override
- Future close() async {
- if (_onCellChangedFn != null) {
- cellController.removeListener(_onCellChangedFn!);
- _onCellChangedFn = null;
- }
- return super.close();
- }
-
- Future _createOption(String name) async {
- final result = await _selectOptionService.create(name: name);
- result.fold((l) => {}, (err) => Log.error(err));
- }
-
- Future _deleteOption(List options) async {
- final result = await _selectOptionService.delete(options: options);
- result.fold((l) => null, (err) => Log.error(err));
- }
-
- Future _updateOption(SelectOptionPB option) async {
- final result = await _selectOptionService.update(
- option: option,
- );
-
- result.fold((l) => null, (err) => Log.error(err));
- }
-
- void _trySelectOption(
- String optionName,
- Emitter emit,
- ) {
- SelectOptionPB? matchingOption;
- bool optionExistsButSelected = false;
-
- for (final option in state.options) {
- if (option.name.toLowerCase() == optionName.toLowerCase()) {
- if (!state.selectedOptions.contains(option)) {
- matchingOption = option;
- break;
- } else {
- optionExistsButSelected = true;
- }
- }
- }
-
- // if there isn't a matching option at all, then create it
- if (matchingOption == null && !optionExistsButSelected) {
- _createOption(optionName);
- }
-
- // if there is an unselected matching option, select it
- if (matchingOption != null) {
- _selectOptionService.select(optionIds: [matchingOption.id]);
- }
-
- // clear the filter
- emit(state.copyWith(filter: null));
- }
-
- void _selectMultipleOptions(List optionNames) {
- // The options are unordered. So in order to keep the inserted [optionNames]
- // order, it needs to get the option id in the [optionNames] order.
- final lowerCaseNames = optionNames.map((e) => e.toLowerCase());
- final Map optionIdsMap = {};
- for (final option in state.options) {
- optionIdsMap[option.name.toLowerCase()] = option.id;
- }
-
- final optionIds = lowerCaseNames
- .where((name) => optionIdsMap[name] != null)
- .map((name) => optionIdsMap[name]!)
- .toList();
-
- _selectOptionService.select(optionIds: optionIds);
- }
-
- void _filterOption(String optionName, Emitter emit) {
- final _MakeOptionResult result = _makeOptions(
- optionName,
- state.allOptions,
- );
- emit(
- state.copyWith(
- filter: optionName,
- options: result.options,
- createOption: result.createOption,
- ),
- );
- }
-
- Future _loadOptions() async {
- final result = await _selectOptionService.getCellData();
- if (isClosed) {
- Log.warn("Unexpecteded closing the bloc");
- return;
- }
-
- return result.fold(
- (data) => add(
- SelectOptionEditorEvent.didReceiveOptions(
- data.options,
- data.selectOptions,
- ),
- ),
- (err) {
- Log.error(err);
- return null;
- },
- );
- }
-
- _MakeOptionResult _makeOptions(
- String? filter,
- List allOptions,
- ) {
- final List options = List.from(allOptions);
- String? createOption = filter;
-
- if (filter != null && filter.isNotEmpty) {
- options.retainWhere((option) {
- final name = option.name.toLowerCase();
- final lFilter = filter.toLowerCase();
-
- if (name == lFilter) {
- createOption = null;
- }
-
- return name.contains(lFilter);
- });
- } else {
- createOption = null;
- }
-
- return _MakeOptionResult(
- options: options,
- createOption: createOption,
- );
- }
-
- void _startListening() {
- _onCellChangedFn = cellController.addListener(
- onCellChanged: (selectOptionContext) {
- _loadOptions();
- },
- onCellFieldChanged: (field) {
- _loadOptions();
- },
- );
- }
-}
-
-@freezed
-class SelectOptionEditorEvent with _$SelectOptionEditorEvent {
- const factory SelectOptionEditorEvent.initial() = _Initial;
- const factory SelectOptionEditorEvent.didReceiveOptions(
- List options,
- List selectedOptions,
- ) = _DidReceiveOptions;
- const factory SelectOptionEditorEvent.newOption(String optionName) =
- _NewOption;
- const factory SelectOptionEditorEvent.selectOption(String optionId) =
- _SelectOption;
- const factory SelectOptionEditorEvent.unSelectOption(String optionId) =
- _UnSelectOption;
- const factory SelectOptionEditorEvent.updateOption(SelectOptionPB option) =
- _UpdateOption;
- const factory SelectOptionEditorEvent.deleteOption(SelectOptionPB option) =
- _DeleteOption;
- const factory SelectOptionEditorEvent.deleteAllOptions() = _DeleteAllOptions;
- const factory SelectOptionEditorEvent.filterOption(String optionName) =
- _SelectOptionFilter;
- const factory SelectOptionEditorEvent.trySelectOption(String optionName) =
- _TrySelectOption;
- const factory SelectOptionEditorEvent.selectMultipleOptions(
- List optionNames,
- String remainder,
- ) = _SelectMultipleOptions;
-}
-
-@freezed
-class SelectOptionEditorState with _$SelectOptionEditorState {
- const factory SelectOptionEditorState({
- required List options,
- required List allOptions,
- required List selectedOptions,
- required String? createOption,
- required String? filter,
- }) = _SelectOptionEditorState;
-
- factory SelectOptionEditorState.initial(SelectOptionCellController context) {
- final data = context.getCellData(loadIfNotExist: false);
- return SelectOptionEditorState(
- options: data?.options ?? [],
- allOptions: data?.options ?? [],
- selectedOptions: data?.selectOptions ?? [],
- createOption: null,
- filter: null,
- );
- }
-}
-
-class _MakeOptionResult {
- _MakeOptionResult({
- required this.options,
- required this.createOption,
- });
-
- List options;
- String? createOption;
-}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart
index dbd2258cc1..fe52a73aac 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/cell/bloc/url_cell_bloc.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io';
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/url_entities.pb.dart';
@@ -29,15 +30,17 @@ class URLCellBloc extends Bloc {
void _dispatch() {
on(
(event, emit) async {
- event.when(
+ await event.when(
initial: () {
_startListening();
},
- didReceiveCellUpdate: (cellData) {
+ didReceiveCellUpdate: (cellData) async {
+ final content = cellData?.content ?? "";
+ final isValid = await isUrlValid(content);
emit(
state.copyWith(
- content: cellData?.content ?? "",
- url: cellData?.url ?? "",
+ content: content,
+ isValid: isValid,
),
);
},
@@ -58,6 +61,31 @@ class URLCellBloc extends Bloc {
},
);
}
+
+ Future isUrlValid(String content) async {
+ if (content.isEmpty) {
+ return true;
+ }
+
+ try {
+ // check protocol is provided
+ const linkPrefix = [
+ 'http://',
+ 'https://',
+ ];
+ final shouldAddScheme =
+ !linkPrefix.any((pattern) => content.startsWith(pattern));
+ final url = shouldAddScheme ? 'http://$content' : content;
+
+ // get hostname and check validity
+ final uri = Uri.parse(url);
+ final hostName = uri.host;
+ await InternetAddress.lookup(hostName);
+ } catch (_) {
+ return false;
+ }
+ return true;
+ }
}
@freezed
@@ -72,14 +100,14 @@ class URLCellEvent with _$URLCellEvent {
class URLCellState with _$URLCellState {
const factory URLCellState({
required String content,
- required String url,
+ required bool isValid,
}) = _URLCellState;
factory URLCellState.initial(URLCellController context) {
final cellData = context.getCellData();
return URLCellState(
content: cellData?.content ?? "",
- url: cellData?.url ?? "",
+ isValid: true,
);
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart
index 5beb2bba88..db1a56071e 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_controller.dart
@@ -162,75 +162,6 @@ class FieldController {
/// Listen for filter changes in the backend.
void _listenOnFilterChanges() {
- void deleteFilterFromChangeset(
- List filters,
- FilterChangesetNotificationPB changeset,
- ) {
- final deleteFilterIds = changeset.deleteFilters.map((e) => e.id).toList();
- if (deleteFilterIds.isNotEmpty) {
- filters.retainWhere(
- (element) => !deleteFilterIds.contains(element.filter.id),
- );
- }
- }
-
- void insertFilterFromChangeset(
- List filters,
- FilterChangesetNotificationPB changeset,
- ) {
- for (final newFilter in changeset.insertFilters) {
- final filterIndex =
- filters.indexWhere((element) => element.filter.id == newFilter.id);
- if (filterIndex == -1) {
- final fieldInfo = _findFieldInfo(
- fieldInfos: fieldInfos,
- fieldId: newFilter.fieldId,
- fieldType: newFilter.fieldType,
- );
- if (fieldInfo != null) {
- filters.add(FilterInfo(viewId, newFilter, fieldInfo));
- }
- }
- }
- }
-
- void updateFilterFromChangeset(
- List filters,
- FilterChangesetNotificationPB changeset,
- ) {
- for (final updatedFilter in changeset.updateFilters) {
- final filterIndex = filters.indexWhere(
- (element) => element.filter.id == updatedFilter.filterId,
- );
- // Remove the old filter
- if (filterIndex != -1) {
- filters.removeAt(filterIndex);
- }
-
- // Insert the filter if there is a filter and its field info is
- // not null
- if (updatedFilter.hasFilter()) {
- final fieldInfo = _findFieldInfo(
- fieldInfos: fieldInfos,
- fieldId: updatedFilter.filter.fieldId,
- fieldType: updatedFilter.filter.fieldType,
- );
-
- if (fieldInfo != null) {
- // Insert the filter with the position: filterIndex, otherwise,
- // append it to the end of the list.
- final filterInfo =
- FilterInfo(viewId, updatedFilter.filter, fieldInfo);
- if (filterIndex != -1) {
- filters.insert(filterIndex, filterInfo);
- } else {
- filters.add(filterInfo);
- }
- }
- }
- }
- }
-
_filtersListener.start(
onFilterChanged: (result) {
if (_isDisposed) {
@@ -239,15 +170,19 @@ class FieldController {
result.fold(
(FilterChangesetNotificationPB changeset) {
- final List filters = filterInfos;
- // delete removed filters
- deleteFilterFromChangeset(filters, changeset);
+ final List filters = [];
+ for (final filter in changeset.filters.items) {
+ final fieldInfo = _findFieldInfo(
+ fieldInfos: fieldInfos,
+ fieldId: filter.data.fieldId,
+ fieldType: filter.data.fieldType,
+ );
- // insert new filters
- insertFilterFromChangeset(filters, changeset);
-
- // edit modified filters
- updateFilterFromChangeset(filters, changeset);
+ if (fieldInfo != null) {
+ final filterInfo = FilterInfo(viewId, filter, fieldInfo);
+ filters.add(filterInfo);
+ }
+ }
_filterNotifier?.filters = filters;
_updateFieldInfos();
@@ -665,8 +600,8 @@ class FieldController {
FilterInfo? getFilterInfo(FilterPB filterPB) {
final fieldInfo = _findFieldInfo(
fieldInfos: fieldInfos,
- fieldId: filterPB.fieldId,
- fieldType: filterPB.fieldType,
+ fieldId: filterPB.data.fieldId,
+ fieldType: filterPB.data.fieldType,
);
return fieldInfo != null ? FilterInfo(viewId, filterPB, fieldInfo) : null;
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart
index 64d5a398be..1612ab6a23 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/field_info.dart
@@ -47,7 +47,7 @@ class FieldInfo with _$FieldInfo {
}
bool get canCreateFilter {
- if (hasFilter) {
+ if (isGroupField) {
return false;
}
@@ -58,6 +58,7 @@ class FieldInfo with _$FieldInfo {
case FieldType.RichText:
case FieldType.SingleSelect:
case FieldType.Checklist:
+ case FieldType.URL:
return true;
default:
return false;
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart
new file mode 100644
index 0000000000..df8e0d46fb
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/relation_type_option_cubit.dart
@@ -0,0 +1,63 @@
+import 'package:appflowy/workspace/application/view/view_service.dart';
+import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:bloc/bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'relation_type_option_cubit.freezed.dart';
+
+class RelationDatabaseListCubit extends Cubit {
+ RelationDatabaseListCubit() : super(RelationDatabaseListState.initial()) {
+ _loadDatabaseMetas();
+ }
+
+ void _loadDatabaseMetas() async {
+ final getDatabaseResult = await DatabaseEventGetDatabases().send();
+ final metaPBs = getDatabaseResult.fold>(
+ (s) => s.items,
+ (f) => [],
+ );
+ final futures = metaPBs.map((meta) {
+ return ViewBackendService.getView(meta.inlineViewId).then(
+ (result) => result.fold(
+ (s) => DatabaseMeta(
+ databaseId: meta.databaseId,
+ inlineViewId: meta.inlineViewId,
+ databaseName: s.name,
+ ),
+ (f) => null,
+ ),
+ );
+ });
+ final databaseMetas = await Future.wait(futures);
+ emit(
+ RelationDatabaseListState(
+ databaseMetas: databaseMetas.nonNulls.toList(),
+ ),
+ );
+ }
+}
+
+@freezed
+class DatabaseMeta with _$DatabaseMeta {
+ factory DatabaseMeta({
+ /// id of the database
+ required String databaseId,
+
+ /// id of the inline view
+ required String inlineViewId,
+
+ /// name of the database, currently identical to the name of the inline view
+ required String databaseName,
+ }) = _DatabaseMeta;
+}
+
+@freezed
+class RelationDatabaseListState with _$RelationDatabaseListState {
+ factory RelationDatabaseListState({
+ required List databaseMetas,
+ }) = _RelationDatabaseListState;
+
+ factory RelationDatabaseListState.initial() =>
+ RelationDatabaseListState(databaseMetas: []);
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart
index cd1db30fc6..72f49dd084 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_option_type_option_bloc.dart
@@ -20,10 +20,10 @@ class SelectOptionTypeOptionBloc
void _dispatch() {
on(
(event, emit) async {
- await event.when(
- createOption: (optionName) async {
+ event.when(
+ createOption: (optionName) {
final List options =
- await typeOptionAction.insertOption(state.options, optionName);
+ typeOptionAction.insertOption(state.options, optionName);
emit(state.copyWith(options: options));
},
addingOption: () {
@@ -33,15 +33,23 @@ class SelectOptionTypeOptionBloc
emit(state.copyWith(isEditingOption: false, newOptionName: null));
},
updateOption: (option) {
- final List options =
+ final options =
typeOptionAction.updateOption(state.options, option);
emit(state.copyWith(options: options));
},
deleteOption: (option) {
- final List options =
+ final options =
typeOptionAction.deleteOption(state.options, option);
emit(state.copyWith(options: options));
},
+ reorderOption: (fromOptionId, toOptionId) {
+ final options = typeOptionAction.reorderOption(
+ state.options,
+ fromOptionId,
+ toOptionId,
+ );
+ emit(state.copyWith(options: options));
+ },
);
},
);
@@ -61,6 +69,10 @@ class SelectOptionTypeOptionEvent with _$SelectOptionTypeOptionEvent {
const factory SelectOptionTypeOptionEvent.deleteOption(
SelectOptionPB option,
) = _DeleteOption;
+ const factory SelectOptionTypeOptionEvent.reorderOption(
+ String fromOptionId,
+ String toOptionId,
+ ) = _ReorderOption;
}
@freezed
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart
index 8d46b994bb..235bdb60eb 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/field/type_option/select_type_option_actions.dart
@@ -1,9 +1,7 @@
-import 'dart:async';
-
import 'package:appflowy/plugins/database/domain/type_option_service.dart';
-import 'package:appflowy/plugins/database/grid/presentation/widgets/header/type_option/builder.dart';
-import 'package:appflowy_backend/log.dart';
+import 'package:appflowy/plugins/database/widgets/field/type_option_editor/builder.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
+import 'package:nanoid/nanoid.dart';
abstract class ISelectOptionAction {
ISelectOptionAction({
@@ -20,29 +18,25 @@ abstract class ISelectOptionAction {
onTypeOptionUpdated(newTypeOption.writeToBuffer());
}
- Future> insertOption(
+ List insertOption(
List options,
String optionName,
) {
- final newOptions = List.from(options);
- return service.newOption(name: optionName).then((result) {
- return result.fold(
- (option) {
- final exists =
- newOptions.any((element) => element.name == option.name);
- if (!exists) {
- newOptions.insert(0, option);
- }
+ if (options.any((element) => element.name == optionName)) {
+ return options;
+ }
- updateTypeOption(newOptions);
- return newOptions;
- },
- (err) {
- Log.error(err);
- return newOptions;
- },
- );
- });
+ final newOptions = List.from(options);
+
+ final newSelectOption = SelectOptionPB()
+ ..id = nanoid(4)
+ ..color = newSelectOptionColor(options)
+ ..name = optionName;
+
+ newOptions.insert(0, newSelectOption);
+
+ updateTypeOption(newOptions);
+ return newOptions;
}
List deleteOption(
@@ -73,6 +67,25 @@ abstract class ISelectOptionAction {
updateTypeOption(newOptions);
return newOptions;
}
+
+ List reorderOption(
+ List options,
+ String fromOptionId,
+ String toOptionId,
+ ) {
+ final newOptions = List.from(options);
+ final fromIndex =
+ newOptions.indexWhere((element) => element.id == fromOptionId);
+ final toIndex =
+ newOptions.indexWhere((element) => element.id == toOptionId);
+
+ if (fromIndex != -1 && toIndex != -1) {
+ newOptions.insert(toIndex, newOptions.removeAt(fromIndex));
+ }
+
+ updateTypeOption(newOptions);
+ return newOptions;
+ }
}
class MultiSelectAction extends ISelectOptionAction {
@@ -102,3 +115,19 @@ class SingleSelectAction extends ISelectOptionAction {
onTypeOptionUpdated(newTypeOption.writeToBuffer());
}
}
+
+SelectOptionColorPB newSelectOptionColor(List options) {
+ final colorFrequency = List.filled(SelectOptionColorPB.values.length, 0);
+
+ for (final option in options) {
+ colorFrequency[option.color.value]++;
+ }
+
+ final minIndex = colorFrequency
+ .asMap()
+ .entries
+ .reduce((a, b) => a.value <= b.value ? a : b)
+ .key;
+
+ return SelectOptionColorPB.valueOf(minIndex) ?? SelectOptionColorPB.Purple;
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart
index 8479120d7b..1b0d73de8f 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/row/row_service.dart
@@ -28,16 +28,10 @@ class RowBackendService {
),
);
- Map? cellDataByFieldId;
-
if (withCells != null) {
final rowBuilder = RowDataBuilder();
withCells(rowBuilder);
- cellDataByFieldId = rowBuilder.build();
- }
-
- if (cellDataByFieldId != null) {
- payload.data = RowDataPB(cellDataByFieldId: cellDataByFieldId);
+ payload.data.addAll(rowBuilder.build());
}
return DatabaseEventCreateRow(payload).send();
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart
new file mode 100644
index 0000000000..740fadddc1
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_bloc.dart
@@ -0,0 +1,112 @@
+import 'dart:async';
+
+import 'package:appflowy/plugins/database/application/sync/database_sync_state_listener.dart';
+import 'package:appflowy/plugins/database/domain/database_view_service.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/auth/auth_service.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:connectivity_plus/connectivity_plus.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'database_sync_bloc.freezed.dart';
+
+class DatabaseSyncBloc extends Bloc {
+ DatabaseSyncBloc({
+ required this.view,
+ }) : super(DatabaseSyncBlocState.initial()) {
+ on(
+ (event, emit) async {
+ await event.when(
+ initial: () async {
+ final userProfile = await getIt().getUser().then(
+ (value) => value.fold((s) => s, (f) => null),
+ );
+ final databaseId = await DatabaseViewBackendService(viewId: view.id)
+ .getDatabaseId()
+ .then((value) => value.fold((s) => s, (f) => null));
+ emit(
+ state.copyWith(
+ shouldShowIndicator:
+ userProfile?.authenticator != AuthenticatorPB.Local &&
+ databaseId != null,
+ ),
+ );
+ if (databaseId != null) {
+ _syncStateListener =
+ DatabaseSyncStateListener(databaseId: databaseId)
+ ..start(
+ didReceiveSyncState: (syncState) {
+ Log.info(
+ 'database sync state changed, from ${state.syncState} to $syncState',
+ );
+ add(DatabaseSyncEvent.syncStateChanged(syncState));
+ },
+ );
+ }
+
+ final isNetworkConnected = await _connectivity
+ .checkConnectivity()
+ .then((value) => value != ConnectivityResult.none);
+ emit(state.copyWith(isNetworkConnected: isNetworkConnected));
+
+ connectivityStream =
+ _connectivity.onConnectivityChanged.listen((result) {
+ add(DatabaseSyncEvent.networkStateChanged(result));
+ });
+ },
+ syncStateChanged: (syncState) {
+ emit(state.copyWith(syncState: syncState.value));
+ },
+ networkStateChanged: (result) {
+ emit(
+ state.copyWith(
+ isNetworkConnected: result != ConnectivityResult.none,
+ ),
+ );
+ },
+ );
+ },
+ );
+ }
+
+ final ViewPB view;
+ final _connectivity = Connectivity();
+
+ StreamSubscription? connectivityStream;
+ DatabaseSyncStateListener? _syncStateListener;
+
+ @override
+ Future close() async {
+ await connectivityStream?.cancel();
+ await _syncStateListener?.stop();
+ return super.close();
+ }
+}
+
+@freezed
+class DatabaseSyncEvent with _$DatabaseSyncEvent {
+ const factory DatabaseSyncEvent.initial() = Initial;
+ const factory DatabaseSyncEvent.syncStateChanged(
+ DatabaseSyncStatePB syncState,
+ ) = syncStateChanged;
+ const factory DatabaseSyncEvent.networkStateChanged(
+ ConnectivityResult result,
+ ) = NetworkStateChanged;
+}
+
+@freezed
+class DatabaseSyncBlocState with _$DatabaseSyncBlocState {
+ const factory DatabaseSyncBlocState({
+ required DatabaseSyncState syncState,
+ @Default(true) bool isNetworkConnected,
+ @Default(false) bool shouldShowIndicator,
+ }) = _DatabaseSyncState;
+
+ factory DatabaseSyncBlocState.initial() => const DatabaseSyncBlocState(
+ syncState: DatabaseSyncState.Syncing,
+ );
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart
new file mode 100644
index 0000000000..67914e3007
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/plugins/database/application/sync/database_sync_state_listener.dart
@@ -0,0 +1,63 @@
+import 'dart:async';
+import 'dart:typed_data';
+
+import 'package:appflowy/core/notification/grid_notification.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
+import 'package:appflowy_backend/rust_stream.dart';
+import 'package:appflowy_result/appflowy_result.dart';
+
+typedef DatabaseSyncStateCallback = void Function(
+ DatabaseSyncStatePB syncState,
+);
+
+class DatabaseSyncStateListener {
+ DatabaseSyncStateListener({
+ // NOTE: NOT the view id.
+ required this.databaseId,
+ });
+
+ final String databaseId;
+ StreamSubscription? _subscription;
+ DatabaseNotificationParser? _parser;
+
+ DatabaseSyncStateCallback? didReceiveSyncState;
+
+ void start({
+ DatabaseSyncStateCallback? didReceiveSyncState,
+ }) {
+ this.didReceiveSyncState = didReceiveSyncState;
+
+ _parser = DatabaseNotificationParser(
+ id: databaseId,
+ callback: _callback,
+ );
+ _subscription = RustStreamReceiver.listen(
+ (observable) => _parser?.parse(observable),
+ );
+ }
+
+ void _callback(
+ DatabaseNotification ty,
+ FlowyResult result,
+ ) {
+ switch (ty) {
+ case DatabaseNotification.DidUpdateDatabaseSyncUpdate:
+ result.map(
+ (r) {
+ final value = DatabaseSyncStatePB.fromBuffer(r);
+ didReceiveSyncState?.call(value);
+ },
+ );
+ break;
+ default:
+ break;
+ }
+ }
+
+ Future stop() async {
+ await _subscription?.cancel();
+ _subscription = null;
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart
index 8ea1d004fa..7b816bbafc 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/board_bloc.dart
@@ -11,9 +11,11 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_board/appflowy_board.dart';
import 'package:appflowy_result/appflowy_result.dart';
+import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:intl/intl.dart';
import 'package:protobuf/protobuf.dart' hide FieldInfo;
import '../../application/database_controller.dart';
@@ -383,21 +385,28 @@ class BoardBloc extends Bloc {
groupList.insert(insertGroups.index, group);
add(BoardEvent.didReceiveGroups(groupList));
},
- onUpdateGroup: (updatedGroups) {
+ onUpdateGroup: (updatedGroups) async {
if (isClosed) {
return;
}
+ // workaround: update group most of the time gets called before fields in
+ // field controller are updated. For single and multi-select group
+ // renames, this is required before generating the new group name.
+ await Future.delayed(const Duration(milliseconds: 50));
+
for (final group in updatedGroups) {
// see if the column is already in the board
-
final index = groupList.indexWhere((g) => g.groupId == group.groupId);
- if (index == -1) continue;
+ if (index == -1) {
+ continue;
+ }
+
final columnController =
boardController.getGroupController(group.groupId);
if (columnController != null) {
// remove the group or update its name
- columnController.updateGroupName(group.groupName);
+ columnController.updateGroupName(generateGroupNameFromGroup(group));
if (!group.isVisible) {
boardController.removeGroup(group.groupId);
}
@@ -491,7 +500,7 @@ class BoardBloc extends Bloc {
AppFlowyGroupData _initializeGroupData(GroupPB group) {
return AppFlowyGroupData(
id: group.groupId,
- name: group.groupName,
+ name: generateGroupNameFromGroup(group),
items: _buildGroupItems(group),
customData: GroupData(
group: group,
@@ -499,6 +508,72 @@ class BoardBloc extends Bloc {
),
);
}
+
+ String generateGroupNameFromGroup(GroupPB group) {
+ final field = fieldController.getField(group.fieldId);
+ if (field == null) {
+ return "";
+ }
+
+ // if the group is the default group, then
+ if (group.isDefault) {
+ return "No ${field.name}";
+ }
+
+ switch (field.fieldType) {
+ case FieldType.SingleSelect:
+ final options =
+ SingleSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
+ .options;
+ final option =
+ options.firstWhereOrNull((option) => option.id == group.groupId);
+ return option == null ? "" : option.name;
+ case FieldType.MultiSelect:
+ final options =
+ MultiSelectTypeOptionPB.fromBuffer(field.field.typeOptionData)
+ .options;
+ final option =
+ options.firstWhereOrNull((option) => option.id == group.groupId);
+ return option == null ? "" : option.name;
+ case FieldType.Checkbox:
+ return group.groupId;
+ case FieldType.URL:
+ return group.groupId;
+ case FieldType.DateTime:
+ // Assume DateCondition::Relative as there isn't an option for this
+ // right now.
+ final dateFormat = DateFormat("y/MM/dd");
+ try {
+ final targetDateTime = dateFormat.parseLoose(group.groupId);
+ final targetDateTimeDay = DateTime(
+ targetDateTime.year,
+ targetDateTime.month,
+ targetDateTime.day,
+ );
+ final now = DateTime.now();
+ final nowDay = DateTime(
+ now.year,
+ now.month,
+ now.day,
+ );
+ final diff = targetDateTimeDay.difference(nowDay).inDays;
+ return switch (diff) {
+ 0 => "Today",
+ -1 => "Yesterday",
+ 1 => "Tomorrow",
+ -7 => "Last 7 days",
+ 2 => "Next 7 days",
+ -30 => "Last 30 days",
+ 8 => "Next 30 days",
+ _ => DateFormat("MMM y").format(targetDateTimeDay)
+ };
+ } on FormatException {
+ return "";
+ }
+ default:
+ return "";
+ }
+ }
}
@freezed
diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart
index 5f1b8c9609..7e0700e263 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/board/application/group_controller.dart
@@ -31,23 +31,11 @@ class GroupController {
final GroupControllerDelegate delegate;
final void Function(GroupPB group) onGroupChanged;
- RowMetaPB? rowAtIndex(int index) {
- if (index < group.rows.length) {
- return group.rows[index];
- } else {
- return null;
- }
- }
+ RowMetaPB? rowAtIndex(int index) => group.rows.elementAtOrNull(index);
- RowMetaPB? firstRow() {
- if (group.rows.isEmpty) return null;
- return group.rows.first;
- }
+ RowMetaPB? firstRow() => group.rows.firstOrNull;
- RowMetaPB? lastRow() {
- if (group.rows.isEmpty) return null;
- return group.rows.last;
- }
+ RowMetaPB? lastRow() => group.rows.lastOrNull;
void startListening() {
_listener.start(
diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart
index 16b2d45f95..40f60a09f4 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/board_page.dart
@@ -1,5 +1,7 @@
import 'dart:collection';
+import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
+import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
import 'package:flutter/material.dart' hide Card;
import 'package:flutter/services.dart';
@@ -34,6 +36,8 @@ import 'toolbar/board_setting_bar.dart';
import 'widgets/board_hidden_groups.dart';
class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
+ final _toggleExtension = ToggleExtensionNotifier();
+
@override
Widget content(
BuildContext context,
@@ -49,14 +53,27 @@ class BoardPageTabBarBuilderImpl extends DatabaseTabBarItemBuilder {
BoardSettingBar(
key: _makeValueKey(controller),
databaseController: controller,
+ toggleExtension: _toggleExtension,
);
@override
Widget settingBarExtension(
BuildContext context,
DatabaseController controller,
- ) =>
- const SizedBox.shrink();
+ ) {
+ return DatabaseViewSettingExtension(
+ key: _makeValueKey(controller),
+ viewId: controller.viewId,
+ databaseController: controller,
+ toggleExtension: _toggleExtension,
+ );
+ }
+
+ @override
+ void dispose() {
+ _toggleExtension.dispose();
+ super.dispose();
+ }
ValueKey _makeValueKey(DatabaseController controller) =>
ValueKey(controller.viewId);
diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart
index 4a678a160e..aa2883ff73 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/toolbar/board_setting_bar.dart
@@ -1,24 +1,53 @@
import 'package:appflowy/plugins/database/application/database_controller.dart';
+import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bloc.dart';
+import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
+import 'package:appflowy/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart';
import 'package:appflowy/plugins/database/widgets/setting/setting_button.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
class BoardSettingBar extends StatelessWidget {
const BoardSettingBar({
super.key,
required this.databaseController,
+ required this.toggleExtension,
});
final DatabaseController databaseController;
+ final ToggleExtensionNotifier toggleExtension;
@override
Widget build(BuildContext context) {
- return SizedBox(
- height: 20,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.end,
- children: [
- SettingButton(databaseController: databaseController),
- ],
+ return BlocProvider(
+ create: (context) => DatabaseFilterMenuBloc(
+ viewId: databaseController.viewId,
+ fieldController: databaseController.fieldController,
+ )..add(const DatabaseFilterMenuEvent.initial()),
+ child: BlocListener(
+ listenWhen: (p, c) => p.isVisible != c.isVisible,
+ listener: (context, state) => toggleExtension.toggle(),
+ child: ValueListenableBuilder(
+ valueListenable: databaseController.isLoading,
+ builder: (context, value, child) {
+ if (value) {
+ return const SizedBox.shrink();
+ }
+ return SizedBox(
+ height: 20,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ const FilterButton(),
+ const HSpace(2),
+ SettingButton(
+ databaseController: databaseController,
+ ),
+ ],
+ ),
+ );
+ },
+ ),
),
);
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart
index 82b11a0fd1..0469378c8b 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/board/presentation/widgets/board_hidden_groups.dart
@@ -269,7 +269,7 @@ class HiddenGroupButtonContent extends StatelessWidget {
),
const HSpace(4),
FlowyText.medium(
- group.groupName,
+ bloc.generateGroupNameFromGroup(group),
overflow: TextOverflow.ellipsis,
),
const HSpace(6),
@@ -369,7 +369,7 @@ class HiddenGroupPopupItemList extends StatelessWidget {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: FlowyText.medium(
- group.groupName,
+ context.read().generateGroupNameFromGroup(group),
fontSize: 10,
color: Theme.of(context).hintColor,
overflow: TextOverflow.ellipsis,
diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart
index 8d237f9114..8144904cad 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/domain/database_service.dart
@@ -4,7 +4,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
class DatabaseBackendService {
- static Future, FlowyError>>
+ static Future, FlowyError>>
getAllDatabases() {
return DatabaseEventGetDatabases().send().then((result) {
return result.fold(
diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart
index 4274c97c87..9bc873f7f1 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/domain/field_service.dart
@@ -62,6 +62,19 @@ class FieldBackendService {
return DatabaseEventDeleteField(payload).send();
}
+ // Clear all data of all cells in a Field
+ static Future> clearField({
+ required String viewId,
+ required String fieldId,
+ }) {
+ final payload = ClearFieldPayloadPB(
+ viewId: viewId,
+ fieldId: fieldId,
+ );
+
+ return DatabaseEventClearField(payload).send();
+ }
+
/// Duplicate a field
static Future> duplicateField({
required String viewId,
diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart
index f1b0a82723..a9295e38dd 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_listener.dart
@@ -61,19 +61,11 @@ class FilterListener {
final String viewId;
final String filterId;
- PublishNotifier? _onDeleteNotifier = PublishNotifier();
PublishNotifier? _onUpdateNotifier = PublishNotifier();
DatabaseNotificationListener? _listener;
- void start({
- void Function()? onDeleted,
- void Function(FilterPB)? onUpdated,
- }) {
- _onDeleteNotifier?.addPublishListener((_) {
- onDeleted?.call();
- });
-
+ void start({void Function(FilterPB)? onUpdated}) {
_onUpdateNotifier?.addPublishListener((filter) {
onUpdated?.call(filter);
});
@@ -85,20 +77,12 @@ class FilterListener {
}
void handleChangeset(FilterChangesetNotificationPB changeset) {
- // check the delete filter
- final deletedIndex = changeset.deleteFilters.indexWhere(
- (element) => element.id == filterId,
- );
- if (deletedIndex != -1) {
- _onDeleteNotifier?.value = changeset.deleteFilters[deletedIndex];
- }
-
- // check the updated filter
- final updatedIndex = changeset.updateFilters.indexWhere(
- (element) => element.filter.id == filterId,
+ final filters = changeset.filters.items;
+ final updatedIndex = filters.indexWhere(
+ (filter) => filter.id == filterId,
);
if (updatedIndex != -1) {
- _onUpdateNotifier?.value = changeset.updateFilters[updatedIndex].filter;
+ _onUpdateNotifier?.value = filters[updatedIndex];
}
}
@@ -122,9 +106,6 @@ class FilterListener {
Future stop() async {
await _listener?.stop();
- _onDeleteNotifier?.dispose();
- _onDeleteNotifier = null;
-
_onUpdateNotifier?.dispose();
_onUpdateNotifier = null;
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart
index a44cc13731..64854a8faf 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/domain/filter_service.dart
@@ -1,15 +1,6 @@
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbserver.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/database_entities.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/date_filter.pbserver.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/number_filter.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pbserver.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart' as $fixnum;
@@ -40,12 +31,18 @@ class FilterBackendService {
..condition = condition
..content = content;
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: FieldType.RichText,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: FieldType.RichText,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: FieldType.RichText,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertCheckboxFilter({
@@ -55,12 +52,18 @@ class FilterBackendService {
}) {
final filter = CheckboxFilterPB()..condition = condition;
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: FieldType.Checkbox,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: FieldType.Checkbox,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: FieldType.Checkbox,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertNumberFilter({
@@ -73,12 +76,18 @@ class FilterBackendService {
..condition = condition
..content = content;
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: FieldType.Number,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: FieldType.Number,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: FieldType.Number,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertDateFilter({
@@ -91,33 +100,35 @@ class FilterBackendService {
int? timestamp,
}) {
assert(
- [
- FieldType.DateTime,
- FieldType.LastEditedTime,
- FieldType.CreatedTime,
- ].contains(fieldType),
+ fieldType == FieldType.DateTime ||
+ fieldType == FieldType.LastEditedTime ||
+ fieldType == FieldType.CreatedTime,
);
final filter = DateFilterPB();
+
if (timestamp != null) {
filter.timestamp = $fixnum.Int64(timestamp);
- } else {
- if (start != null && end != null) {
- filter.start = $fixnum.Int64(start);
- filter.end = $fixnum.Int64(end);
- } else {
- throw Exception(
- "Start and end should not be null if the timestamp is null",
- );
- }
+ }
+ if (start != null) {
+ filter.start = $fixnum.Int64(start);
+ }
+ if (end != null) {
+ filter.end = $fixnum.Int64(end);
}
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: fieldType,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: FieldType.DateTime,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: FieldType.DateTime,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertURLFilter({
@@ -130,18 +141,24 @@ class FilterBackendService {
..condition = condition
..content = content;
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: FieldType.URL,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: FieldType.URL,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: FieldType.URL,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertSelectOptionFilter({
required String fieldId,
required FieldType fieldType,
- required SelectOptionConditionPB condition,
+ required SelectOptionFilterConditionPB condition,
String? filterId,
List optionIds = const [],
}) {
@@ -149,12 +166,18 @@ class FilterBackendService {
..condition = condition
..optionIds.addAll(optionIds);
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: fieldType,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: fieldType,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: fieldType,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertChecklistFilter({
@@ -165,67 +188,94 @@ class FilterBackendService {
}) {
final filter = ChecklistFilterPB()..condition = condition;
- return insertFilter(
- fieldId: fieldId,
- filterId: filterId,
- fieldType: FieldType.Checklist,
- data: filter.writeToBuffer(),
- );
+ return filterId == null
+ ? insertFilter(
+ fieldId: fieldId,
+ fieldType: FieldType.Checklist,
+ data: filter.writeToBuffer(),
+ )
+ : updateFilter(
+ filterId: filterId,
+ fieldId: fieldId,
+ fieldType: FieldType.Checklist,
+ data: filter.writeToBuffer(),
+ );
}
Future> insertFilter({
required String fieldId,
- String? filterId,
required FieldType fieldType,
required List data,
- }) {
- final insertFilterPayload = UpdateFilterPayloadPB.create()
+ }) async {
+ final filterData = FilterDataPB()
..fieldId = fieldId
..fieldType = fieldType
- ..viewId = viewId
..data = data;
- if (filterId != null) {
- insertFilterPayload.filterId = filterId;
- }
+ final insertFilterPayload = InsertFilterPB()..data = filterData;
- final payload = DatabaseSettingChangesetPB.create()
+ final payload = DatabaseSettingChangesetPB()
..viewId = viewId
- ..updateFilter = insertFilterPayload;
- return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {
- return result.fold(
- (l) => FlowyResult.success(l),
- (err) {
- Log.error(err);
- return FlowyResult.failure(err);
- },
- );
- });
+ ..insertFilter = insertFilterPayload;
+
+ final result = await DatabaseEventUpdateDatabaseSetting(payload).send();
+ return result.fold(
+ (l) => FlowyResult.success(l),
+ (err) {
+ Log.error(err);
+ return FlowyResult.failure(err);
+ },
+ );
+ }
+
+ Future> updateFilter({
+ required String filterId,
+ required String fieldId,
+ required FieldType fieldType,
+ required List data,
+ }) async {
+ final filterData = FilterDataPB()
+ ..fieldId = fieldId
+ ..fieldType = fieldType
+ ..data = data;
+
+ final updateFilterPayload = UpdateFilterDataPB()
+ ..filterId = filterId
+ ..data = filterData;
+
+ final payload = DatabaseSettingChangesetPB()
+ ..viewId = viewId
+ ..updateFilterData = updateFilterPayload;
+
+ final result = await DatabaseEventUpdateDatabaseSetting(payload).send();
+ return result.fold(
+ (l) => FlowyResult.success(l),
+ (err) {
+ Log.error(err);
+ return FlowyResult.failure(err);
+ },
+ );
}
Future> deleteFilter({
required String fieldId,
required String filterId,
- required FieldType fieldType,
- }) {
- final deleteFilterPayload = DeleteFilterPayloadPB.create()
+ }) async {
+ final deleteFilterPayload = DeleteFilterPB()
..fieldId = fieldId
- ..filterId = filterId
- ..viewId = viewId
- ..fieldType = fieldType;
+ ..filterId = filterId;
- final payload = DatabaseSettingChangesetPB.create()
+ final payload = DatabaseSettingChangesetPB()
..viewId = viewId
..deleteFilter = deleteFilterPayload;
- return DatabaseEventUpdateDatabaseSetting(payload).send().then((result) {
- return result.fold(
- (l) => FlowyResult.success(l),
- (err) {
- Log.error(err);
- return FlowyResult.failure(err);
- },
- );
- });
+ final result = await DatabaseEventUpdateDatabaseSetting(payload).send();
+ return result.fold(
+ (l) => FlowyResult.success(l),
+ (err) {
+ Log.error(err);
+ return FlowyResult.failure(err);
+ },
+ );
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart b/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart
index 165671b211..45f3155c4e 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/domain/select_option_cell_service.dart
@@ -2,8 +2,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
-
-import 'type_option_service.dart';
+import 'package:nanoid/nanoid.dart';
class SelectOptionCellBackendService {
SelectOptionCellBackendService({
@@ -18,26 +17,23 @@ class SelectOptionCellBackendService {
Future> create({
required String name,
+ SelectOptionColorPB? color,
bool isSelected = true,
}) {
- return TypeOptionBackendService(viewId: viewId, fieldId: fieldId)
- .newOption(name: name)
- .then(
- (result) {
- return result.fold(
- (option) {
- final payload = RepeatedSelectOptionPayload()
- ..viewId = viewId
- ..fieldId = fieldId
- ..rowId = rowId
- ..items.add(option);
+ final option = SelectOptionPB()
+ ..id = nanoid(4)
+ ..name = name;
+ if (color != null) {
+ option.color = color;
+ }
- return DatabaseEventInsertOrUpdateSelectOption(payload).send();
- },
- (r) => FlowyResult.failure(r),
- );
- },
- );
+ final payload = RepeatedSelectOptionPayload()
+ ..viewId = viewId
+ ..fieldId = fieldId
+ ..rowId = rowId
+ ..items.add(option);
+
+ return DatabaseEventInsertOrUpdateSelectOption(payload).send();
}
Future> update({
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart
index 67c4662b56..17449bda44 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart
@@ -44,7 +44,6 @@ class CheckboxFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
- fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@@ -64,11 +63,10 @@ class CheckboxFilterEditorBloc
void _startListening() {
_listener.start(
- onDeleted: () {
- if (!isClosed) add(const CheckboxFilterEditorEvent.delete());
- },
onUpdated: (filter) {
- if (!isClosed) add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
+ if (!isClosed) {
+ add(CheckboxFilterEditorEvent.didReceiveFilter(filter));
+ }
},
);
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart
index 5b91cd9195..1decdd8215 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/checklist_filter_bloc.dart
@@ -44,7 +44,6 @@ class ChecklistFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
- fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@@ -64,9 +63,6 @@ class ChecklistFilterEditorBloc
void _startListening() {
_listener.start(
- onDeleted: () {
- if (!isClosed) add(const ChecklistFilterEditorEvent.delete());
- },
onUpdated: (filter) {
if (!isClosed) {
add(ChecklistFilterEditorEvent.didReceiveFilter(filter));
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart
index 12dd22f266..d8ea5906a8 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_create_bloc.dart
@@ -114,7 +114,7 @@ class GridCreateFilterBloc
case FieldType.MultiSelect:
return _filterBackendSvc.insertSelectOptionFilter(
fieldId: fieldId,
- condition: SelectOptionConditionPB.OptionIs,
+ condition: SelectOptionFilterConditionPB.OptionContains,
fieldType: FieldType.MultiSelect,
);
case FieldType.Checklist:
@@ -130,19 +130,19 @@ class GridCreateFilterBloc
case FieldType.RichText:
return _filterBackendSvc.insertTextFilter(
fieldId: fieldId,
- condition: TextFilterConditionPB.Contains,
+ condition: TextFilterConditionPB.TextContains,
content: '',
);
case FieldType.SingleSelect:
return _filterBackendSvc.insertSelectOptionFilter(
fieldId: fieldId,
- condition: SelectOptionConditionPB.OptionIs,
+ condition: SelectOptionFilterConditionPB.OptionIs,
fieldType: FieldType.SingleSelect,
);
case FieldType.URL:
return _filterBackendSvc.insertURLFilter(
fieldId: fieldId,
- condition: TextFilterConditionPB.Contains,
+ condition: TextFilterConditionPB.TextContains,
);
default:
throw UnimplementedError();
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart
index 08e45305de..cc26e42b83 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/filter_menu_bloc.dart
@@ -8,11 +8,11 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'filter_menu_bloc.freezed.dart';
-class GridFilterMenuBloc
- extends Bloc {
- GridFilterMenuBloc({required this.viewId, required this.fieldController})
+class DatabaseFilterMenuBloc
+ extends Bloc {
+ DatabaseFilterMenuBloc({required this.viewId, required this.fieldController})
: super(
- GridFilterMenuState.initial(
+ DatabaseFilterMenuState.initial(
viewId,
fieldController.filterInfos,
fieldController.fieldInfos,
@@ -27,7 +27,7 @@ class GridFilterMenuBloc
void Function(List)? _onFieldFn;
void _dispatch() {
- on(
+ on(
(event, emit) async {
event.when(
initial: () {
@@ -55,11 +55,11 @@ class GridFilterMenuBloc
void _startListening() {
_onFilterFn = (filters) {
- add(GridFilterMenuEvent.didReceiveFilters(filters));
+ add(DatabaseFilterMenuEvent.didReceiveFilters(filters));
};
_onFieldFn = (fields) {
- add(GridFilterMenuEvent.didReceiveFields(fields));
+ add(DatabaseFilterMenuEvent.didReceiveFields(fields));
};
fieldController.addListener(
@@ -87,32 +87,33 @@ class GridFilterMenuBloc
}
@freezed
-class GridFilterMenuEvent with _$GridFilterMenuEvent {
- const factory GridFilterMenuEvent.initial() = _Initial;
- const factory GridFilterMenuEvent.didReceiveFilters(
+class DatabaseFilterMenuEvent with _$DatabaseFilterMenuEvent {
+ const factory DatabaseFilterMenuEvent.initial() = _Initial;
+ const factory DatabaseFilterMenuEvent.didReceiveFilters(
List filters,
) = _DidReceiveFilters;
- const factory GridFilterMenuEvent.didReceiveFields(List fields) =
- _DidReceiveFields;
- const factory GridFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
+ const factory DatabaseFilterMenuEvent.didReceiveFields(
+ List fields,
+ ) = _DidReceiveFields;
+ const factory DatabaseFilterMenuEvent.toggleMenu() = _SetMenuVisibility;
}
@freezed
-class GridFilterMenuState with _$GridFilterMenuState {
- const factory GridFilterMenuState({
+class DatabaseFilterMenuState with _$DatabaseFilterMenuState {
+ const factory DatabaseFilterMenuState({
required String viewId,
required List filters,
required List fields,
required List creatableFields,
required bool isVisible,
- }) = _GridFilterMenuState;
+ }) = _DatabaseFilterMenuState;
- factory GridFilterMenuState.initial(
+ factory DatabaseFilterMenuState.initial(
String viewId,
List filterInfos,
List fields,
) =>
- GridFilterMenuState(
+ DatabaseFilterMenuState(
viewId: viewId,
filters: filterInfos,
fields: fields,
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart
index 5b1de6d782..d68dd17537 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/number_filter_editor_bloc.dart
@@ -59,7 +59,6 @@ class NumberFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
- fieldType: filterInfo.fieldInfo.fieldType,
);
},
);
@@ -69,11 +68,6 @@ class NumberFilterEditorBloc
void _startListening() {
_listener.start(
- onDeleted: () {
- if (!isClosed) {
- add(const NumberFilterEditorEvent.delete());
- }
- },
onUpdated: (filter) {
if (!isClosed) {
add(NumberFilterEditorEvent.didReceiveFilter(filter));
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart
index cc92d2d906..3f44cb6d36 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_bloc.dart
@@ -38,7 +38,7 @@ class SelectOptionFilterEditorBloc
_startListening();
_loadOptions();
},
- updateCondition: (SelectOptionConditionPB condition) {
+ updateCondition: (SelectOptionFilterConditionPB condition) {
_filterBackendSvc.insertSelectOptionFilter(
filterId: filterInfo.filter.id,
fieldId: filterInfo.fieldInfo.id,
@@ -60,7 +60,6 @@ class SelectOptionFilterEditorBloc
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
- fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@@ -83,9 +82,6 @@ class SelectOptionFilterEditorBloc
void _startListening() {
_listener.start(
- onDeleted: () {
- if (!isClosed) add(const SelectOptionFilterEditorEvent.delete());
- },
onUpdated: (filter) {
if (!isClosed) {
add(SelectOptionFilterEditorEvent.didReceiveFilter(filter));
@@ -121,7 +117,7 @@ class SelectOptionFilterEditorEvent with _$SelectOptionFilterEditorEvent {
FilterPB filter,
) = _DidReceiveFilter;
const factory SelectOptionFilterEditorEvent.updateCondition(
- SelectOptionConditionPB condition,
+ SelectOptionFilterConditionPB condition,
) = _UpdateCondition;
const factory SelectOptionFilterEditorEvent.updateContent(
List optionIds,
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart
index 6bcc5b8806..84e1284822 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart
@@ -1,5 +1,5 @@
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -24,16 +24,19 @@ class SelectOptionFilterListBloc
_startListening();
_loadOptions();
},
- selectOption: (option) {
- final selectedOptionIds = Set.from(state.selectedOptionIds);
- selectedOptionIds.add(option.id);
+ selectOption: (option, condition) {
+ final selectedOptionIds = delegate.selectOption(
+ state.selectedOptionIds,
+ option.id,
+ condition,
+ );
_updateSelectOptions(
selectedOptionIds: selectedOptionIds,
emit: emit,
);
},
- unselectOption: (option) {
+ unSelectOption: (option) {
final selectedOptionIds = Set.from(state.selectedOptionIds);
selectedOptionIds.remove(option.id);
@@ -116,8 +119,9 @@ class SelectOptionFilterListEvent with _$SelectOptionFilterListEvent {
const factory SelectOptionFilterListEvent.initial() = _Initial;
const factory SelectOptionFilterListEvent.selectOption(
SelectOptionPB option,
+ SelectOptionFilterConditionPB condition,
) = _SelectOption;
- const factory SelectOptionFilterListEvent.unselectOption(
+ const factory SelectOptionFilterListEvent.unSelectOption(
SelectOptionPB option,
) = _UnSelectOption;
const factory SelectOptionFilterListEvent.didReceiveOptions(
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart
index 1c2791d8d0..e4fa67c4a8 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/application/filter/text_filter_editor_bloc.dart
@@ -3,8 +3,7 @@ import 'dart:async';
import 'package:appflowy/plugins/database/domain/filter_listener.dart';
import 'package:appflowy/plugins/database/domain/filter_service.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pbserver.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/util.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
@@ -12,7 +11,7 @@ part 'text_filter_editor_bloc.freezed.dart';
class TextFilterEditorBloc
extends Bloc {
- TextFilterEditorBloc({required this.filterInfo})
+ TextFilterEditorBloc({required this.filterInfo, required this.fieldType})
: _filterBackendSvc = FilterBackendService(viewId: filterInfo.viewId),
_listener = FilterListener(
viewId: filterInfo.viewId,
@@ -23,6 +22,7 @@ class TextFilterEditorBloc
}
final FilterInfo filterInfo;
+ final FieldType fieldType;
final FilterBackendService _filterBackendSvc;
final FilterListener _listener;
@@ -34,26 +34,39 @@ class TextFilterEditorBloc
_startListening();
},
updateCondition: (TextFilterConditionPB condition) {
- _filterBackendSvc.insertTextFilter(
- filterId: filterInfo.filter.id,
- fieldId: filterInfo.fieldInfo.id,
- condition: condition,
- content: state.filter.content,
- );
+ fieldType == FieldType.RichText
+ ? _filterBackendSvc.insertTextFilter(
+ filterId: filterInfo.filter.id,
+ fieldId: filterInfo.fieldInfo.id,
+ condition: condition,
+ content: state.filter.content,
+ )
+ : _filterBackendSvc.insertURLFilter(
+ filterId: filterInfo.filter.id,
+ fieldId: filterInfo.fieldInfo.id,
+ condition: condition,
+ content: state.filter.content,
+ );
},
- updateContent: (content) {
- _filterBackendSvc.insertTextFilter(
- filterId: filterInfo.filter.id,
- fieldId: filterInfo.fieldInfo.id,
- condition: state.filter.condition,
- content: content,
- );
+ updateContent: (String content) {
+ fieldType == FieldType.RichText
+ ? _filterBackendSvc.insertTextFilter(
+ filterId: filterInfo.filter.id,
+ fieldId: filterInfo.fieldInfo.id,
+ condition: state.filter.condition,
+ content: content,
+ )
+ : _filterBackendSvc.insertURLFilter(
+ filterId: filterInfo.filter.id,
+ fieldId: filterInfo.fieldInfo.id,
+ condition: state.filter.condition,
+ content: content,
+ );
},
delete: () {
_filterBackendSvc.deleteFilter(
fieldId: filterInfo.fieldInfo.id,
filterId: filterInfo.filter.id,
- fieldType: filterInfo.fieldInfo.fieldType,
);
},
didReceiveFilter: (FilterPB filter) {
@@ -73,11 +86,10 @@ class TextFilterEditorBloc
void _startListening() {
_listener.start(
- onDeleted: () {
- if (!isClosed) add(const TextFilterEditorEvent.delete());
- },
onUpdated: (filter) {
- if (!isClosed) add(TextFilterEditorEvent.didReceiveFilter(filter));
+ if (!isClosed) {
+ add(TextFilterEditorEvent.didReceiveFilter(filter));
+ }
},
);
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart
index 74b1dfdced..d33dba6293 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/condition_list.dart
@@ -1,15 +1,12 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/condition_button.dart';
+import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
-
-import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart';
-import 'package:flutter/material.dart';
-
-import '../../condition_button.dart';
-import '../../filter_info.dart';
+import 'package:flutter/widgets.dart';
class SelectOptionFilterConditionList extends StatelessWidget {
const SelectOptionFilterConditionList({
@@ -21,7 +18,7 @@ class SelectOptionFilterConditionList extends StatelessWidget {
final FilterInfo filterInfo;
final PopoverMutex popoverMutex;
- final Function(SelectOptionConditionPB) onCondition;
+ final Function(SelectOptionFilterConditionPB) onCondition;
@override
Widget build(BuildContext context) {
@@ -30,18 +27,17 @@ class SelectOptionFilterConditionList extends StatelessWidget {
asBarrier: true,
mutex: popoverMutex,
direction: PopoverDirection.bottomWithCenterAligned,
- actions: SelectOptionConditionPB.values
+ actions: _conditionsForFieldType(filterInfo.fieldInfo.fieldType)
.map(
(action) => ConditionWrapper(
action,
selectOptionFilter.condition == action,
- filterInfo.fieldInfo.fieldType,
),
)
.toList(),
buildChild: (controller) {
return ConditionButton(
- conditionName: filterName(selectOptionFilter),
+ conditionName: selectOptionFilter.condition.i18n,
onTap: () => controller.show(),
);
},
@@ -52,69 +48,62 @@ class SelectOptionFilterConditionList extends StatelessWidget {
);
}
- String filterName(SelectOptionFilterPB filter) {
- if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
- return filter.condition.singleSelectFilterName;
- } else {
- return filter.condition.multiSelectFilterName;
- }
+ List _conditionsForFieldType(
+ FieldType fieldType,
+ ) {
+ // SelectOptionFilterConditionPB.values is not in order
+ return switch (fieldType) {
+ FieldType.SingleSelect => [
+ SelectOptionFilterConditionPB.OptionIs,
+ SelectOptionFilterConditionPB.OptionIsNot,
+ SelectOptionFilterConditionPB.OptionIsEmpty,
+ SelectOptionFilterConditionPB.OptionIsNotEmpty,
+ ],
+ FieldType.MultiSelect => [
+ SelectOptionFilterConditionPB.OptionContains,
+ SelectOptionFilterConditionPB.OptionDoesNotContain,
+ SelectOptionFilterConditionPB.OptionIs,
+ SelectOptionFilterConditionPB.OptionIsNot,
+ SelectOptionFilterConditionPB.OptionIsEmpty,
+ SelectOptionFilterConditionPB.OptionIsNotEmpty,
+ ],
+ _ => [],
+ };
}
}
class ConditionWrapper extends ActionCell {
- ConditionWrapper(this.inner, this.isSelected, this.fieldType);
+ ConditionWrapper(this.inner, this.isSelected);
- final SelectOptionConditionPB inner;
+ final SelectOptionFilterConditionPB inner;
final bool isSelected;
- final FieldType fieldType;
@override
Widget? rightIcon(Color iconColor) {
- if (isSelected) {
- return const FlowySvg(FlowySvgs.check_s);
- } else {
- return null;
- }
+ return isSelected ? const FlowySvg(FlowySvgs.check_s) : null;
}
@override
- String get name {
- if (fieldType == FieldType.SingleSelect) {
- return inner.singleSelectFilterName;
- } else {
- return inner.multiSelectFilterName;
- }
- }
+ String get name => inner.i18n;
}
-extension SelectOptionConditionPBExtension on SelectOptionConditionPB {
- String get singleSelectFilterName {
- switch (this) {
- case SelectOptionConditionPB.OptionIs:
- return LocaleKeys.grid_singleSelectOptionFilter_is.tr();
- case SelectOptionConditionPB.OptionIsEmpty:
- return LocaleKeys.grid_singleSelectOptionFilter_isEmpty.tr();
- case SelectOptionConditionPB.OptionIsNot:
- return LocaleKeys.grid_singleSelectOptionFilter_isNot.tr();
- case SelectOptionConditionPB.OptionIsNotEmpty:
- return LocaleKeys.grid_singleSelectOptionFilter_isNotEmpty.tr();
- default:
- return "";
- }
- }
-
- String get multiSelectFilterName {
- switch (this) {
- case SelectOptionConditionPB.OptionIs:
- return LocaleKeys.grid_multiSelectOptionFilter_contains.tr();
- case SelectOptionConditionPB.OptionIsEmpty:
- return LocaleKeys.grid_multiSelectOptionFilter_isEmpty.tr();
- case SelectOptionConditionPB.OptionIsNot:
- return LocaleKeys.grid_multiSelectOptionFilter_doesNotContain.tr();
- case SelectOptionConditionPB.OptionIsNotEmpty:
- return LocaleKeys.grid_multiSelectOptionFilter_isNotEmpty.tr();
- default:
- return "";
- }
+extension SelectOptionFilterConditionPBExtension
+ on SelectOptionFilterConditionPB {
+ String get i18n {
+ return switch (this) {
+ SelectOptionFilterConditionPB.OptionIs =>
+ LocaleKeys.grid_selectOptionFilter_is.tr(),
+ SelectOptionFilterConditionPB.OptionIsNot =>
+ LocaleKeys.grid_selectOptionFilter_isNot.tr(),
+ SelectOptionFilterConditionPB.OptionContains =>
+ LocaleKeys.grid_selectOptionFilter_contains.tr(),
+ SelectOptionFilterConditionPB.OptionDoesNotContain =>
+ LocaleKeys.grid_selectOptionFilter_doesNotContain.tr(),
+ SelectOptionFilterConditionPB.OptionIsEmpty =>
+ LocaleKeys.grid_selectOptionFilter_isEmpty.tr(),
+ SelectOptionFilterConditionPB.OptionIsNotEmpty =>
+ LocaleKeys.grid_selectOptionFilter_isNotEmpty.tr(),
+ _ => "",
+ };
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart
index 5d2406094a..b3c1482453 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/option_list.dart
@@ -1,8 +1,9 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart';
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_list_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
-import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
+import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
@@ -88,11 +89,18 @@ class _SelectOptionFilterCellState extends State {
if (widget.isSelected) {
context
.read()
- .add(SelectOptionFilterListEvent.unselectOption(widget.option));
+ .add(SelectOptionFilterListEvent.unSelectOption(widget.option));
} else {
- context
- .read()
- .add(SelectOptionFilterListEvent.selectOption(widget.option));
+ context.read().add(
+ SelectOptionFilterListEvent.selectOption(
+ widget.option,
+ context
+ .read()
+ .state
+ .filter
+ .condition,
+ ),
+ );
}
},
children: [
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart
index 6c80c9f01a..24d1955a3f 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option.dart
@@ -101,9 +101,10 @@ class _SelectOptionFilterEditorState extends State {
SliverToBoxAdapter(child: _buildFilterPanel(context, state)),
];
- if (state.filter.condition != SelectOptionConditionPB.OptionIsEmpty &&
+ if (state.filter.condition !=
+ SelectOptionFilterConditionPB.OptionIsEmpty &&
state.filter.condition !=
- SelectOptionConditionPB.OptionIsNotEmpty) {
+ SelectOptionFilterConditionPB.OptionIsNotEmpty) {
slivers.add(const SliverToBoxAdapter(child: VSpace(4)));
slivers.add(
SliverToBoxAdapter(
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart
index c36ac8c992..7a7aa25cad 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart
@@ -1,9 +1,15 @@
import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/filter_info.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/select_option_entities.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
abstract class SelectOptionFilterDelegate {
List loadOptions();
+
+ Set selectOption(
+ Set currentOptionIds,
+ String optionId,
+ SelectOptionFilterConditionPB condition,
+ );
}
class SingleSelectOptionFilterDelegateImpl
@@ -17,6 +23,35 @@ class SingleSelectOptionFilterDelegateImpl
final parser = SingleSelectTypeOptionDataParser();
return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options;
}
+
+ @override
+ Set selectOption(
+ Set currentOptionIds,
+ String optionId,
+ SelectOptionFilterConditionPB condition,
+ ) {
+ final selectOptionIds = Set.from(currentOptionIds);
+
+ switch (condition) {
+ case SelectOptionFilterConditionPB.OptionIs:
+ if (selectOptionIds.isNotEmpty) {
+ selectOptionIds.clear();
+ }
+ selectOptionIds.add(optionId);
+ break;
+ case SelectOptionFilterConditionPB.OptionIsNot:
+ selectOptionIds.add(optionId);
+ break;
+ case SelectOptionFilterConditionPB.OptionIsEmpty ||
+ SelectOptionFilterConditionPB.OptionIsNotEmpty:
+ selectOptionIds.clear();
+ break;
+ default:
+ throw UnimplementedError();
+ }
+
+ return selectOptionIds;
+ }
}
class MultiSelectOptionFilterDelegateImpl
@@ -30,4 +65,12 @@ class MultiSelectOptionFilterDelegateImpl
final parser = MultiSelectTypeOptionDataParser();
return parser.fromBuffer(filterInfo.fieldInfo.field.typeOptionData).options;
}
+
+ @override
+ Set selectOption(
+ Set currentOptionIds,
+ String optionId,
+ SelectOptionFilterConditionPB condition,
+ ) =>
+ Set.from(currentOptionIds)..add(optionId);
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart
index 9c15af4f93..66f17e0971 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart
@@ -1,59 +1,44 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:easy_localization/easy_localization.dart';
-
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/text_filter.pb.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../../../application/filter/text_filter_editor_bloc.dart';
+
import '../condition_button.dart';
import '../disclosure_button.dart';
import '../filter_info.dart';
import 'choicechip.dart';
-class TextFilterChoicechip extends StatefulWidget {
+class TextFilterChoicechip extends StatelessWidget {
const TextFilterChoicechip({required this.filterInfo, super.key});
final FilterInfo filterInfo;
- @override
- State createState() => _TextFilterChoicechipState();
-}
-
-class _TextFilterChoicechipState extends State {
- late TextFilterEditorBloc bloc;
-
- @override
- void initState() {
- bloc = TextFilterEditorBloc(filterInfo: widget.filterInfo)
- ..add(const TextFilterEditorEvent.initial());
- super.initState();
- }
-
- @override
- void dispose() {
- bloc.close();
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
- return BlocProvider.value(
- value: bloc,
+ return BlocProvider(
+ create: (_) => TextFilterEditorBloc(
+ filterInfo: filterInfo,
+ fieldType: FieldType.RichText,
+ )..add(const TextFilterEditorEvent.initial()),
child: BlocBuilder(
- builder: (blocContext, state) {
+ builder: (context, state) {
return AppFlowyPopover(
- controller: PopoverController(),
constraints: BoxConstraints.loose(const Size(200, 76)),
direction: PopoverDirection.bottomWithCenterAligned,
- popupBuilder: (BuildContext context) {
- return TextFilterEditor(bloc: bloc);
+ popupBuilder: (popoverContext) {
+ return BlocProvider.value(
+ value: context.read(),
+ child: const TextFilterEditor(),
+ );
},
child: ChoiceChipButton(
- filterInfo: widget.filterInfo,
+ filterInfo: filterInfo,
filterDesc: _makeFilterDesc(state),
),
);
@@ -78,9 +63,7 @@ class _TextFilterChoicechipState extends State {
}
class TextFilterEditor extends StatefulWidget {
- const TextFilterEditor({required this.bloc, super.key});
-
- final TextFilterEditorBloc bloc;
+ const TextFilterEditor({super.key});
@override
State createState() => _TextFilterEditorState();
@@ -91,26 +74,23 @@ class _TextFilterEditorState extends State {
@override
Widget build(BuildContext context) {
- return BlocProvider.value(
- value: widget.bloc,
- child: BlocBuilder(
- builder: (context, state) {
- final List children = [
- _buildFilterPanel(context, state),
- ];
+ return BlocBuilder(
+ builder: (context, state) {
+ final List children = [
+ _buildFilterPanel(context, state),
+ ];
- if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
- state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
- children.add(const VSpace(4));
- children.add(_buildFilterTextField(context, state));
- }
+ if (state.filter.condition != TextFilterConditionPB.TextIsEmpty &&
+ state.filter.condition != TextFilterConditionPB.TextIsNotEmpty) {
+ children.add(const VSpace(4));
+ children.add(_buildFilterTextField(context, state));
+ }
- return Padding(
- padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
- child: IntrinsicHeight(child: Column(children: children)),
- );
- },
- ),
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
+ child: IntrinsicHeight(child: Column(children: children)),
+ );
+ },
);
}
@@ -236,17 +216,17 @@ class ConditionWrapper extends ActionCell {
extension TextFilterConditionPBExtension on TextFilterConditionPB {
String get filterName {
switch (this) {
- case TextFilterConditionPB.Contains:
+ case TextFilterConditionPB.TextContains:
return LocaleKeys.grid_textFilter_contains.tr();
- case TextFilterConditionPB.DoesNotContain:
+ case TextFilterConditionPB.TextDoesNotContain:
return LocaleKeys.grid_textFilter_doesNotContain.tr();
- case TextFilterConditionPB.EndsWith:
+ case TextFilterConditionPB.TextEndsWith:
return LocaleKeys.grid_textFilter_endsWith.tr();
- case TextFilterConditionPB.Is:
+ case TextFilterConditionPB.TextIs:
return LocaleKeys.grid_textFilter_is.tr();
- case TextFilterConditionPB.IsNot:
+ case TextFilterConditionPB.TextIsNot:
return LocaleKeys.grid_textFilter_isNot.tr();
- case TextFilterConditionPB.StartsWith:
+ case TextFilterConditionPB.TextStartsWith:
return LocaleKeys.grid_textFilter_startWith.tr();
case TextFilterConditionPB.TextIsEmpty:
return LocaleKeys.grid_textFilter_isEmpty.tr();
@@ -259,13 +239,13 @@ extension TextFilterConditionPBExtension on TextFilterConditionPB {
String get choicechipPrefix {
switch (this) {
- case TextFilterConditionPB.DoesNotContain:
+ case TextFilterConditionPB.TextDoesNotContain:
return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
- case TextFilterConditionPB.EndsWith:
+ case TextFilterConditionPB.TextEndsWith:
return LocaleKeys.grid_textFilter_choicechipPrefix_endWith.tr();
- case TextFilterConditionPB.IsNot:
+ case TextFilterConditionPB.TextIsNot:
return LocaleKeys.grid_textFilter_choicechipPrefix_isNot.tr();
- case TextFilterConditionPB.StartsWith:
+ case TextFilterConditionPB.TextStartsWith:
return LocaleKeys.grid_textFilter_choicechipPrefix_startWith.tr();
case TextFilterConditionPB.TextIsEmpty:
return LocaleKeys.grid_textFilter_choicechipPrefix_isEmpty.tr();
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart
index 440091f24d..53d2b0ace8 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/choicechip/url.dart
@@ -1,14 +1,58 @@
+import 'package:appflowy/plugins/database/grid/application/filter/text_filter_editor_bloc.dart';
+import 'package:appflowy/plugins/database/grid/presentation/widgets/filter/choicechip/text.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
+import 'package:appflowy_popover/appflowy_popover.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
import '../filter_info.dart';
import 'choicechip.dart';
-class URLFilterChoicechip extends StatelessWidget {
- const URLFilterChoicechip({required this.filterInfo, super.key});
+class URLFilterChoiceChip extends StatelessWidget {
+ const URLFilterChoiceChip({required this.filterInfo, super.key});
final FilterInfo filterInfo;
@override
Widget build(BuildContext context) {
- return ChoiceChipButton(filterInfo: filterInfo);
+ return BlocProvider(
+ create: (_) => TextFilterEditorBloc(
+ filterInfo: filterInfo,
+ fieldType: FieldType.URL,
+ ),
+ child: BlocBuilder(
+ builder: (context, state) {
+ return AppFlowyPopover(
+ constraints: BoxConstraints.loose(const Size(200, 76)),
+ direction: PopoverDirection.bottomWithCenterAligned,
+ popupBuilder: (popoverContext) {
+ return BlocProvider.value(
+ value: context.read(),
+ child: const TextFilterEditor(),
+ );
+ },
+ child: ChoiceChipButton(
+ filterInfo: filterInfo,
+ filterDesc: _makeFilterDesc(state),
+ ),
+ );
+ },
+ ),
+ );
+ }
+
+ String _makeFilterDesc(TextFilterEditorState state) {
+ String filterDesc = state.filter.condition.choicechipPrefix;
+ if (state.filter.condition == TextFilterConditionPB.TextIsEmpty ||
+ state.filter.condition == TextFilterConditionPB.TextIsNotEmpty) {
+ return filterDesc;
+ }
+
+ if (state.filter.content.isNotEmpty) {
+ filterDesc += " ${state.filter.content}";
+ }
+
+ return filterDesc;
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart
index 97fc590748..19c201d026 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_info.dart
@@ -18,42 +18,46 @@ class FilterInfo {
String get filterId => filter.id;
- String get fieldId => filter.fieldId;
+ String get fieldId => filter.data.fieldId;
DateFilterPB? dateFilter() {
- return filter.fieldType == FieldType.DateTime
- ? DateFilterPB.fromBuffer(filter.data)
+ final fieldType = filter.data.fieldType;
+ return fieldType == FieldType.DateTime ||
+ fieldType == FieldType.CreatedTime ||
+ fieldType == FieldType.LastEditedTime
+ ? DateFilterPB.fromBuffer(filter.data.data)
: null;
}
TextFilterPB? textFilter() {
- return filter.fieldType == FieldType.RichText
- ? TextFilterPB.fromBuffer(filter.data)
+ return filter.data.fieldType == FieldType.RichText ||
+ filter.data.fieldType == FieldType.URL
+ ? TextFilterPB.fromBuffer(filter.data.data)
: null;
}
CheckboxFilterPB? checkboxFilter() {
- return filter.fieldType == FieldType.Checkbox
- ? CheckboxFilterPB.fromBuffer(filter.data)
+ return filter.data.fieldType == FieldType.Checkbox
+ ? CheckboxFilterPB.fromBuffer(filter.data.data)
: null;
}
SelectOptionFilterPB? selectOptionFilter() {
- return filter.fieldType == FieldType.SingleSelect ||
- filter.fieldType == FieldType.MultiSelect
- ? SelectOptionFilterPB.fromBuffer(filter.data)
+ return filter.data.fieldType == FieldType.SingleSelect ||
+ filter.data.fieldType == FieldType.MultiSelect
+ ? SelectOptionFilterPB.fromBuffer(filter.data.data)
: null;
}
ChecklistFilterPB? checklistFilter() {
- return filter.fieldType == FieldType.Checklist
- ? ChecklistFilterPB.fromBuffer(filter.data)
+ return filter.data.fieldType == FieldType.Checklist
+ ? ChecklistFilterPB.fromBuffer(filter.data.data)
: null;
}
NumberFilterPB? numberFilter() {
- return filter.fieldType == FieldType.Number
- ? NumberFilterPB.fromBuffer(filter.data)
+ return filter.data.fieldType == FieldType.Number
+ ? NumberFilterPB.fromBuffer(filter.data.data)
: null;
}
}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart
index caca16a1ac..80deb98695 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu.dart
@@ -23,14 +23,14 @@ class FilterMenu extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return BlocProvider(
- create: (context) => GridFilterMenuBloc(
+ return BlocProvider(
+ create: (context) => DatabaseFilterMenuBloc(
viewId: fieldController.viewId,
fieldController: fieldController,
)..add(
- const GridFilterMenuEvent.initial(),
+ const DatabaseFilterMenuEvent.initial(),
),
- child: BlocBuilder(
+ child: BlocBuilder(
builder: (context, state) {
final List children = [];
children.addAll(
@@ -115,7 +115,7 @@ class _AddFilterButtonState extends State {
triggerActions: PopoverTriggerFlags.none,
child: child,
popupBuilder: (BuildContext context) {
- final bloc = buildContext.read();
+ final bloc = buildContext.read();
return GridCreateFilterList(
viewId: widget.viewId,
fieldController: bloc.fieldController,
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart
index f661ea57de..3ca86d3969 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/filter/filter_menu_item.dart
@@ -26,7 +26,7 @@ class FilterMenuItem extends StatelessWidget {
FieldType.RichText => TextFilterChoicechip(filterInfo: filterInfo),
FieldType.SingleSelect =>
SelectOptionFilterChoicechip(filterInfo: filterInfo),
- FieldType.URL => URLFilterChoicechip(filterInfo: filterInfo),
+ FieldType.URL => URLFilterChoiceChip(filterInfo: filterInfo),
FieldType.Checklist => ChecklistFilterChoicechip(filterInfo: filterInfo),
_ => const SizedBox(),
};
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart
index 2a36f5fece..1a63e3c678 100755
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart
@@ -2,6 +2,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
+import 'package:appflowy/plugins/database/widgets/field/field_editor.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@@ -12,7 +13,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../layout/sizes.dart';
-import 'field_editor.dart';
class GridFieldCell extends StatefulWidget {
const GridFieldCell({
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart
deleted file mode 100644
index e848200e4b..0000000000
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/header/type_option/relation.dart
+++ /dev/null
@@ -1,160 +0,0 @@
-import 'package:appflowy/generated/flowy_svgs.g.dart';
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/database/application/field/type_option/type_option_data_parser.dart';
-import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
-import 'package:appflowy_backend/dispatch/dispatch.dart';
-import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
-import 'package:appflowy_backend/protobuf/flowy-error/protobuf.dart';
-import 'package:appflowy_popover/appflowy_popover.dart';
-import 'package:appflowy_result/appflowy_result.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
-import 'package:protobuf/protobuf.dart';
-
-import 'builder.dart';
-
-class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
- const RelationTypeOptionEditorFactory();
-
- @override
- Widget? build({
- required BuildContext context,
- required String viewId,
- required FieldPB field,
- required PopoverMutex popoverMutex,
- required TypeOptionDataCallback onTypeOptionUpdated,
- }) {
- final typeOption = _parseTypeOptionData(field.typeOptionData);
-
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Container(
- padding: const EdgeInsets.only(left: 14, right: 8),
- height: GridSize.popoverItemHeight,
- alignment: Alignment.centerLeft,
- child: FlowyText.regular(
- LocaleKeys.grid_relation_relatedDatabasePlaceLabel.tr(),
- color: Theme.of(context).hintColor,
- fontSize: 11,
- ),
- ),
- AppFlowyPopover(
- mutex: popoverMutex,
- triggerActions: PopoverTriggerFlags.hover | PopoverTriggerFlags.click,
- offset: const Offset(6, 0),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- height: GridSize.popoverItemHeight,
- child: FlowyButton(
- text: FlowyText(
- typeOption.databaseId.isEmpty
- ? LocaleKeys.grid_relation_relatedDatabasePlaceholder.tr()
- : typeOption.databaseId,
- color: typeOption.databaseId.isEmpty
- ? Theme.of(context).hintColor
- : null,
- overflow: TextOverflow.ellipsis,
- ),
- rightIcon: const FlowySvg(FlowySvgs.more_s),
- ),
- ),
- popupBuilder: (context) {
- return _DatabaseList(
- onSelectDatabase: (newDatabaseId) {
- final newTypeOption = _updateTypeOption(
- typeOption: typeOption,
- databaseId: newDatabaseId,
- );
- onTypeOptionUpdated(newTypeOption.writeToBuffer());
- PopoverContainer.of(context).close();
- },
- currentDatabaseId:
- typeOption.databaseId.isEmpty ? null : typeOption.databaseId,
- );
- },
- ),
- ],
- );
- }
-
- RelationTypeOptionPB _parseTypeOptionData(List data) {
- return RelationTypeOptionDataParser().fromBuffer(data);
- }
-
- RelationTypeOptionPB _updateTypeOption({
- required RelationTypeOptionPB typeOption,
- required String databaseId,
- }) {
- typeOption.freeze();
- return typeOption.rebuild((typeOption) {
- typeOption.databaseId = databaseId;
- });
- }
-}
-
-class _DatabaseList extends StatefulWidget {
- const _DatabaseList({
- required this.onSelectDatabase,
- required this.currentDatabaseId,
- });
-
- final String? currentDatabaseId;
- final void Function(String databaseId) onSelectDatabase;
-
- @override
- State<_DatabaseList> createState() => _DatabaseListState();
-}
-
-class _DatabaseListState extends State<_DatabaseList> {
- late Future> future;
-
- @override
- void initState() {
- super.initState();
- future = DatabaseEventGetDatabases().send();
- }
-
- @override
- Widget build(BuildContext context) {
- return FutureBuilder(
- future: future,
- builder: (context, snapshot) {
- final data = snapshot.data;
- if (!snapshot.hasData ||
- snapshot.connectionState != ConnectionState.done ||
- data!.isFailure()) {
- return const SizedBox.shrink();
- }
-
- final databaseIds = data
- .fold>((l) => l.items, (r) => [])
- .map((databaseDescription) {
- final databaseId = databaseDescription.databaseId;
- return FlowyButton(
- onTap: () => widget.onSelectDatabase(databaseId),
- text: FlowyText.medium(
- databaseId,
- overflow: TextOverflow.ellipsis,
- ),
- rightIcon: databaseId == widget.currentDatabaseId
- ? FlowySvg(
- FlowySvgs.check_s,
- color: Theme.of(context).colorScheme.primary,
- )
- : null,
- );
- }).toList();
-
- return ListView.separated(
- shrinkWrap: true,
- separatorBuilder: (_, __) =>
- VSpace(GridSize.typeOptionSeparatorHeight),
- itemCount: databaseIds.length,
- itemBuilder: (context, index) => databaseIds[index],
- );
- },
- );
- }
-}
diff --git a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart
index 1ecb82e66d..21c61713e4 100644
--- a/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart
+++ b/frontend/appflowy_flutter/lib/plugins/database/grid/presentation/widgets/toolbar/filter_button.dart
@@ -23,7 +23,7 @@ class _FilterButtonState extends State {
@override
Widget build(BuildContext context) {
- return BlocBuilder(
+ return BlocBuilder(
builder: (context, state) {
final textColor = state.filters.isEmpty
? AFThemeExtension.of(context).textColor
@@ -41,11 +41,11 @@ class _FilterButtonState extends State {
padding: GridSize.toolbarSettingButtonInsets,
radius: Corners.s4Border,
onPressed: () {
- final bloc = context.read();
+ final bloc = context.read();
if (bloc.state.filters.isEmpty) {
_popoverController.show();
} else {
- bloc.add(const GridFilterMenuEvent.toggleMenu());
+ bloc.add(const DatabaseFilterMenuEvent.toggleMenu());
}
},
),
@@ -63,14 +63,14 @@ class _FilterButtonState extends State