Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a387b275f | ||
|
|
b6ce7aff9f | ||
|
|
eee3dcf43a | ||
|
|
384f8ddcb6 | ||
|
|
af716be7aa | ||
|
|
86e31253de | ||
|
|
3f00e78508 | ||
|
|
45c73a20c2 | ||
|
|
ce86227da3 | ||
|
|
4643851b3f | ||
|
|
5d7008edd7 | ||
|
|
079fab6118 | ||
|
|
b5f6591daa | ||
|
|
9f7f86a49a | ||
|
|
260705280f | ||
|
|
60d9a6b3ab | ||
|
|
67e350e797 | ||
|
|
85e489babb | ||
|
|
d0a914c10f | ||
|
|
bb312b0a79 | ||
|
|
8cdf6f9ec0 | ||
|
|
9666269e27 | ||
|
|
31458e817a | ||
|
|
9ba17e004e | ||
|
|
e141e7ee63 | ||
|
|
3932d0661d | ||
|
|
98d408af87 | ||
|
|
15e3d3b486 | ||
|
|
1ac1fabb6e | ||
|
|
6b4291ccc7 | ||
|
|
0cf0b29721 | ||
|
|
2a75ad144a | ||
|
|
d487820963 | ||
|
|
350c55e71d | ||
|
|
fd603fe80f | ||
|
|
07ebb0cb95 | ||
|
|
9dfc8dbb43 | ||
|
|
3f27576066 | ||
|
|
247a19b0c6 | ||
|
|
8494e0f0a6 | ||
|
|
e94eb20388 | ||
|
|
8bbae46a7c | ||
|
|
8521cc0897 | ||
|
|
12441f1183 | ||
|
|
53b7595de8 | ||
|
|
8c225fe547 | ||
|
|
a507fb8ec6 | ||
|
|
4db5b7a544 | ||
|
|
1de5b274c3 | ||
|
|
12b549bad2 | ||
|
|
6dbb08c2ae | ||
|
|
9711d670ca | ||
|
|
442dfe7ef8 | ||
|
|
f81d5eb23e | ||
|
|
32be754eb7 | ||
|
|
0643e93e94 | ||
|
|
681d9028e3 | ||
|
|
49574d3ff0 | ||
|
|
c64b83c2d6 | ||
|
|
52d65c4c4f | ||
|
|
4da786a439 | ||
|
|
114968f2ee | ||
|
|
29952bc7fd | ||
|
|
64ee3140a3 | ||
|
|
a2f9ca2f28 | ||
|
|
72dc0b8f67 | ||
|
|
721781f0ae | ||
|
|
0d879a6091 | ||
|
|
806a924fff | ||
|
|
9488f42749 | ||
|
|
a2161216d8 | ||
|
|
aca9b81c79 | ||
|
|
81dd353d12 | ||
|
|
eb1659fd3e | ||
|
|
c89277d507 | ||
|
|
fa01dbb572 | ||
|
|
63613689cd | ||
|
|
89f89e8822 | ||
|
|
a801fa627c | ||
|
|
c6c164d347 | ||
|
|
be4222a62f | ||
|
|
4fa2d6dc2e | ||
|
|
e476337a6a | ||
|
|
44f9254c0a | ||
|
|
89becbfe71 | ||
|
|
cf42d8cd43 | ||
|
|
2b27fe85aa | ||
|
|
30e7323cf7 | ||
|
|
39a97499de | ||
|
|
157f929ff9 | ||
|
|
bf36ef7fd9 | ||
|
|
7ba638268b | ||
|
|
96c427e869 | ||
|
|
fff4af53e2 | ||
|
|
6910004003 | ||
|
|
7cb964c7a2 | ||
|
|
eaa1cb5a67 | ||
|
|
553cfb3f5e | ||
|
|
086195d2cf | ||
|
|
64d9b76e3a | ||
|
|
5ce8087c91 | ||
|
|
6117f84373 | ||
|
|
c8b90974fb | ||
|
|
f1a25ab4b7 | ||
|
|
2d11d3a68d | ||
|
|
d89a89131e | ||
|
|
a800e01f0b | ||
|
|
29e07089ca | ||
|
|
3cdd6665b3 | ||
|
|
1ba299815e | ||
|
|
b4671c1d99 | ||
|
|
aa85970b0f | ||
|
|
3b3b61e67c | ||
|
|
f02e77fcd8 | ||
|
|
5adf3139ba | ||
|
|
c127fd28b8 | ||
|
|
d7783a6440 | ||
|
|
fba69767a7 | ||
|
|
acaaabee2a | ||
|
|
925ef48a83 | ||
|
|
80d1cbabe0 | ||
|
|
43031b50eb | ||
|
|
52ba0c0092 | ||
|
|
0e55cd07a4 | ||
|
|
14ac2db06d | ||
|
|
a162b02476 | ||
|
|
04e14c6bba | ||
|
|
d75645c4bc | ||
|
|
6d6e61956a | ||
|
|
37b119172b | ||
|
|
0ba26e0a84 |
No files matched your search
@@ -44,6 +44,7 @@ jobs:
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
name: appflowy_editor
|
||||
flags: appflowy editor
|
||||
env_vars: ${{ matrix.os }}
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
name: integration test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/app_flowy/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: "stable-2022-04-07"
|
||||
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
flutter-version: "3.0.5"
|
||||
cache: true
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
|
||||
- name: Cache Rust
|
||||
id: cache-rust-target
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
frontend/rust-lib/target
|
||||
shared-lib/target
|
||||
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
vcpkg integrate install
|
||||
cargo install --force duckscript_cli
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- if: steps.cache-cargo.outputs.cache-hit != 'true'
|
||||
name: Rust Deps
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Build Test lib
|
||||
working-directory: frontend
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
cargo make --profile production-linux-x86_64 appflowy
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
cargo make --profile production-mac-x86_64 appflowy
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
cargo make --profile production-windows-x86 appflowy
|
||||
fi
|
||||
|
||||
- name: Config Flutter
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
flutter config --enable-linux-desktop
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter config --enable-macos-desktop
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter config --enable-windows-desktop
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Flutter Code Generation
|
||||
working-directory: frontend/app_flowy
|
||||
run: |
|
||||
flutter packages pub get
|
||||
flutter packages pub run easy_localization:generate -f keys -o locale_keys.g.dart -S assets/translations -s en.json
|
||||
flutter packages pub run build_runner build --delete-conflicting-outputs
|
||||
|
||||
- name: Run AppFlowy tests
|
||||
working-directory: frontend/app_flowy
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
flutter test integration_test -d Linux --coverage
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
flutter test integration_test -d macOS --coverage
|
||||
elif [ "$RUNNER_OS" == "Windows" ]; then
|
||||
flutter test integration_test -d Windows --coverage
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
with:
|
||||
name: appflowy
|
||||
flags: appflowy
|
||||
env_vars: ${{ matrix.os }}
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
@@ -71,45 +71,29 @@ jobs:
|
||||
flutter config --enable-linux-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-linux-x86_64 appflowy
|
||||
|
||||
- name: Configuring Linux Package
|
||||
working-directory: frontend
|
||||
run: |
|
||||
mkdir -p ../${{ env.LINUX_APP_RELEASE_PATH }}/package/opt
|
||||
mkdir -p ../${{ env.LINUX_APP_RELEASE_PATH }}/package/usr/share/applications
|
||||
cp -r ./scripts/linux_installer ../${{ env.LINUX_APP_RELEASE_PATH }}/package/DEBIAN
|
||||
cd ../${{ env.LINUX_APP_RELEASE_PATH }}/package/DEBIAN
|
||||
grep -rl "\[CHANGE_THIS\]" ./control | xargs sed -i "s/\[CHANGE_THIS\]/${{ github.ref_name }}/"
|
||||
chmod 0755 {postinst,postrm}
|
||||
|
||||
- name: Build Linux package
|
||||
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
|
||||
run: |
|
||||
mkdir -p package/opt && mv AppFlowy package/opt/
|
||||
cd package && mkdir DEBIAN
|
||||
# Create control file
|
||||
printf 'Package: AppFlowy
|
||||
Version: %s
|
||||
Architecture: amd64
|
||||
Essential: no
|
||||
Priority: optional
|
||||
Maintainer: AppFlowy
|
||||
Description: An Open Source Alternative to Notion\n' "${{ github.ref_name }}" > DEBIAN/control
|
||||
mv AppFlowy package/opt/
|
||||
cd package
|
||||
|
||||
# postinst script for creating symlink
|
||||
printf '#!/bin/bash
|
||||
if [ -e /usr/local/bin/appflowy ]; then
|
||||
echo "Symlink already exists, skipping."
|
||||
else
|
||||
echo "Creating Symlink in /usr/local/bin/appflowy"
|
||||
ln -s /opt/AppFlowy/app_flowy /usr/local/bin/appflowy
|
||||
fi' > DEBIAN/postinst
|
||||
chmod 0755 DEBIAN/postinst
|
||||
|
||||
# postrm script for cleaning up residuals
|
||||
printf '#!/bin/bash
|
||||
if [ -e /usr/local/bin/appflowy ]; then
|
||||
rm /usr/local/bin/appflowy
|
||||
fi' > DEBIAN/postrm
|
||||
chmod 0755 DEBIAN/postrm
|
||||
|
||||
mkdir -p usr/share/applications
|
||||
# Update Exec & icon path in desktop entry
|
||||
grep -rl "\[CHANGE_THIS\]" ./opt/AppFlowy/appflowy.desktop.temp | xargs sed -i "s/\[CHANGE_THIS\]/\/opt/"
|
||||
# Add desktop entry in package
|
||||
mv ./opt/AppFlowy/appflowy.desktop.temp ./usr/share/applications/appflowy.desktop
|
||||
|
||||
# Build
|
||||
cd ../ && dpkg-deb --build --root-owner-group package ${{ env.LINUX_PACKAGE_NAME }}
|
||||
cd ../ && dpkg-deb --build --root-owner-group -Z xz package ${{ env.LINUX_PACKAGE_NAME }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
@@ -133,6 +117,80 @@ jobs:
|
||||
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-linux-x86-alt:
|
||||
runs-on: ubuntu-18.04
|
||||
needs: create-release
|
||||
env:
|
||||
LINUX_APP_RELEASE_PATH: frontend/app_flowy/product/${{ github.ref_name }}/linux/Release
|
||||
LINUX_PACKAGE_NAME: AppFlowy_${{ github.ref_name }}_linux-amd64-alt.deb
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup environment - Rust and Cargo
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 'stable-2022-04-07'
|
||||
|
||||
- name: Setup environment - Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.0.5'
|
||||
|
||||
- name: Pre build
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
source $HOME/.cargo/env
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
cargo make appflowy-deps-tools
|
||||
|
||||
- name: Build Linux app
|
||||
working-directory: frontend
|
||||
run: |
|
||||
flutter config --enable-linux-desktop
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-linux-x86_64 appflowy
|
||||
|
||||
- name: Configuring Linux Package
|
||||
working-directory: frontend
|
||||
run: |
|
||||
mkdir -p ../${{ env.LINUX_APP_RELEASE_PATH }}/package/opt
|
||||
mkdir -p ../${{ env.LINUX_APP_RELEASE_PATH }}/package/usr/share/applications
|
||||
cp -r ./scripts/linux_installer ../${{ env.LINUX_APP_RELEASE_PATH }}/package/DEBIAN
|
||||
cd ../${{ env.LINUX_APP_RELEASE_PATH }}/package/DEBIAN
|
||||
grep -rl "\[CHANGE_THIS\]" ./control | xargs sed -i "s/\[CHANGE_THIS\]/${{ github.ref_name }}/"
|
||||
chmod 0755 {postinst,postrm}
|
||||
|
||||
- name: Build Linux package
|
||||
working-directory: ${{ env.LINUX_APP_RELEASE_PATH }}
|
||||
run: |
|
||||
mv AppFlowy package/opt/
|
||||
cd package
|
||||
|
||||
# Update Exec & icon path in desktop entry
|
||||
grep -rl "\[CHANGE_THIS\]" ./opt/AppFlowy/appflowy.desktop.temp | xargs sed -i "s/\[CHANGE_THIS\]/\/opt/"
|
||||
# Add desktop entry in package
|
||||
mv ./opt/AppFlowy/appflowy.desktop.temp ./usr/share/applications/appflowy.desktop
|
||||
|
||||
# Build
|
||||
cd ../ && dpkg-deb --build --root-owner-group -Z xz package ${{ env.LINUX_PACKAGE_NAME }}
|
||||
|
||||
- name: Upload Alternative Release Asset Install Package
|
||||
id: upload-release-asset-install-package
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
||||
asset_path: ${{ env.LINUX_APP_RELEASE_PATH }}/${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_name: ${{ env.LINUX_PACKAGE_NAME }}
|
||||
asset_content_type: application/octet-stream
|
||||
|
||||
build-macos-x86_64:
|
||||
runs-on: macos-latest
|
||||
needs: create-release
|
||||
@@ -170,13 +228,12 @@ jobs:
|
||||
cargo make --env APP_VERSION=${{ github.ref_name }} --profile production-mac-x86_64 appflowy
|
||||
|
||||
- name: Create MacOS dmg
|
||||
working-directory: frontend
|
||||
run: |
|
||||
brew install create-dmg
|
||||
create-dmg \
|
||||
--volname ${{ env.MACOS_DMG_NAME }} \
|
||||
--hide-extension "AppFlowy.app" \
|
||||
--background scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
|
||||
--background frontend/scripts/dmg_assets/AppFlowyInstallerBackground.jpg \
|
||||
--window-size 600 450 \
|
||||
--icon-size 94 \
|
||||
--icon "AppFlowy.app" 141 249 \
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Rust coverage tests
|
||||
name: Rust code coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -9,25 +9,16 @@ on:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- "shared-lib/**"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
|
||||
jobs:
|
||||
test-coverage:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -35,39 +26,41 @@ jobs:
|
||||
|
||||
- name: Cache Cargo
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
key: ${{ runner.os }}-cargo-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
|
||||
- name: Cache Rust
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
with:
|
||||
path: |
|
||||
frontend/rust-lib/target
|
||||
shared-lib/target
|
||||
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
key: ${{ runner.os }}-rust-rust-lib-share-lib-${{ steps.rust_toolchain.outputs.rustc_hash }}-${{ hashFiles('./frontend/rust-lib/Cargo.toml') }}
|
||||
|
||||
- name: Setup Environment
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Linux" ]; then
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo wget -qO /etc/apt/sources.list.d/dart_stable.list https://storage.googleapis.com/download.dartlang.org/linux/debian/dart_stable.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dart curl build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
elif [ "$RUNNER_OS" == "macOS" ]; then
|
||||
echo 'do nothing'
|
||||
fi
|
||||
shell: bash
|
||||
|
||||
- name: Install cargo-make, grcov and llvm-tools-preview
|
||||
- name: Install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install cargo-make
|
||||
|
||||
- name: Setup environment - Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: 'stable'
|
||||
flutter-version: '3.0.5'
|
||||
|
||||
- name: Install code-coverage tools
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo wget -qO /etc/apt/trusted.gpg.d/dart_linux_signing_key.asc https://dl-ssl.google.com/linux/linux_signing_key.pub
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y build-essential libsqlite3-dev libssl-dev clang cmake ninja-build pkg-config libgtk-3-dev
|
||||
sudo apt-get install keybinder-3.0
|
||||
cargo install grcov
|
||||
rustup component add llvm-tools-preview
|
||||
|
||||
- name: Run Coverage tests and generate LCOV report
|
||||
- name: Run tests
|
||||
working-directory: frontend
|
||||
run: cargo make get_ci_test_coverage
|
||||
run: cargo make rust_unit_test_with_coverage
|
||||
+28
-1
@@ -1,4 +1,31 @@
|
||||
# Release Notes
|
||||
|
||||
## Version 0.0.9 - 12/21/2022
|
||||
### New features
|
||||
- Enable the user to define where to store their data
|
||||
- Support inserting Emojis through the slash command
|
||||
### Bug Fixes
|
||||
- Fix some bugs
|
||||
|
||||
## Version 0.0.8.1 - 12/09/2022
|
||||
### New features
|
||||
- Support following your default system theme
|
||||
- Improve the filter in Grid
|
||||
### Bug Fixes
|
||||
- Copy/Paste
|
||||
|
||||
## Version 0.0.8 - 11/30/2022
|
||||
### New features
|
||||
- Table-view database
|
||||
- support column type: Checklist
|
||||
- Board-view database
|
||||
- support column type: Checklist
|
||||
- Customize font size: small, medium, large
|
||||
|
||||
## Version 0.0.7.1 - 11/30/2022
|
||||
### Bug Fixes
|
||||
- Fix some bugs
|
||||
|
||||
## Version 0.0.7 - 11/27/2022
|
||||
### New features
|
||||
- Support adding filters by the text/checkbox/single-select property in Grid
|
||||
@@ -129,7 +156,7 @@ v0.0.4 - beta.1 is pre-release
|
||||
|
||||
### New features
|
||||
- Table-view database
|
||||
- supported column types: Text, Checkbox, Single-select, Multi-select, Numbers
|
||||
- support column types: Text, Checkbox, Single-select, Multi-select, Numbers
|
||||
- hide / delete columns
|
||||
- insert rows
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
CURRENT_APP_VERSION = "0.0.7"
|
||||
CURRENT_APP_VERSION = "0.0.9"
|
||||
FEATURES = "flutter"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
# CRATE_TYPE: https://doc.rust-lang.org/reference/linkage.html
|
||||
|
||||
@@ -138,12 +138,16 @@
|
||||
"open": "Obrir la configuració"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Mode Clar",
|
||||
"darkLabel": "Mode Fosc"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Mode Clar",
|
||||
"dark": "Mode Fosc",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"welcomeText": "Welcome to @:appName",
|
||||
"githubStarText": "Star on GitHub",
|
||||
"subscribeNewsletterText": "Subscribe to Newsletter",
|
||||
"letsGoButtonText": "Let's Go",
|
||||
"letsGoButtonText": "Quick Start",
|
||||
"title": "Title",
|
||||
"signUp": {
|
||||
"buttonText": "Sign Up",
|
||||
@@ -40,6 +40,12 @@
|
||||
"markdown": "Markdown",
|
||||
"copyLink": "Copy Link"
|
||||
},
|
||||
"moreAction": {
|
||||
"small": "small",
|
||||
"medium": "medium",
|
||||
"large": "large",
|
||||
"fontSize": "Font Size"
|
||||
},
|
||||
"disclosureAction": {
|
||||
"rename": "Rename",
|
||||
"delete": "Delete",
|
||||
@@ -149,11 +155,37 @@
|
||||
"appearance": "Appearance",
|
||||
"language": "Language",
|
||||
"user": "User",
|
||||
"files": "Files",
|
||||
"open": "Open Settings"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Light Mode",
|
||||
"darkLabel": "Dark Mode"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Light Mode",
|
||||
"dark": "Dark Mode",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
},
|
||||
"files": {
|
||||
"defaultLocation": "Where your data is stored now",
|
||||
"doubleTapToCopy": "Double tap to copy the path",
|
||||
"restoreLocation": "Restore to AppFlowy default path",
|
||||
"customizeLocation": "Open another folder",
|
||||
"restartApp": "Please restart app for the changes to take effect.",
|
||||
"exportDatabase": "Export databae",
|
||||
"selectFiles": "Select the files that need to be export",
|
||||
"createNewFolder": "Create a new folder",
|
||||
"createNewFolderDesc": "Tell us where you want to store your data ...",
|
||||
"open": "Open",
|
||||
"openFolder": "Open an existing folder",
|
||||
"openFolderDesc": "Read and write it to your existing AppFlowy folder ...",
|
||||
"folderHintText": "folder name",
|
||||
"location": "Creating a new folder",
|
||||
"locationDesc": "Pick a name for your AppFlowy data folder",
|
||||
"browser": "Browse",
|
||||
"create": "Create",
|
||||
"folderPath": "Path to store your folder",
|
||||
"locationCannotBeEmpty": "Path cannot be empty"
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
@@ -191,6 +223,10 @@
|
||||
"is": "is"
|
||||
}
|
||||
},
|
||||
"checklistFilter": {
|
||||
"isComplete": "is complete",
|
||||
"isIncomplted": "is incomplete"
|
||||
},
|
||||
"singleSelectOptionFilter": {
|
||||
"is": "Is",
|
||||
"isNot": "Is not",
|
||||
@@ -216,6 +252,7 @@
|
||||
"singleSelectFieldName": "Select",
|
||||
"multiSelectFieldName": "Multiselect",
|
||||
"urlFieldName": "URL",
|
||||
"checklistFieldName": "Checklist",
|
||||
"numberFormat": " Number format",
|
||||
"dateFormat": " Date format",
|
||||
"includeTime": " Include time",
|
||||
@@ -258,6 +295,9 @@
|
||||
"panelTitle": "Select an option or create one",
|
||||
"searchOption": "Search for an option"
|
||||
},
|
||||
"checklist": {
|
||||
"panelTitle": "Add an item"
|
||||
},
|
||||
"menuName": "Grid"
|
||||
},
|
||||
"document": {
|
||||
|
||||
@@ -144,8 +144,12 @@
|
||||
"open": "Abrir ajustes"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Modo Claro",
|
||||
"darkLabel": "Modo Oscuro"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Modo Claro",
|
||||
"dark": "Modo Oscuro",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
@@ -218,9 +222,9 @@
|
||||
"openSidebar": "Abrir panel lateral",
|
||||
"closeSidebar": "Cerrar panel lateral"
|
||||
},
|
||||
"board": {
|
||||
"board": {
|
||||
"column": {
|
||||
"create_new_card": "Nuevo"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,12 +138,16 @@
|
||||
"open": "Ouvrir les paramètres"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Mode clair",
|
||||
"darkLabel": "Mode sombre"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Mode clair",
|
||||
"dark": "Mode sombre",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +152,12 @@
|
||||
"open": "Ouvrir les paramètres"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Mode clair",
|
||||
"darkLabel": "Mode sombre"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Mode clair",
|
||||
"dark": "Mode sombre",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -138,12 +138,16 @@
|
||||
"open": "Beállítások megnyitása"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Világos mód",
|
||||
"darkLabel": "Éjjeli mód"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Világos mód",
|
||||
"dark": "Éjjeli mód",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,8 +145,12 @@
|
||||
"open": "Buka Pengaturan"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Mode Terang",
|
||||
"darkLabel": "Mode Gelap"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Mode Terang",
|
||||
"dark": "Mode Gelap",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -138,18 +138,22 @@
|
||||
"open": "aprire le impostazioni"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Modalità Chiara",
|
||||
"darkLabel": "Modalità Scura"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Modalità Chiara",
|
||||
"dark": "Modalità Scura",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
"menuName":"Griglia"
|
||||
"menuName": "Griglia"
|
||||
},
|
||||
"document":{
|
||||
"menuName":"Documento"
|
||||
"document": {
|
||||
"menuName": "Documento"
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,8 +138,12 @@
|
||||
"open": "設定"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "ライトモード",
|
||||
"darkLabel": "ダークモード"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "ライトモード",
|
||||
"dark": "ダークモード",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -138,12 +138,16 @@
|
||||
"open": "Otwórz Ustawienia"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Tryb Jasny",
|
||||
"darkLabel": "Tryb Ciemny"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Tryb Jasny",
|
||||
"dark": "Tryb Ciemny",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +152,12 @@
|
||||
"open": "Abrir as Configurações"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Modo Claro",
|
||||
"darkLabel": "Modo Escuro"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Modo Claro",
|
||||
"dark": "Modo Escuro",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -151,8 +151,12 @@
|
||||
"open": "Открыть настройки"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Светлая",
|
||||
"darkLabel": "Тёмная"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Светлая",
|
||||
"dark": "Тёмная",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -152,8 +152,12 @@
|
||||
"open": "Öppna inställningarna"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Ljust läge",
|
||||
"darkLabel": "Mörkt läge"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Ljust läge",
|
||||
"dark": "Mörkt läge",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
@@ -232,4 +236,4 @@
|
||||
"create_new_card": "Nytt"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,12 +138,16 @@
|
||||
"open": "Ayarları Aç"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "Aydınlık Mod",
|
||||
"darkLabel": "Karanlık Mod"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "Aydınlık Mod",
|
||||
"dark": "Karanlık Mod",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sideBar": {
|
||||
"openSidebar": "Open sidebar",
|
||||
"closeSidebar": "Close sidebar"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -152,8 +152,12 @@
|
||||
"open": "打开设置"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "日间模式",
|
||||
"darkLabel": "夜间模式"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "日间模式",
|
||||
"dark": "夜间模式",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -145,8 +145,12 @@
|
||||
"open": "開啟設定"
|
||||
},
|
||||
"appearance": {
|
||||
"lightLabel": "亮色模式",
|
||||
"darkLabel": "暗色模式"
|
||||
"themeMode": {
|
||||
"label": "Theme Mode",
|
||||
"light": "亮色模式",
|
||||
"dark": "暗色模式",
|
||||
"system": "Adapt to System"
|
||||
}
|
||||
}
|
||||
},
|
||||
"grid": {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import 'package:app_flowy/user/presentation/folder/folder_widget.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'util/mock/mock_file_picker.dart';
|
||||
import 'util/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('customize the folder path', () {
|
||||
const location = 'appflowy';
|
||||
|
||||
setUp(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
await TestFolder.setTestLocation(location);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await TestFolder.cleanTestLocation(location);
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await TestFolder.cleanTestLocation(null);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'customize folder name and path when launching app in first time',
|
||||
(tester) async {
|
||||
const folderName = 'appflowy';
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
// Click create button
|
||||
await tester.tapCreateButton();
|
||||
|
||||
// Set directory
|
||||
final cfw = find.byType(CreateFolderWidget);
|
||||
expect(cfw, findsOneWidget);
|
||||
final state = tester.state(cfw) as CreateFolderWidgetState;
|
||||
final dir = await TestFolder.testLocation(null);
|
||||
state.directory = dir.path;
|
||||
|
||||
// input folder name
|
||||
final ftf = find.byType(FlowyTextField);
|
||||
expect(ftf, findsOneWidget);
|
||||
await tester.enterText(ftf, 'appflowy');
|
||||
|
||||
// Click create button again
|
||||
await tester.tapCreateButton();
|
||||
|
||||
await tester.expectToSeeWelcomePage();
|
||||
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
});
|
||||
|
||||
testWidgets('open a new folder when launching app in first time',
|
||||
(tester) async {
|
||||
const folderName = 'appflowy';
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
await TestFolder.setTestLocation(folderName);
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
// tap open button
|
||||
await mockGetDirectoryPath(folderName);
|
||||
await tester.tapOpenFolderButton();
|
||||
|
||||
await tester.wait(1000);
|
||||
await tester.expectToSeeWelcomePage();
|
||||
|
||||
await TestFolder.cleanTestLocation(folderName);
|
||||
});
|
||||
|
||||
testWidgets('switch to B from A, then switch to A again', (tester) async {
|
||||
const String userA = 'userA';
|
||||
const String userB = 'userB';
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.setTestLocation(userA);
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
await tester.expectToSeeWelcomePage();
|
||||
|
||||
// swith to user B
|
||||
{
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userA);
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userB);
|
||||
await tester.tapCustomLocationButton();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.expectToSeeWelcomePage();
|
||||
}
|
||||
|
||||
// switch to the userA
|
||||
{
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.user);
|
||||
await tester.enterUserName(userB);
|
||||
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userA);
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.expectToSeeWelcomePage();
|
||||
expect(find.textContaining(userA), findsOneWidget);
|
||||
}
|
||||
|
||||
// swith to the userB again
|
||||
{
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// mock the file_picker result
|
||||
await mockGetDirectoryPath(userB);
|
||||
await tester.tapCustomLocationButton();
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
await tester.expectToSeeWelcomePage();
|
||||
expect(find.textContaining(userB), findsOneWidget);
|
||||
}
|
||||
|
||||
await TestFolder.cleanTestLocation(userA);
|
||||
await TestFolder.cleanTestLocation(userB);
|
||||
});
|
||||
|
||||
testWidgets('reset to default location', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
await tester.tapGoButton();
|
||||
|
||||
// home and readme document
|
||||
await tester.expectToSeeWelcomePage();
|
||||
|
||||
// open settings and restore the location
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.files);
|
||||
await tester.restoreLocation();
|
||||
|
||||
expect(
|
||||
await TestFolder.defaultDevelopmentLocation(),
|
||||
await TestFolder.currentLocation(),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:app_flowy/main.dart' as app;
|
||||
import 'package:app_flowy/startup/tasks/prelude.dart';
|
||||
import 'package:app_flowy/workspace/application/settings/settings_location_cubit.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class TestFolder {
|
||||
/// Location / Path
|
||||
|
||||
/// Set a given AppFlowy data storage location under test environment.
|
||||
///
|
||||
/// To pass null means clear the location.
|
||||
///
|
||||
/// The file_picker is a system component and can't be tapped, so using logic instead of tapping.
|
||||
///
|
||||
static Future<void> setTestLocation(String? name) async {
|
||||
final location = await testLocation(name);
|
||||
SharedPreferences.setMockInitialValues({
|
||||
kSettingsLocationDefaultLocation: location.path,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/// Clean the location.
|
||||
static Future<void> cleanTestLocation(String? name) async {
|
||||
final dir = await testLocation(name);
|
||||
await dir.delete(recursive: true);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Get current using location.
|
||||
static Future<String> currentLocation() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(kSettingsLocationDefaultLocation)!;
|
||||
}
|
||||
|
||||
/// Get default location under development environment.
|
||||
static Future<String> defaultDevelopmentLocation() async {
|
||||
final dir = await appFlowyDocumentDirectory();
|
||||
return dir.path;
|
||||
}
|
||||
|
||||
/// Get default location under test environment.
|
||||
static Future<Directory> testLocation(String? name) async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
var path = '${dir.path}/flowy_test';
|
||||
if (name != null) {
|
||||
path += '/$name';
|
||||
}
|
||||
return Directory(path).create(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> initializeAppFlowy() async {
|
||||
const MethodChannel('hotkey_manager')
|
||||
.setMockMethodCallHandler((MethodCall methodCall) async {
|
||||
if (methodCall.method == 'unregisterAll') {
|
||||
// do nothing
|
||||
}
|
||||
});
|
||||
|
||||
await app.main();
|
||||
await wait(3000);
|
||||
await pumpAndSettle(const Duration(seconds: 2));
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapButton(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
bool warnIfMissed = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
await tap(finder);
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithName(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final button = find.text(tr);
|
||||
await tapButton(
|
||||
button,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithTooltip(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final button = find.byTooltip(tr);
|
||||
await tapButton(
|
||||
button,
|
||||
milliseconds: milliseconds,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
extension AppFlowyLaunch on WidgetTester {
|
||||
Future<void> tapGoButton() async {
|
||||
await tapButtonWithName(LocaleKeys.letsGoButtonText.tr());
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapCreateButton() async {
|
||||
await tapButtonWithName(LocaleKeys.settings_files_create.tr());
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> expectToSeeWelcomePage() async {
|
||||
expect(find.byType(HomeStack), findsOneWidget);
|
||||
expect(find.textContaining('Read me'), findsNWidgets(2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/util/file_picker/file_picker_impl.dart';
|
||||
import 'package:app_flowy/util/file_picker/file_picker_service.dart';
|
||||
|
||||
import '../util.dart';
|
||||
|
||||
class MockFilePicker extends FilePicker {
|
||||
MockFilePicker({
|
||||
required this.mockPath,
|
||||
});
|
||||
|
||||
final String mockPath;
|
||||
|
||||
@override
|
||||
Future<String?> getDirectoryPath({String? title}) {
|
||||
return Future.value(mockPath);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> mockGetDirectoryPath(String? name) async {
|
||||
final dir = await TestFolder.testLocation(name);
|
||||
getIt.unregister<FilePickerService>();
|
||||
getIt.registerFactory<FilePickerService>(
|
||||
() => MockFilePicker(
|
||||
mockPath: dir.path,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:app_flowy/workspace/presentation/settings/widgets/settings_user_view.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
|
||||
enum SettingsPage {
|
||||
appearance,
|
||||
language,
|
||||
files,
|
||||
user,
|
||||
}
|
||||
|
||||
extension on SettingsPage {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case SettingsPage.appearance:
|
||||
return LocaleKeys.settings_menu_appearance.tr();
|
||||
case SettingsPage.language:
|
||||
return LocaleKeys.settings_menu_language.tr();
|
||||
case SettingsPage.files:
|
||||
return LocaleKeys.settings_menu_files.tr();
|
||||
case SettingsPage.user:
|
||||
return LocaleKeys.settings_menu_user.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowySettings on WidgetTester {
|
||||
/// Open settings page
|
||||
Future<void> openSettings() async {
|
||||
final settingsButton = find.byTooltip(LocaleKeys.settings_menu_open.tr());
|
||||
expect(settingsButton, findsOneWidget);
|
||||
await tapButton(settingsButton);
|
||||
final settingsDialog = find.byType(SettingsDialog);
|
||||
expect(settingsDialog, findsOneWidget);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Open the page taht insides the settings page
|
||||
Future<void> openSettingsPage(SettingsPage page) async {
|
||||
final button = find.text(page.name, findRichText: true);
|
||||
expect(button, findsOneWidget);
|
||||
await tapButton(button);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Restore the AppFlowy data storage location
|
||||
Future<void> restoreLocation() async {
|
||||
final buton =
|
||||
find.byTooltip(LocaleKeys.settings_files_restoreLocation.tr());
|
||||
expect(buton, findsOneWidget);
|
||||
await tapButton(buton);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapOpenFolderButton() async {
|
||||
final buton = find.text(LocaleKeys.settings_files_open.tr());
|
||||
expect(buton, findsOneWidget);
|
||||
await tapButton(buton);
|
||||
return;
|
||||
}
|
||||
|
||||
Future<void> tapCustomLocationButton() async {
|
||||
final buton =
|
||||
find.byTooltip(LocaleKeys.settings_files_customizeLocation.tr());
|
||||
expect(buton, findsOneWidget);
|
||||
await tapButton(buton);
|
||||
return;
|
||||
}
|
||||
|
||||
/// Enter user name
|
||||
Future<void> enterUserName(String name) async {
|
||||
final uni = find.byType(UserNameInput);
|
||||
expect(uni, findsOneWidget);
|
||||
await tap(uni);
|
||||
await enterText(uni, name);
|
||||
await wait(300); //
|
||||
await testTextInput.receiveAction(TextInputAction.done);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'base.dart';
|
||||
export 'launch.dart';
|
||||
export 'settings.dart';
|
||||
@@ -1,17 +1,21 @@
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/user/presentation/splash_screen.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
import 'startup/launch_configuration.dart';
|
||||
import 'startup/startup.dart';
|
||||
import 'user/presentation/splash_screen.dart';
|
||||
|
||||
class FlowyApp implements EntryPoint {
|
||||
@override
|
||||
Widget create() {
|
||||
return const SplashScreen();
|
||||
Widget create(LaunchConfiguration config) {
|
||||
return SplashScreen(
|
||||
autoRegister: config.autoRegistrationSupported,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() async {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await EasyLocalization.ensureInitialized();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
|
||||
@@ -229,8 +228,7 @@ class BoardBloc extends Bloc<BoardEvent, BoardState> {
|
||||
}
|
||||
|
||||
GridRowCache? getRowCache(String blockId) {
|
||||
final GridBlockCache? blockCache = _gridDataController.blocks[blockId];
|
||||
return blockCache?.rowCache;
|
||||
return _gridDataController.rowCache;
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/block/block_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/view/grid_view_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/grid_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||
@@ -31,11 +31,7 @@ class BoardDataController {
|
||||
final GridFFIService _gridFFIService;
|
||||
final GridFieldController fieldController;
|
||||
final BoardListener _listener;
|
||||
|
||||
// key: the block id
|
||||
final LinkedHashMap<String, GridBlockCache> _blocks;
|
||||
UnmodifiableMapView<String, GridBlockCache> get blocks =>
|
||||
UnmodifiableMapView(_blocks);
|
||||
late GridViewCache _viewCache;
|
||||
|
||||
OnFieldsChanged? _onFieldsChanged;
|
||||
OnGridChanged? _onGridChanged;
|
||||
@@ -43,21 +39,23 @@ class BoardDataController {
|
||||
OnRowsChanged? _onRowsChanged;
|
||||
OnError? _onError;
|
||||
|
||||
List<RowInfo> get rowInfos {
|
||||
final List<RowInfo> rows = [];
|
||||
for (var block in _blocks.values) {
|
||||
rows.addAll(block.rows);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
List<RowInfo> get rowInfos => _viewCache.rowInfos;
|
||||
GridRowCache get rowCache => _viewCache.rowCache;
|
||||
|
||||
BoardDataController({required ViewPB view})
|
||||
: gridId = view.id,
|
||||
_listener = BoardListener(view.id),
|
||||
// ignore: prefer_collection_literals
|
||||
_blocks = LinkedHashMap(),
|
||||
_gridFFIService = GridFFIService(gridId: view.id),
|
||||
fieldController = GridFieldController(gridId: view.id);
|
||||
fieldController = GridFieldController(gridId: view.id) {
|
||||
//
|
||||
_viewCache = GridViewCache(
|
||||
gridId: view.id,
|
||||
fieldController: fieldController,
|
||||
);
|
||||
_viewCache.addListener(onRowsChanged: (reason) {
|
||||
_onRowsChanged?.call(rowInfos, reason);
|
||||
});
|
||||
}
|
||||
|
||||
void addListener({
|
||||
required OnGridChanged onGridChanged,
|
||||
@@ -110,23 +108,21 @@ class BoardDataController {
|
||||
|
||||
Future<Either<Unit, FlowyError>> openGrid() async {
|
||||
final result = await _gridFFIService.openGrid();
|
||||
return Future(
|
||||
() => result.fold(
|
||||
(grid) async {
|
||||
_onGridChanged?.call(grid);
|
||||
final result = await fieldController.loadFields(
|
||||
fieldIds: grid.fields,
|
||||
);
|
||||
return result.fold(
|
||||
(l) {
|
||||
_loadGroups(grid.blocks);
|
||||
return left(l);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
},
|
||||
(err) => right(err),
|
||||
),
|
||||
|
||||
return result.fold(
|
||||
(grid) async {
|
||||
_onGridChanged?.call(grid);
|
||||
final result = await fieldController.loadFields(fieldIds: grid.fields);
|
||||
return result.fold(
|
||||
(l) {
|
||||
_loadGroups();
|
||||
_viewCache.rowCache.initializeRows(grid.rows);
|
||||
return left(l);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
},
|
||||
(err) => right(err),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,26 +134,9 @@ class BoardDataController {
|
||||
Future<void> dispose() async {
|
||||
await _gridFFIService.closeGrid();
|
||||
await fieldController.dispose();
|
||||
|
||||
for (final blockCache in _blocks.values) {
|
||||
blockCache.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadGroups(List<BlockPB> blocks) async {
|
||||
for (final block in blocks) {
|
||||
final cache = GridBlockCache(
|
||||
gridId: gridId,
|
||||
block: block,
|
||||
fieldController: fieldController,
|
||||
);
|
||||
|
||||
cache.addListener(onRowsChanged: (reason) {
|
||||
_onRowsChanged?.call(rowInfos, reason);
|
||||
});
|
||||
_blocks[block.id] = cache;
|
||||
}
|
||||
|
||||
Future<void> _loadGroups() async {
|
||||
final result = await _gridFFIService.loadGroups();
|
||||
return Future(
|
||||
() => result.fold(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_service.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
@@ -23,7 +23,6 @@ class BoardCardBloc extends Bloc<BoardCardEvent, BoardCardState> {
|
||||
required bool isEditing,
|
||||
}) : _rowService = RowFFIService(
|
||||
gridId: gridId,
|
||||
blockId: dataController.rowPB.blockId,
|
||||
),
|
||||
_dataController = dataController,
|
||||
super(
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_field_notifier.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/row/row_cache.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
typedef OnCardChanged = void Function(GridCellMap, RowsChangedReason);
|
||||
|
||||
@@ -10,11 +10,3 @@ class BoardGroupService {
|
||||
groupField = field;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class CanBeGroupField {
|
||||
String get groupContent;
|
||||
}
|
||||
|
||||
// class SingleSelectGroup extends CanBeGroupField {
|
||||
// final SingleSelectTypeOptionContext typeOptionContext;
|
||||
// }
|
||||
@@ -16,8 +16,8 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui_web.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../grid/application/row/row_cache.dart';
|
||||
@@ -358,6 +358,8 @@ Widget? _buildHeaderIcon(GroupData customData) {
|
||||
break;
|
||||
case FieldType.URL:
|
||||
break;
|
||||
case FieldType.Checklist:
|
||||
break;
|
||||
}
|
||||
|
||||
if (widget != null) {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_cell.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class BoardChecklistCell extends StatefulWidget {
|
||||
final GridCellControllerBuilder cellControllerBuilder;
|
||||
const BoardChecklistCell({required this.cellControllerBuilder, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<BoardChecklistCell> createState() => _BoardChecklistCellState();
|
||||
}
|
||||
|
||||
class _BoardChecklistCellState extends State<BoardChecklistCell> {
|
||||
late ChecklistCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as GridChecklistCellController;
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const ChecklistCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: const ChecklistProgressBar(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import 'board_cell.dart';
|
||||
import 'board_checkbox_cell.dart';
|
||||
import 'board_checklist_cell.dart';
|
||||
import 'board_date_cell.dart';
|
||||
import 'board_number_cell.dart';
|
||||
import 'board_select_option_cell.dart';
|
||||
@@ -58,6 +59,11 @@ class BoardCellBuilder {
|
||||
editableNotifier: cellNotifier,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return BoardChecklistCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return BoardNumberCell(
|
||||
groupId: groupId,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
library document_plugin;
|
||||
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/document/document_page.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/more/more_button.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:app_flowy/plugins/util.dart';
|
||||
import 'package:app_flowy/startup/plugin/plugin.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/document/application/share_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/left_bar_item.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'document_page.dart';
|
||||
|
||||
class DocumentPluginBuilder extends PluginBuilder {
|
||||
@override
|
||||
Plugin build(dynamic data) {
|
||||
@@ -49,6 +39,8 @@ class DocumentPluginBuilder extends PluginBuilder {
|
||||
|
||||
class DocumentPlugin extends Plugin<int> {
|
||||
late PluginType _pluginType;
|
||||
final DocumentAppearanceCubit _documentAppearanceCubit =
|
||||
DocumentAppearanceCubit();
|
||||
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
@@ -59,10 +51,22 @@ class DocumentPlugin extends Plugin<int> {
|
||||
Key? key,
|
||||
}) : notifier = ViewPluginNotifier(view: view) {
|
||||
_pluginType = pluginType;
|
||||
_documentAppearanceCubit.fetch();
|
||||
}
|
||||
|
||||
@override
|
||||
PluginDisplay get display => DocumentPluginDisplay(notifier: notifier);
|
||||
void dispose() {
|
||||
_documentAppearanceCubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
PluginDisplay get display {
|
||||
return DocumentPluginDisplay(
|
||||
notifier: notifier,
|
||||
documentAppearanceCubit: _documentAppearanceCubit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
PluginType get ty => _pluginType;
|
||||
@@ -75,8 +79,13 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
|
||||
final ViewPluginNotifier notifier;
|
||||
ViewPB get view => notifier.view;
|
||||
int? deletedViewIndex;
|
||||
DocumentAppearanceCubit documentAppearanceCubit;
|
||||
|
||||
DocumentPluginDisplay({required this.notifier, Key? key});
|
||||
DocumentPluginDisplay({
|
||||
required this.notifier,
|
||||
required this.documentAppearanceCubit,
|
||||
Key? key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget buildWidget(PluginContext context) {
|
||||
@@ -88,10 +97,17 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
|
||||
});
|
||||
});
|
||||
|
||||
return DocumentPage(
|
||||
view: view,
|
||||
onDeleted: () => context.onDeleted(view, deletedViewIndex),
|
||||
key: ValueKey(view.id),
|
||||
return BlocProvider.value(
|
||||
value: documentAppearanceCubit,
|
||||
child: BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (_, state) {
|
||||
return DocumentPage(
|
||||
view: view,
|
||||
onDeleted: () => context.onDeleted(view, deletedViewIndex),
|
||||
key: ValueKey(view.id),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,133 +115,19 @@ class DocumentPluginDisplay extends PluginDisplay with NavigationItem {
|
||||
Widget get leftBarItem => ViewLeftBarItem(view: view);
|
||||
|
||||
@override
|
||||
Widget? get rightBarItem => DocumentShareButton(view: view);
|
||||
Widget? get rightBarItem {
|
||||
return Row(
|
||||
children: [
|
||||
DocumentShareButton(view: view),
|
||||
const SizedBox(width: 10),
|
||||
BlocProvider.value(
|
||||
value: documentAppearanceCubit,
|
||||
child: const DocumentMoreButton(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
}
|
||||
|
||||
class DocumentShareButton extends StatelessWidget {
|
||||
final ViewPB view;
|
||||
DocumentShareButton({Key? key, required this.view})
|
||||
: super(key: ValueKey(view.hashCode));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DocShareBloc>(param1: view),
|
||||
child: BlocListener<DocShareBloc, DocShareState>(
|
||||
listener: (context, state) {
|
||||
state.map(
|
||||
initial: (_) {},
|
||||
loading: (_) {},
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
_handleExportData,
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<DocShareBloc, DocShareState>(
|
||||
builder: (context, state) => ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(
|
||||
height: 30,
|
||||
width: 100,
|
||||
),
|
||||
child: ShareActionList(view: view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleExportData(ExportDataPB exportData) {
|
||||
switch (exportData.exportType) {
|
||||
case ExportType.Link:
|
||||
break;
|
||||
case ExportType.Markdown:
|
||||
FlutterClipboard.copy(exportData.data)
|
||||
.then((value) => Log.info('copied to clipboard'));
|
||||
break;
|
||||
case ExportType.Text:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleExportError(FlowyError error) {}
|
||||
}
|
||||
|
||||
class ShareActionList extends StatelessWidget {
|
||||
const ShareActionList({
|
||||
Key? key,
|
||||
required this.view,
|
||||
}) : super(key: key);
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final docShareBloc = context.read<DocShareBloc>();
|
||||
return PopoverActionList<ShareActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: ShareAction.values
|
||||
.map((action) => ShareActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case ShareAction.markdown:
|
||||
final exportPath = await FilePicker.platform.saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${view.name}.md',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
|
||||
showMessageToast('Exported to: $exportPath');
|
||||
}
|
||||
break;
|
||||
case ShareAction.copyLink:
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.shareAction_workInProgress.tr())
|
||||
.show(context);
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ShareAction {
|
||||
markdown,
|
||||
copyLink,
|
||||
}
|
||||
|
||||
class ShareActionWrapper extends ActionCell {
|
||||
final ShareAction inner;
|
||||
|
||||
ShareActionWrapper(this.inner);
|
||||
|
||||
@override
|
||||
Widget? leftIcon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
}
|
||||
|
||||
extension QuestionBubbleExtension on ShareAction {
|
||||
String get name {
|
||||
switch (this) {
|
||||
case ShareAction.markdown:
|
||||
return LocaleKeys.shareAction_markdown.tr();
|
||||
case ShareAction.copyLink:
|
||||
return LocaleKeys.shareAction_copyLink.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import 'package:app_flowy/plugins/document/editor_styles.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/plugins/horizontal_rule_node_widget.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import '../../startup/startup.dart';
|
||||
import 'application/doc_bloc.dart';
|
||||
import 'editor_styles.dart';
|
||||
import 'presentation/banner.dart';
|
||||
|
||||
class DocumentPage extends StatefulWidget {
|
||||
final VoidCallback onDeleted;
|
||||
@@ -101,10 +102,30 @@ class _DocumentPageState extends State<DocumentPage> {
|
||||
editorState: editorState,
|
||||
autoFocus: editorState.document.isEmpty,
|
||||
customBuilders: {
|
||||
'horizontal_rule': HorizontalRuleWidgetBuilder(),
|
||||
// Divider
|
||||
kDividerType: DividerWidgetBuilder(),
|
||||
// Math Equation
|
||||
kMathEquationType: MathEquationNodeWidgetBuidler(),
|
||||
// Code Block
|
||||
kCodeBlockType: CodeBlockNodeWidgetBuilder(),
|
||||
},
|
||||
shortcutEvents: [
|
||||
insertHorizontalRule,
|
||||
// Divider
|
||||
insertDividerEvent,
|
||||
// Code Block
|
||||
enterInCodeBlock,
|
||||
ignoreKeysInCodeBlock,
|
||||
pasteInCodeBlock,
|
||||
],
|
||||
selectionMenuItems: [
|
||||
// Divider
|
||||
dividerMenuItem,
|
||||
// Math Equation
|
||||
mathEquationMenuItem,
|
||||
// Code Block
|
||||
codeBlockMenuItem,
|
||||
// Emoji
|
||||
emojiMenuItem,
|
||||
],
|
||||
themeData: theme.copyWith(extensions: [
|
||||
...theme.extensions.values,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
const _baseFontSize = 14.0;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
EditorStyle customEditorTheme(BuildContext context) {
|
||||
final documentStyle = context.watch<DocumentAppearanceCubit>().state;
|
||||
var editorStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? EditorStyle.dark
|
||||
: EditorStyle.light;
|
||||
@@ -11,14 +12,15 @@ EditorStyle customEditorTheme(BuildContext context) {
|
||||
padding: const EdgeInsets.all(0),
|
||||
textStyle: editorStyle.textStyle?.copyWith(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: _baseFontSize,
|
||||
fontSize: documentStyle.fontSize,
|
||||
),
|
||||
placeholderTextStyle: editorStyle.placeholderTextStyle?.copyWith(
|
||||
fontFamily: 'poppins',
|
||||
fontSize: _baseFontSize,
|
||||
fontSize: documentStyle.fontSize,
|
||||
),
|
||||
bold: editorStyle.bold?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: 'poppins-Bold',
|
||||
),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
);
|
||||
@@ -26,6 +28,8 @@ EditorStyle customEditorTheme(BuildContext context) {
|
||||
}
|
||||
|
||||
Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
||||
final documentStyle = context.watch<DocumentAppearanceCubit>().state;
|
||||
final baseFontSize = documentStyle.fontSize;
|
||||
const basePadding = 12.0;
|
||||
var headingPluginStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? HeadingPluginStyle.dark
|
||||
@@ -33,15 +37,15 @@ Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
||||
headingPluginStyle = headingPluginStyle.copyWith(
|
||||
textStyle: (EditorState editorState, Node node) {
|
||||
final headingToFontSize = {
|
||||
'h1': _baseFontSize + 12,
|
||||
'h2': _baseFontSize + 8,
|
||||
'h3': _baseFontSize + 4,
|
||||
'h4': _baseFontSize,
|
||||
'h5': _baseFontSize,
|
||||
'h6': _baseFontSize,
|
||||
'h1': baseFontSize + 12,
|
||||
'h2': baseFontSize + 8,
|
||||
'h3': baseFontSize + 4,
|
||||
'h4': baseFontSize,
|
||||
'h5': baseFontSize,
|
||||
'h6': baseFontSize,
|
||||
};
|
||||
final fontSize =
|
||||
headingToFontSize[node.attributes.heading] ?? _baseFontSize;
|
||||
headingToFontSize[node.attributes.heading] ?? baseFontSize;
|
||||
return TextStyle(fontSize: fontSize, fontWeight: FontWeight.w600);
|
||||
},
|
||||
padding: (EditorState editorState, Node node) {
|
||||
@@ -57,10 +61,28 @@ Iterable<ThemeExtension<dynamic>> customPluginTheme(BuildContext context) {
|
||||
return EdgeInsets.only(bottom: padding);
|
||||
},
|
||||
);
|
||||
var numberListPluginStyle = Theme.of(context).brightness == Brightness.dark
|
||||
? NumberListPluginStyle.dark
|
||||
: NumberListPluginStyle.light;
|
||||
|
||||
numberListPluginStyle = numberListPluginStyle.copyWith(
|
||||
icon: (_, textNode) {
|
||||
const iconPadding = EdgeInsets.only(left: 5.0, right: 5.0);
|
||||
return Container(
|
||||
padding: iconPadding,
|
||||
child: Text(
|
||||
'${textNode.attributes.number.toString()}.',
|
||||
style: customEditorTheme(context).textStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
final pluginTheme = Theme.of(context).brightness == Brightness.dark
|
||||
? darkPlguinStyleExtension
|
||||
: lightPlguinStyleExtension;
|
||||
return pluginTheme.toList()
|
||||
..removeWhere((element) => element is HeadingPluginStyle)
|
||||
..add(headingPluginStyle);
|
||||
..removeWhere((element) =>
|
||||
element is HeadingPluginStyle || element is NumberListPluginStyle)
|
||||
..add(headingPluginStyle)
|
||||
..add(numberListPluginStyle);
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const String _kDocumentAppearenceFontSize = 'kDocumentAppearenceFontSize';
|
||||
|
||||
class DocumentAppearance {
|
||||
const DocumentAppearance({
|
||||
required this.fontSize,
|
||||
});
|
||||
|
||||
final double fontSize;
|
||||
// Will be supported...
|
||||
// final String fontName;
|
||||
|
||||
DocumentAppearance copyWith({double? fontSize}) {
|
||||
return DocumentAppearance(
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DocumentAppearanceCubit extends Cubit<DocumentAppearance> {
|
||||
DocumentAppearanceCubit() : super(const DocumentAppearance(fontSize: 14.0));
|
||||
|
||||
void fetch() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final fontSize = prefs.getDouble(_kDocumentAppearenceFontSize) ?? 14.0;
|
||||
emit(state.copyWith(
|
||||
fontSize: fontSize,
|
||||
));
|
||||
}
|
||||
|
||||
void syncFontSize(double fontSize) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setDouble(_kDocumentAppearenceFontSize, fontSize);
|
||||
emit(state.copyWith(
|
||||
fontSize: fontSize,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
class FontSizeSwitcher extends StatefulWidget {
|
||||
const FontSizeSwitcher({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FontSizeSwitcher> createState() => _FontSizeSwitcherState();
|
||||
}
|
||||
|
||||
class _FontSizeSwitcherState extends State<FontSizeSwitcher> {
|
||||
final List<Tuple3<String, double, bool>> _fontSizes = [
|
||||
Tuple3(LocaleKeys.moreAction_small.tr(), 12.0, false),
|
||||
Tuple3(LocaleKeys.moreAction_medium.tr(), 14.0, true),
|
||||
Tuple3(LocaleKeys.moreAction_large.tr(), 18.0, false),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.semibold(
|
||||
LocaleKeys.moreAction_fontSize.tr(),
|
||||
fontSize: 12,
|
||||
),
|
||||
const SizedBox(
|
||||
height: 5,
|
||||
),
|
||||
ToggleButtons(
|
||||
isSelected:
|
||||
_fontSizes.map((e) => e.item2 == state.fontSize).toList(),
|
||||
onPressed: (int index) {
|
||||
_updateSelectedFontSize(_fontSizes[index].item2);
|
||||
},
|
||||
borderRadius: const BorderRadius.all(Radius.circular(5)),
|
||||
selectedBorderColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
selectedColor: Theme.of(context).colorScheme.onSurface,
|
||||
fillColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
color: Theme.of(context).hintColor,
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: 40.0,
|
||||
minWidth: 80.0,
|
||||
),
|
||||
children: _fontSizes
|
||||
.map((e) => Text(
|
||||
e.item1,
|
||||
style: TextStyle(fontSize: e.item2),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateSelectedFontSize(double fontSize) {
|
||||
context.read<DocumentAppearanceCubit>().syncFontSize(fontSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import 'package:app_flowy/plugins/document/presentation/more/cubit/document_appearance_cubit.dart';
|
||||
import 'package:app_flowy/plugins/document/presentation/more/font_size_switcher.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentMoreButton extends StatelessWidget {
|
||||
const DocumentMoreButton({
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<int>(
|
||||
offset: const Offset(0, 30),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: 1,
|
||||
enabled: false,
|
||||
child: BlocProvider.value(
|
||||
value: context.read<DocumentAppearanceCubit>(),
|
||||
child: const FontSizeSwitcher(),
|
||||
),
|
||||
),
|
||||
];
|
||||
},
|
||||
child: svgWidget(
|
||||
'editor/details',
|
||||
size: const Size(18, 18),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
library document_plugin;
|
||||
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/document/application/share_bloc.dart';
|
||||
import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:clipboard/clipboard.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentShareButton extends StatelessWidget {
|
||||
final ViewPB view;
|
||||
DocumentShareButton({Key? key, required this.view})
|
||||
: super(key: ValueKey(view.hashCode));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DocShareBloc>(param1: view),
|
||||
child: BlocListener<DocShareBloc, DocShareState>(
|
||||
listener: (context, state) {
|
||||
state.map(
|
||||
initial: (_) {},
|
||||
loading: (_) {},
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
_handleExportData,
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<DocShareBloc, DocShareState>(
|
||||
builder: (context, state) => ConstrainedBox(
|
||||
constraints: const BoxConstraints.expand(
|
||||
height: 30,
|
||||
width: 100,
|
||||
),
|
||||
child: ShareActionList(view: view),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleExportData(ExportDataPB exportData) {
|
||||
switch (exportData.exportType) {
|
||||
case ExportType.Link:
|
||||
break;
|
||||
case ExportType.Markdown:
|
||||
FlutterClipboard.copy(exportData.data)
|
||||
.then((value) => Log.info('copied to clipboard'));
|
||||
break;
|
||||
case ExportType.Text:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleExportError(FlowyError error) {}
|
||||
}
|
||||
|
||||
class ShareActionList extends StatelessWidget {
|
||||
const ShareActionList({
|
||||
Key? key,
|
||||
required this.view,
|
||||
}) : super(key: key);
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final docShareBloc = context.read<DocShareBloc>();
|
||||
return PopoverActionList<ShareActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: ShareAction.values
|
||||
.map((action) => ShareActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
onPressed: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case ShareAction.markdown:
|
||||
final exportPath = await FilePicker.platform.saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${view.name}.md',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
docShareBloc.add(DocShareEvent.shareMarkdown(exportPath));
|
||||
showMessageToast('Exported to: $exportPath');
|
||||
}
|
||||
break;
|
||||
case ShareAction.copyLink:
|
||||
NavigatorAlertDialog(
|
||||
title: LocaleKeys.shareAction_workInProgress.tr())
|
||||
.show(context);
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ShareAction {
|
||||
markdown,
|
||||
copyLink,
|
||||
}
|
||||
|
||||
class ShareActionWrapper extends ActionCell {
|
||||
final ShareAction inner;
|
||||
|
||||
ShareActionWrapper(this.inner);
|
||||
|
||||
Widget? icon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
String get name => inner.name;
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:app_flowy/core/grid_notification.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
|
||||
|
||||
typedef GridBlockUpdateNotifierValue = Either<GridBlockChangesetPB, FlowyError>;
|
||||
|
||||
class GridBlockListener {
|
||||
final String blockId;
|
||||
PublishNotifier<GridBlockUpdateNotifierValue>? _rowsUpdateNotifier =
|
||||
PublishNotifier();
|
||||
GridNotificationListener? _listener;
|
||||
|
||||
GridBlockListener({required this.blockId});
|
||||
|
||||
void start(void Function(GridBlockUpdateNotifierValue) onBlockChanged) {
|
||||
if (_listener != null) {
|
||||
_listener?.stop();
|
||||
}
|
||||
|
||||
_listener = GridNotificationListener(
|
||||
objectId: blockId,
|
||||
handler: _handler,
|
||||
);
|
||||
|
||||
_rowsUpdateNotifier?.addPublishListener(onBlockChanged);
|
||||
}
|
||||
|
||||
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
switch (ty) {
|
||||
case GridDartNotification.DidUpdateGridBlock:
|
||||
result.fold(
|
||||
(payload) => _rowsUpdateNotifier?.value =
|
||||
left(GridBlockChangesetPB.fromBuffer(payload)),
|
||||
(error) => _rowsUpdateNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_rowsUpdateNotifier?.dispose();
|
||||
_rowsUpdateNotifier = null;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ typedef GridCheckboxCellController = IGridCellController<String, String>;
|
||||
typedef GridNumberCellController = IGridCellController<String, String>;
|
||||
typedef GridSelectOptionCellController
|
||||
= IGridCellController<SelectOptionCellDataPB, String>;
|
||||
typedef GridChecklistCellController
|
||||
= IGridCellController<SelectOptionCellDataPB, String>;
|
||||
typedef GridDateCellController
|
||||
= IGridCellController<DateCellDataPB, CalendarData>;
|
||||
typedef GridURLCellController = IGridCellController<URLCellDataPB, String>;
|
||||
@@ -81,6 +83,7 @@ class GridCellControllerBuilder {
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.Checklist:
|
||||
final cellDataLoader = GridCellDataLoader(
|
||||
cellId: _cellId,
|
||||
parser: SelectOptionCellDataParser(),
|
||||
|
||||
@@ -38,7 +38,7 @@ class CellService {
|
||||
..gridId = cellId.gridId
|
||||
..fieldId = cellId.fieldId
|
||||
..rowId = cellId.rowId
|
||||
..content = data;
|
||||
..typeCellData = data;
|
||||
return GridEventUpdateCell(payload).send();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'cell_service/cell_service.dart';
|
||||
import 'checklist_cell_editor_bloc.dart';
|
||||
import 'select_option_service.dart';
|
||||
part 'checklist_cell_bloc.freezed.dart';
|
||||
|
||||
class ChecklistCellBloc extends Bloc<ChecklistCellEvent, ChecklistCellState> {
|
||||
final GridChecklistCellController cellController;
|
||||
final SelectOptionFFIService _selectOptionService;
|
||||
void Function()? _onCellChangedFn;
|
||||
ChecklistCellBloc({
|
||||
required this.cellController,
|
||||
}) : _selectOptionService =
|
||||
SelectOptionFFIService(cellId: cellController.cellId),
|
||||
super(ChecklistCellState.initial(cellController)) {
|
||||
on<ChecklistCellEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
emit(state.copyWith(
|
||||
allOptions: data.options,
|
||||
selectedOptions: data.selectOptions,
|
||||
percent: percentFromSelectOptionCellData(data),
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
if (_onCellChangedFn != null) {
|
||||
cellController.removeListener(_onCellChangedFn!);
|
||||
_onCellChangedFn = null;
|
||||
}
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_onCellChangedFn = cellController.startListening(
|
||||
onCellFieldChanged: () {
|
||||
_loadOptions();
|
||||
},
|
||||
onCellChanged: (data) {
|
||||
if (!isClosed && data != null) {
|
||||
add(ChecklistCellEvent.didReceiveOptions(data));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
_selectOptionService.getOptionContext().then((result) {
|
||||
if (isClosed) return;
|
||||
|
||||
return result.fold(
|
||||
(data) => add(ChecklistCellEvent.didReceiveOptions(data)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistCellEvent with _$ChecklistCellEvent {
|
||||
const factory ChecklistCellEvent.initial() = _InitialCell;
|
||||
const factory ChecklistCellEvent.didReceiveOptions(
|
||||
SelectOptionCellDataPB data) = _DidReceiveCellUpdate;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistCellState with _$ChecklistCellState {
|
||||
const factory ChecklistCellState({
|
||||
required List<SelectOptionPB> allOptions,
|
||||
required List<SelectOptionPB> selectedOptions,
|
||||
required double percent,
|
||||
}) = _ChecklistCellState;
|
||||
|
||||
factory ChecklistCellState.initial(
|
||||
GridChecklistCellController cellController) {
|
||||
return const ChecklistCellState(
|
||||
allOptions: [],
|
||||
selectedOptions: [],
|
||||
percent: 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'select_option_service.dart';
|
||||
|
||||
part 'checklist_cell_editor_bloc.freezed.dart';
|
||||
|
||||
class ChecklistCellEditorBloc
|
||||
extends Bloc<ChecklistCellEditorEvent, ChecklistCellEditorState> {
|
||||
final SelectOptionFFIService _selectOptionService;
|
||||
final GridChecklistCellController cellController;
|
||||
|
||||
ChecklistCellEditorBloc({
|
||||
required this.cellController,
|
||||
}) : _selectOptionService =
|
||||
SelectOptionFFIService(cellId: cellController.cellId),
|
||||
super(ChecklistCellEditorState.initial(cellController)) {
|
||||
on<ChecklistCellEditorEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
didReceiveOptions: (data) {
|
||||
emit(state.copyWith(
|
||||
allOptions: _makeChecklistSelectOptions(data, state.predicate),
|
||||
percent: percentFromSelectOptionCellData(data),
|
||||
));
|
||||
},
|
||||
newOption: (optionName) {
|
||||
_createOption(optionName);
|
||||
emit(state.copyWith(
|
||||
predicate: '',
|
||||
));
|
||||
},
|
||||
deleteOption: (option) {
|
||||
_deleteOption([option]);
|
||||
},
|
||||
updateOption: (option) {
|
||||
_updateOption(option);
|
||||
},
|
||||
selectOption: (option) async {
|
||||
if (option.isSelected) {
|
||||
await _selectOptionService.unSelect(optionIds: [option.data.id]);
|
||||
} else {
|
||||
await _selectOptionService.select(optionIds: [option.data.id]);
|
||||
}
|
||||
},
|
||||
filterOption: (String predicate) {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await cellController.dispose();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _createOption(String name) async {
|
||||
final result = await _selectOptionService.create(
|
||||
name: name,
|
||||
isSelected: false,
|
||||
);
|
||||
result.fold((l) => {}, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _deleteOption(List<SelectOptionPB> options) async {
|
||||
final result = await _selectOptionService.delete(options: options);
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _updateOption(SelectOptionPB option) async {
|
||||
final result = await _selectOptionService.update(
|
||||
option: option,
|
||||
);
|
||||
|
||||
result.fold((l) => null, (err) => Log.error(err));
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
_selectOptionService.getOptionContext().then((result) {
|
||||
if (isClosed) return;
|
||||
|
||||
return result.fold(
|
||||
(data) => add(ChecklistCellEditorEvent.didReceiveOptions(data)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
cellController.startListening(
|
||||
onCellChanged: ((data) {
|
||||
if (!isClosed && data != null) {
|
||||
add(ChecklistCellEditorEvent.didReceiveOptions(data));
|
||||
}
|
||||
}),
|
||||
onCellFieldChanged: () {
|
||||
_loadOptions();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistCellEditorEvent with _$ChecklistCellEditorEvent {
|
||||
const factory ChecklistCellEditorEvent.initial() = _Initial;
|
||||
const factory ChecklistCellEditorEvent.didReceiveOptions(
|
||||
SelectOptionCellDataPB data) = _DidReceiveOptions;
|
||||
const factory ChecklistCellEditorEvent.newOption(String optionName) =
|
||||
_NewOption;
|
||||
const factory ChecklistCellEditorEvent.selectOption(
|
||||
ChecklistSelectOption option) = _SelectOption;
|
||||
const factory ChecklistCellEditorEvent.updateOption(SelectOptionPB option) =
|
||||
_UpdateOption;
|
||||
const factory ChecklistCellEditorEvent.deleteOption(SelectOptionPB option) =
|
||||
_DeleteOption;
|
||||
const factory ChecklistCellEditorEvent.filterOption(String predicate) =
|
||||
_FilterOption;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistCellEditorState with _$ChecklistCellEditorState {
|
||||
const factory ChecklistCellEditorState({
|
||||
required List<ChecklistSelectOption> allOptions,
|
||||
required Option<String> createOption,
|
||||
required double percent,
|
||||
required String predicate,
|
||||
}) = _ChecklistCellEditorState;
|
||||
|
||||
factory ChecklistCellEditorState.initial(
|
||||
GridSelectOptionCellController context) {
|
||||
final data = context.getCellData(loadIfNotExist: true);
|
||||
|
||||
return ChecklistCellEditorState(
|
||||
allOptions: _makeChecklistSelectOptions(data, ''),
|
||||
createOption: none(),
|
||||
percent: percentFromSelectOptionCellData(data),
|
||||
predicate: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
double percentFromSelectOptionCellData(SelectOptionCellDataPB? data) {
|
||||
if (data == null) return 0;
|
||||
|
||||
final b = data.options.length.toDouble();
|
||||
if (b == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final a = data.selectOptions.length.toDouble();
|
||||
if (a > b) return 1.0;
|
||||
|
||||
return a / b;
|
||||
}
|
||||
|
||||
List<ChecklistSelectOption> _makeChecklistSelectOptions(
|
||||
SelectOptionCellDataPB? data, String predicate) {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final List<ChecklistSelectOption> options = [];
|
||||
final List<SelectOptionPB> allOptions = List.from(data.options);
|
||||
if (predicate.isNotEmpty) {
|
||||
allOptions.retainWhere((element) => element.name.contains(predicate));
|
||||
}
|
||||
final selectedOptionIds = data.selectOptions.map((e) => e.id).toList();
|
||||
|
||||
for (final option in allOptions) {
|
||||
options.add(
|
||||
ChecklistSelectOption(selectedOptionIds.contains(option.id), option),
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
class ChecklistSelectOption {
|
||||
final bool isSelected;
|
||||
final SelectOptionPB data;
|
||||
|
||||
ChecklistSelectOption(this.isSelected, this.data);
|
||||
}
|
||||
@@ -1,26 +1,24 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'select_option_service.dart';
|
||||
|
||||
part 'select_option_editor_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionCellEditorBloc
|
||||
extends Bloc<SelectOptionEditorEvent, SelectOptionEditorState> {
|
||||
final SelectOptionService _selectOptionService;
|
||||
final SelectOptionFFIService _selectOptionService;
|
||||
final GridSelectOptionCellController cellController;
|
||||
Timer? _delayOperation;
|
||||
|
||||
SelectOptionCellEditorBloc({
|
||||
required this.cellController,
|
||||
}) : _selectOptionService =
|
||||
SelectOptionService(cellId: cellController.cellId),
|
||||
SelectOptionFFIService(cellId: cellController.cellId),
|
||||
super(SelectOptionEditorState.initial(cellController)) {
|
||||
on<SelectOptionEditorEvent>(
|
||||
(event, emit) async {
|
||||
|
||||
@@ -6,15 +6,16 @@ import 'package:app_flowy/plugins/grid/application/field/type_option/type_option
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'cell_service/cell_service.dart';
|
||||
|
||||
class SelectOptionService {
|
||||
class SelectOptionFFIService {
|
||||
final GridCellIdentifier cellId;
|
||||
SelectOptionService({required this.cellId});
|
||||
SelectOptionFFIService({required this.cellId});
|
||||
|
||||
String get gridId => cellId.gridId;
|
||||
String get fieldId => cellId.fieldInfo.id;
|
||||
String get rowId => cellId.rowId;
|
||||
|
||||
Future<Either<Unit, FlowyError>> create({required String name}) {
|
||||
Future<Either<Unit, FlowyError>> create(
|
||||
{required String name, bool isSelected = true}) {
|
||||
return TypeOptionFFIService(gridId: gridId, fieldId: fieldId)
|
||||
.newOption(name: name)
|
||||
.then(
|
||||
@@ -26,8 +27,13 @@ class SelectOptionService {
|
||||
..fieldId = fieldId
|
||||
..rowId = rowId;
|
||||
final payload = SelectOptionChangesetPB.create()
|
||||
..insertOptions.add(option)
|
||||
..cellIdentifier = cellIdentifier;
|
||||
|
||||
if (isSelected) {
|
||||
payload.insertOptions.add(option);
|
||||
} else {
|
||||
payload.updateOptions.add(option);
|
||||
}
|
||||
return GridEventUpdateSelectOption(payload).send();
|
||||
},
|
||||
(r) => right(r),
|
||||
|
||||
@@ -18,8 +18,10 @@ class TextCellBloc extends Bloc<TextCellEvent, TextCellState> {
|
||||
_startListening();
|
||||
},
|
||||
updateText: (text) {
|
||||
cellController.saveCellData(text);
|
||||
emit(state.copyWith(content: text));
|
||||
if (state.content != text) {
|
||||
cellController.saveCellData(text);
|
||||
emit(state.copyWith(content: text));
|
||||
}
|
||||
},
|
||||
didReceiveCellUpdate: (content) {
|
||||
emit(state.copyWith(content: content));
|
||||
|
||||
@@ -503,25 +503,15 @@ class FieldInfo {
|
||||
|
||||
bool get hasFilter => _hasFilter;
|
||||
|
||||
bool get canGroup {
|
||||
bool get canBeGroup {
|
||||
switch (_field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
return true;
|
||||
case FieldType.DateTime:
|
||||
return false;
|
||||
case FieldType.MultiSelect:
|
||||
return true;
|
||||
case FieldType.Number:
|
||||
return false;
|
||||
case FieldType.RichText:
|
||||
return false;
|
||||
case FieldType.SingleSelect:
|
||||
return true;
|
||||
case FieldType.URL:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool get canCreateFilter {
|
||||
@@ -529,9 +519,10 @@ class FieldInfo {
|
||||
|
||||
switch (_field.fieldType) {
|
||||
case FieldType.Checkbox:
|
||||
// case FieldType.MultiSelect:
|
||||
case FieldType.MultiSelect:
|
||||
case FieldType.RichText:
|
||||
case FieldType.SingleSelect:
|
||||
case FieldType.Checklist:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
|
||||
@@ -3,7 +3,6 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'field_service.freezed.dart';
|
||||
|
||||
@@ -27,7 +27,7 @@ class GridFieldsListener {
|
||||
|
||||
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
switch (ty) {
|
||||
case GridDartNotification.DidUpdateGridField:
|
||||
case GridDartNotification.DidUpdateGridFields:
|
||||
result.fold(
|
||||
(payload) => updateFieldsNotifier?.value =
|
||||
left(GridFieldChangesetPB.fromBuffer(payload)),
|
||||
|
||||
+16
-3
@@ -1,6 +1,7 @@
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checklist_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_type_option.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
@@ -95,6 +96,17 @@ class MultiSelectTypeOptionWidgetDataParser
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-select
|
||||
typedef ChecklistTypeOptionContext = TypeOptionContext<ChecklistTypeOptionPB>;
|
||||
|
||||
class ChecklistTypeOptionWidgetDataParser
|
||||
extends TypeOptionDataParser<ChecklistTypeOptionPB> {
|
||||
@override
|
||||
ChecklistTypeOptionPB fromBuffer(List<int> buffer) {
|
||||
return ChecklistTypeOptionPB.fromBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
class TypeOptionContext<T extends GeneratedMessage> {
|
||||
T? _typeOptionObject;
|
||||
final TypeOptionDataParser<T> dataParser;
|
||||
@@ -109,15 +121,16 @@ class TypeOptionContext<T extends GeneratedMessage> {
|
||||
|
||||
String get fieldId => _dataController.field.id;
|
||||
|
||||
Future<void> loadTypeOptionData({
|
||||
required void Function(T) onCompleted,
|
||||
Future<T> loadTypeOptionData({
|
||||
void Function(T)? onCompleted,
|
||||
required void Function(FlowyError) onError,
|
||||
}) async {
|
||||
await _dataController.loadTypeOptionData().then((result) {
|
||||
result.fold((l) => null, (err) => onError(err));
|
||||
});
|
||||
|
||||
onCompleted(typeOption);
|
||||
onCompleted?.call(typeOption);
|
||||
return typeOption;
|
||||
}
|
||||
|
||||
T get typeOption {
|
||||
|
||||
+2
-2
@@ -33,14 +33,14 @@ class TypeOptionDataController {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> loadTypeOptionData() async {
|
||||
Future<Either<TypeOptionPB, FlowyError>> loadTypeOptionData() async {
|
||||
final result = await loader.load();
|
||||
return result.fold(
|
||||
(data) {
|
||||
data.freeze();
|
||||
_data = data;
|
||||
_fieldNotifier.value = data.field_2;
|
||||
return left(unit);
|
||||
return left(data);
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
|
||||
+5
-5
@@ -28,18 +28,18 @@ class CheckboxFilterEditorBloc
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
updateCondition: (CheckboxFilterCondition condition) {
|
||||
updateCondition: (CheckboxFilterConditionPB condition) {
|
||||
_ffiService.insertCheckboxFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.field.fieldType,
|
||||
fieldType: filterInfo.fieldInfo.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
@@ -79,7 +79,7 @@ class CheckboxFilterEditorEvent with _$CheckboxFilterEditorEvent {
|
||||
const factory CheckboxFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory CheckboxFilterEditorEvent.updateCondition(
|
||||
CheckboxFilterCondition condition) = _UpdateCondition;
|
||||
CheckboxFilterConditionPB condition) = _UpdateCondition;
|
||||
const factory CheckboxFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checklist_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'dart:async';
|
||||
import 'filter_listener.dart';
|
||||
import 'filter_service.dart';
|
||||
|
||||
part 'checklist_filter_bloc.freezed.dart';
|
||||
|
||||
class ChecklistFilterEditorBloc
|
||||
extends Bloc<ChecklistFilterEditorEvent, ChecklistFilterEditorState> {
|
||||
final FilterInfo filterInfo;
|
||||
final FilterFFIService _ffiService;
|
||||
// final SelectOptionFFIService _selectOptionService;
|
||||
final FilterListener _listener;
|
||||
|
||||
ChecklistFilterEditorBloc({
|
||||
required this.filterInfo,
|
||||
}) : _ffiService = FilterFFIService(viewId: filterInfo.viewId),
|
||||
// _selectOptionService =
|
||||
// SelectOptionFFIService(cellId: cellController.cellId)
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
super(ChecklistFilterEditorState.initial(filterInfo)) {
|
||||
on<ChecklistFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
updateCondition: (ChecklistFilterConditionPB condition) {
|
||||
_ffiService.insertChecklistFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.fieldInfo.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
final filterInfo = state.filterInfo.copyWith(filter: filter);
|
||||
final checklistFilter = filterInfo.checklistFilter()!;
|
||||
emit(state.copyWith(
|
||||
filterInfo: filterInfo,
|
||||
filter: checklistFilter,
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
_listener.start(
|
||||
onDeleted: () {
|
||||
if (!isClosed) add(const ChecklistFilterEditorEvent.delete());
|
||||
},
|
||||
onUpdated: (filter) {
|
||||
if (!isClosed) {
|
||||
add(ChecklistFilterEditorEvent.didReceiveFilter(filter));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistFilterEditorEvent with _$ChecklistFilterEditorEvent {
|
||||
const factory ChecklistFilterEditorEvent.initial() = _Initial;
|
||||
const factory ChecklistFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory ChecklistFilterEditorEvent.updateCondition(
|
||||
ChecklistFilterConditionPB condition) = _UpdateCondition;
|
||||
const factory ChecklistFilterEditorEvent.delete() = _Delete;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChecklistFilterEditorState with _$ChecklistFilterEditorState {
|
||||
const factory ChecklistFilterEditorState({
|
||||
required FilterInfo filterInfo,
|
||||
required ChecklistFilterPB filter,
|
||||
required String filterDesc,
|
||||
}) = _GridFilterState;
|
||||
|
||||
factory ChecklistFilterEditorState.initial(FilterInfo filterInfo) {
|
||||
return ChecklistFilterEditorState(
|
||||
filterInfo: filterInfo,
|
||||
filter: filterInfo.checklistFilter()!,
|
||||
filterDesc: '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checklist_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/number_filter.pb.dart';
|
||||
@@ -61,7 +62,7 @@ class GridCreateFilterBloc
|
||||
final List<FieldInfo> allFields = List.from(fields);
|
||||
final keyword = filterText.toLowerCase();
|
||||
allFields.retainWhere((field) {
|
||||
if (field.canCreateFilter) {
|
||||
if (!field.canCreateFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -89,43 +90,48 @@ class GridCreateFilterBloc
|
||||
case FieldType.Checkbox:
|
||||
return _ffiService.insertCheckboxFilter(
|
||||
fieldId: fieldId,
|
||||
condition: CheckboxFilterCondition.IsChecked,
|
||||
condition: CheckboxFilterConditionPB.IsChecked,
|
||||
);
|
||||
case FieldType.DateTime:
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
return _ffiService.insertDateFilter(
|
||||
fieldId: fieldId,
|
||||
condition: DateFilterCondition.DateIs,
|
||||
condition: DateFilterConditionPB.DateIs,
|
||||
timestamp: timestamp,
|
||||
);
|
||||
case FieldType.MultiSelect:
|
||||
return _ffiService.insertSelectOptionFilter(
|
||||
fieldId: fieldId,
|
||||
condition: SelectOptionCondition.OptionIs,
|
||||
condition: SelectOptionConditionPB.OptionIs,
|
||||
fieldType: FieldType.MultiSelect,
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return _ffiService.insertChecklistFilter(
|
||||
fieldId: fieldId,
|
||||
condition: ChecklistFilterConditionPB.IsIncomplete,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return _ffiService.insertNumberFilter(
|
||||
fieldId: fieldId,
|
||||
condition: NumberFilterCondition.Equal,
|
||||
condition: NumberFilterConditionPB.Equal,
|
||||
content: "",
|
||||
);
|
||||
case FieldType.RichText:
|
||||
return _ffiService.insertTextFilter(
|
||||
fieldId: fieldId,
|
||||
condition: TextFilterCondition.Contains,
|
||||
condition: TextFilterConditionPB.Contains,
|
||||
content: '',
|
||||
);
|
||||
case FieldType.SingleSelect:
|
||||
return _ffiService.insertSelectOptionFilter(
|
||||
fieldId: fieldId,
|
||||
condition: SelectOptionCondition.OptionIs,
|
||||
condition: SelectOptionConditionPB.OptionIs,
|
||||
fieldType: FieldType.SingleSelect,
|
||||
);
|
||||
case FieldType.URL:
|
||||
return _ffiService.insertURLFilter(
|
||||
fieldId: fieldId,
|
||||
condition: TextFilterCondition.Contains,
|
||||
condition: TextFilterConditionPB.Contains,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checklist_filter.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/date_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
|
||||
@@ -31,7 +32,7 @@ class FilterFFIService {
|
||||
Future<Either<Unit, FlowyError>> insertTextFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required TextFilterCondition condition,
|
||||
required TextFilterConditionPB condition,
|
||||
required String content,
|
||||
}) {
|
||||
final filter = TextFilterPB()
|
||||
@@ -49,7 +50,7 @@ class FilterFFIService {
|
||||
Future<Either<Unit, FlowyError>> insertCheckboxFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required CheckboxFilterCondition condition,
|
||||
required CheckboxFilterConditionPB condition,
|
||||
}) {
|
||||
final filter = CheckboxFilterPB()..condition = condition;
|
||||
|
||||
@@ -64,7 +65,7 @@ class FilterFFIService {
|
||||
Future<Either<Unit, FlowyError>> insertNumberFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required NumberFilterCondition condition,
|
||||
required NumberFilterConditionPB condition,
|
||||
String content = "",
|
||||
}) {
|
||||
final filter = NumberFilterPB()
|
||||
@@ -82,7 +83,7 @@ class FilterFFIService {
|
||||
Future<Either<Unit, FlowyError>> insertDateFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required DateFilterCondition condition,
|
||||
required DateFilterConditionPB condition,
|
||||
int? start,
|
||||
int? end,
|
||||
int? timestamp,
|
||||
@@ -111,7 +112,7 @@ class FilterFFIService {
|
||||
Future<Either<Unit, FlowyError>> insertURLFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
required TextFilterCondition condition,
|
||||
required TextFilterConditionPB condition,
|
||||
String content = "",
|
||||
}) {
|
||||
final filter = TextFilterPB()
|
||||
@@ -129,7 +130,7 @@ class FilterFFIService {
|
||||
Future<Either<Unit, FlowyError>> insertSelectOptionFilter({
|
||||
required String fieldId,
|
||||
required FieldType fieldType,
|
||||
required SelectOptionCondition condition,
|
||||
required SelectOptionConditionPB condition,
|
||||
String? filterId,
|
||||
List<String> optionIds = const [],
|
||||
}) {
|
||||
@@ -145,6 +146,22 @@ class FilterFFIService {
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertChecklistFilter({
|
||||
required String fieldId,
|
||||
required ChecklistFilterConditionPB condition,
|
||||
String? filterId,
|
||||
List<String> optionIds = const [],
|
||||
}) {
|
||||
final filter = ChecklistFilterPB()..condition = condition;
|
||||
|
||||
return insertFilter(
|
||||
fieldId: fieldId,
|
||||
filterId: filterId,
|
||||
fieldType: FieldType.Checklist,
|
||||
data: filter.writeToBuffer(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<Either<Unit, FlowyError>> insertFilter({
|
||||
required String fieldId,
|
||||
String? filterId,
|
||||
@@ -154,6 +171,7 @@ class FilterFFIService {
|
||||
var insertFilterPayload = AlterFilterPayloadPB.create()
|
||||
..fieldId = fieldId
|
||||
..fieldType = fieldType
|
||||
..viewId = viewId
|
||||
..data = data;
|
||||
|
||||
if (filterId != null) {
|
||||
@@ -162,7 +180,7 @@ class FilterFFIService {
|
||||
|
||||
final payload = GridSettingChangesetPB.create()
|
||||
..gridId = viewId
|
||||
..insertFilter = insertFilterPayload;
|
||||
..alterFilter = insertFilterPayload;
|
||||
return GridEventUpdateGridSetting(payload).send().then((result) {
|
||||
return result.fold(
|
||||
(l) => left(l),
|
||||
@@ -179,11 +197,12 @@ class FilterFFIService {
|
||||
required String filterId,
|
||||
required FieldType fieldType,
|
||||
}) {
|
||||
TextFilterCondition.DoesNotContain.value;
|
||||
TextFilterConditionPB.DoesNotContain.value;
|
||||
|
||||
final deleteFilterPayload = DeleteFilterPayloadPB.create()
|
||||
..fieldId = fieldId
|
||||
..filterId = filterId
|
||||
..viewId = viewId
|
||||
..fieldType = fieldType;
|
||||
|
||||
final payload = GridSettingChangesetPB.create()
|
||||
|
||||
+23
-31
@@ -1,7 +1,5 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_option_filter.pbserver.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/util.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -17,18 +15,16 @@ class SelectOptionFilterEditorBloc
|
||||
final FilterInfo filterInfo;
|
||||
final FilterFFIService _ffiService;
|
||||
final FilterListener _listener;
|
||||
final SingleSelectTypeOptionContext typeOptionContext;
|
||||
final SelectOptionFilterDelegate delegate;
|
||||
|
||||
SelectOptionFilterEditorBloc({required this.filterInfo})
|
||||
: _ffiService = FilterFFIService(viewId: filterInfo.viewId),
|
||||
SelectOptionFilterEditorBloc({
|
||||
required this.filterInfo,
|
||||
required this.delegate,
|
||||
}) : _ffiService = FilterFFIService(viewId: filterInfo.viewId),
|
||||
_listener = FilterListener(
|
||||
viewId: filterInfo.viewId,
|
||||
filterId: filterInfo.filter.id,
|
||||
),
|
||||
typeOptionContext = makeSingleSelectTypeOptionContext(
|
||||
gridId: filterInfo.viewId,
|
||||
fieldPB: filterInfo.field.field,
|
||||
),
|
||||
super(SelectOptionFilterEditorState.initial(filterInfo)) {
|
||||
on<SelectOptionFilterEditorEvent>(
|
||||
(event, emit) async {
|
||||
@@ -37,29 +33,29 @@ class SelectOptionFilterEditorBloc
|
||||
_startListening();
|
||||
_loadOptions();
|
||||
},
|
||||
updateCondition: (SelectOptionCondition condition) {
|
||||
updateCondition: (SelectOptionConditionPB condition) {
|
||||
_ffiService.insertSelectOptionFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
optionIds: state.filter.optionIds,
|
||||
fieldType: state.filterInfo.field.fieldType,
|
||||
fieldType: state.filterInfo.fieldInfo.fieldType,
|
||||
);
|
||||
},
|
||||
updateContent: (List<String> optionIds) {
|
||||
_ffiService.insertSelectOptionFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
optionIds: optionIds,
|
||||
fieldType: state.filterInfo.field.fieldType,
|
||||
fieldType: state.filterInfo.fieldInfo.fieldType,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.field.fieldType,
|
||||
fieldType: filterInfo.fieldInfo.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
@@ -92,21 +88,17 @@ class SelectOptionFilterEditorBloc
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
typeOptionContext.loadTypeOptionData(
|
||||
onCompleted: (value) {
|
||||
if (!isClosed) {
|
||||
String filterDesc = '';
|
||||
for (final option in value.options) {
|
||||
if (state.filter.optionIds.contains(option.id)) {
|
||||
filterDesc += "${option.name} ";
|
||||
}
|
||||
delegate.loadOptions().then((options) {
|
||||
if (!isClosed) {
|
||||
String filterDesc = '';
|
||||
for (final option in options) {
|
||||
if (state.filter.optionIds.contains(option.id)) {
|
||||
filterDesc += "${option.name} ";
|
||||
}
|
||||
add(SelectOptionFilterEditorEvent.updateFilterDescription(
|
||||
filterDesc));
|
||||
}
|
||||
},
|
||||
onError: (error) => Log.error(error),
|
||||
);
|
||||
add(SelectOptionFilterEditorEvent.updateFilterDescription(filterDesc));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -122,7 +114,7 @@ class SelectOptionFilterEditorEvent with _$SelectOptionFilterEditorEvent {
|
||||
const factory SelectOptionFilterEditorEvent.didReceiveFilter(
|
||||
FilterPB filter) = _DidReceiveFilter;
|
||||
const factory SelectOptionFilterEditorEvent.updateCondition(
|
||||
SelectOptionCondition condition) = _UpdateCondition;
|
||||
SelectOptionConditionPB condition) = _UpdateCondition;
|
||||
const factory SelectOptionFilterEditorEvent.updateContent(
|
||||
List<String> optionIds) = _UpdateContent;
|
||||
const factory SelectOptionFilterEditorEvent.updateFilterDescription(
|
||||
|
||||
+9
-17
@@ -1,8 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/application/field/type_option/type_option_context.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/builder.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/choicechip/select_option/select_option_loader.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -12,16 +10,13 @@ part 'select_option_filter_list_bloc.freezed.dart';
|
||||
|
||||
class SelectOptionFilterListBloc<T>
|
||||
extends Bloc<SelectOptionFilterListEvent, SelectOptionFilterListState> {
|
||||
final SingleSelectTypeOptionContext typeOptionContext;
|
||||
final SelectOptionFilterDelegate delegate;
|
||||
SelectOptionFilterListBloc({
|
||||
required String viewId,
|
||||
required FieldPB fieldPB,
|
||||
required this.delegate,
|
||||
required List<String> selectedOptionIds,
|
||||
}) : typeOptionContext = makeSingleSelectTypeOptionContext(
|
||||
gridId: viewId,
|
||||
fieldPB: fieldPB,
|
||||
),
|
||||
super(SelectOptionFilterListState.initial(selectedOptionIds)) {
|
||||
}) : super(SelectOptionFilterListState.initial(selectedOptionIds)) {
|
||||
on<SelectOptionFilterListEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
@@ -103,14 +98,11 @@ class SelectOptionFilterListBloc<T>
|
||||
}
|
||||
|
||||
void _loadOptions() {
|
||||
typeOptionContext.loadTypeOptionData(
|
||||
onCompleted: (value) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionFilterListEvent.didReceiveOptions(value.options));
|
||||
}
|
||||
},
|
||||
onError: (error) => Log.error(error),
|
||||
);
|
||||
delegate.loadOptions().then((options) {
|
||||
if (!isClosed) {
|
||||
add(SelectOptionFilterListEvent.didReceiveOptions(options));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _startListening() {}
|
||||
|
||||
@@ -28,10 +28,10 @@ class TextFilterEditorBloc
|
||||
initial: () async {
|
||||
_startListening();
|
||||
},
|
||||
updateCondition: (TextFilterCondition condition) {
|
||||
updateCondition: (TextFilterConditionPB condition) {
|
||||
_ffiService.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: condition,
|
||||
content: state.filter.content,
|
||||
);
|
||||
@@ -39,16 +39,16 @@ class TextFilterEditorBloc
|
||||
updateContent: (content) {
|
||||
_ffiService.insertTextFilter(
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
condition: state.filter.condition,
|
||||
content: content,
|
||||
);
|
||||
},
|
||||
delete: () {
|
||||
_ffiService.deleteFilter(
|
||||
fieldId: filterInfo.field.id,
|
||||
fieldId: filterInfo.fieldInfo.id,
|
||||
filterId: filterInfo.filter.id,
|
||||
fieldType: filterInfo.field.fieldType,
|
||||
fieldType: filterInfo.fieldInfo.fieldType,
|
||||
);
|
||||
},
|
||||
didReceiveFilter: (FilterPB filter) {
|
||||
@@ -88,7 +88,7 @@ class TextFilterEditorEvent with _$TextFilterEditorEvent {
|
||||
const factory TextFilterEditorEvent.didReceiveFilter(FilterPB filter) =
|
||||
_DidReceiveFilter;
|
||||
const factory TextFilterEditorEvent.updateCondition(
|
||||
TextFilterCondition condition) = _UpdateCondition;
|
||||
TextFilterConditionPB condition) = _UpdateCondition;
|
||||
const factory TextFilterEditorEvent.updateContent(String content) =
|
||||
_UpdateContent;
|
||||
const factory TextFilterEditorEvent.delete() = _Delete;
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'block/block_cache.dart';
|
||||
import 'field/field_controller.dart';
|
||||
import 'grid_data_controller.dart';
|
||||
import 'row/row_cache.dart';
|
||||
@@ -38,7 +37,6 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
},
|
||||
deleteRow: (rowInfo) async {
|
||||
final rowService = RowFFIService(
|
||||
blockId: rowInfo.rowPB.blockId,
|
||||
gridId: rowInfo.gridId,
|
||||
);
|
||||
await rowService.deleteRow(rowInfo.rowPB.id);
|
||||
@@ -70,8 +68,7 @@ class GridBloc extends Bloc<GridEvent, GridState> {
|
||||
}
|
||||
|
||||
GridRowCache? getRowCache(String blockId, String rowId) {
|
||||
final GridBlockCache? blockCache = gridController.blocks[blockId];
|
||||
return blockCache?.rowCache;
|
||||
return gridController.rowCache;
|
||||
}
|
||||
|
||||
void _startListening() {
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
|
||||
import 'dart:async';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'block/block_cache.dart';
|
||||
import 'view/grid_view_cache.dart';
|
||||
import 'field/field_controller.dart';
|
||||
import 'prelude.dart';
|
||||
import 'row/row_cache.dart';
|
||||
@@ -27,29 +23,25 @@ class GridController {
|
||||
final String gridId;
|
||||
final GridFFIService _gridFFIService;
|
||||
final GridFieldController fieldController;
|
||||
late GridViewCache _viewCache;
|
||||
|
||||
OnRowsChanged? _onRowChanged;
|
||||
OnGridChanged? _onGridChanged;
|
||||
|
||||
// Getters
|
||||
// key: the block id
|
||||
final LinkedHashMap<String, GridBlockCache> _blocks;
|
||||
UnmodifiableMapView<String, GridBlockCache> get blocks =>
|
||||
UnmodifiableMapView(_blocks);
|
||||
|
||||
List<RowInfo> get rowInfos {
|
||||
final List<RowInfo> rows = [];
|
||||
for (var block in _blocks.values) {
|
||||
rows.addAll(block.rows);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
List<RowInfo> get rowInfos => _viewCache.rowInfos;
|
||||
GridRowCache get rowCache => _viewCache.rowCache;
|
||||
|
||||
GridController({required ViewPB view})
|
||||
: gridId = view.id,
|
||||
// ignore: prefer_collection_literals
|
||||
_blocks = LinkedHashMap(),
|
||||
_gridFFIService = GridFFIService(gridId: view.id),
|
||||
fieldController = GridFieldController(gridId: view.id);
|
||||
fieldController = GridFieldController(gridId: view.id) {
|
||||
_viewCache = GridViewCache(
|
||||
gridId: gridId,
|
||||
fieldController: fieldController,
|
||||
);
|
||||
_viewCache.addListener(onRowsChanged: (reason) {
|
||||
_onRowChanged?.call(rowInfos, reason);
|
||||
});
|
||||
}
|
||||
|
||||
void addListener({
|
||||
OnGridChanged? onGridChanged,
|
||||
@@ -71,9 +63,8 @@ class GridController {
|
||||
return _gridFFIService.openGrid().then((result) {
|
||||
return result.fold(
|
||||
(grid) async {
|
||||
_initialBlocks(grid.blocks);
|
||||
_onGridChanged?.call(grid);
|
||||
|
||||
_viewCache.rowCache.initializeRows(grid.rows);
|
||||
final result = await fieldController.loadFields(
|
||||
fieldIds: grid.fields,
|
||||
);
|
||||
@@ -91,30 +82,5 @@ class GridController {
|
||||
Future<void> dispose() async {
|
||||
await _gridFFIService.closeGrid();
|
||||
await fieldController.dispose();
|
||||
|
||||
for (final blockCache in _blocks.values) {
|
||||
blockCache.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
void _initialBlocks(List<BlockPB> blocks) {
|
||||
for (final block in blocks) {
|
||||
if (_blocks[block.id] != null) {
|
||||
Log.warn("Initial duplicate block's cache: ${block.id}");
|
||||
return;
|
||||
}
|
||||
|
||||
final cache = GridBlockCache(
|
||||
gridId: gridId,
|
||||
block: block,
|
||||
fieldController: fieldController,
|
||||
);
|
||||
|
||||
cache.addListener(onRowsChanged: (reason) {
|
||||
_onRowChanged?.call(rowInfos, reason);
|
||||
});
|
||||
|
||||
_blocks[block.id] = cache;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/group.pb.dart';
|
||||
|
||||
@@ -15,10 +15,7 @@ class RowActionSheetBloc
|
||||
final RowFFIService _rowService;
|
||||
|
||||
RowActionSheetBloc({required RowInfo rowInfo})
|
||||
: _rowService = RowFFIService(
|
||||
gridId: rowInfo.gridId,
|
||||
blockId: rowInfo.rowPB.blockId,
|
||||
),
|
||||
: _rowService = RowFFIService(gridId: rowInfo.gridId),
|
||||
super(RowActionSheetState.initial(rowInfo)) {
|
||||
on<RowActionSheetEvent>(
|
||||
(event, emit) async {
|
||||
|
||||
@@ -18,10 +18,7 @@ class RowBloc extends Bloc<RowEvent, RowState> {
|
||||
RowBloc({
|
||||
required RowInfo rowInfo,
|
||||
required GridRowDataController dataController,
|
||||
}) : _rowService = RowFFIService(
|
||||
gridId: rowInfo.gridId,
|
||||
blockId: rowInfo.rowPB.blockId,
|
||||
),
|
||||
}) : _rowService = RowFFIService(gridId: rowInfo.gridId),
|
||||
_dataController = dataController,
|
||||
super(RowState.initial(rowInfo, dataController.loadData())) {
|
||||
on<RowEvent>(
|
||||
|
||||
@@ -3,8 +3,7 @@ import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_servic
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/protobuf.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@@ -27,7 +26,7 @@ abstract class IGridRowFieldNotifier {
|
||||
|
||||
class GridRowCache {
|
||||
final String gridId;
|
||||
final BlockPB block;
|
||||
final List<RowPB> rows;
|
||||
|
||||
/// _rows containers the current block's rows
|
||||
/// Use List to reverse the order of the GridRow.
|
||||
@@ -46,7 +45,7 @@ class GridRowCache {
|
||||
|
||||
GridRowCache({
|
||||
required this.gridId,
|
||||
required this.block,
|
||||
required this.rows,
|
||||
required IGridRowFieldNotifier notifier,
|
||||
}) : _cellCache = GridCellCache(gridId: gridId),
|
||||
_rowChangeReasonNotifier = _RowChangesetNotifier(),
|
||||
@@ -56,8 +55,10 @@ class GridRowCache {
|
||||
.receive(const RowsChangedReason.fieldDidChange()));
|
||||
notifier.onRowFieldChanged(
|
||||
(field) => _cellCache.removeCellWithFieldId(field.id));
|
||||
}
|
||||
|
||||
for (final row in block.rows) {
|
||||
void initializeRows(List<RowPB> rows) {
|
||||
for (final row in rows) {
|
||||
final rowInfo = buildGridRow(row);
|
||||
_rowList.add(rowInfo);
|
||||
}
|
||||
@@ -69,10 +70,13 @@ class GridRowCache {
|
||||
await _cellCache.dispose();
|
||||
}
|
||||
|
||||
void applyChangesets(GridBlockChangesetPB changeset) {
|
||||
void applyRowsChanged(GridViewRowsChangesetPB changeset) {
|
||||
_deleteRows(changeset.deletedRows);
|
||||
_insertRows(changeset.insertedRows);
|
||||
_updateRows(changeset.updatedRows);
|
||||
}
|
||||
|
||||
void applyRowsVisibility(GridRowsVisibilityChangesetPB changeset) {
|
||||
_hideRows(changeset.invisibleRows);
|
||||
_showRows(changeset.visibleRows);
|
||||
}
|
||||
@@ -97,11 +101,22 @@ class GridRowCache {
|
||||
}
|
||||
}
|
||||
|
||||
void _updateRows(List<RowPB> updatedRows) {
|
||||
void _updateRows(List<UpdatedRowPB> updatedRows) {
|
||||
if (updatedRows.isEmpty) return;
|
||||
List<RowPB> rowPBs = [];
|
||||
for (final updatedRow in updatedRows) {
|
||||
for (final fieldId in updatedRow.fieldIds) {
|
||||
final key = GridCellCacheKey(
|
||||
fieldId: fieldId,
|
||||
rowId: updatedRow.row.id,
|
||||
);
|
||||
_cellCache.remove(key);
|
||||
}
|
||||
rowPBs.add(updatedRow.row);
|
||||
}
|
||||
|
||||
final updatedIndexs =
|
||||
_rowList.updateRows(updatedRows, (rowPB) => buildGridRow(rowPB));
|
||||
_rowList.updateRows(rowPBs, (rowPB) => buildGridRow(rowPB));
|
||||
if (updatedIndexs.isNotEmpty) {
|
||||
_rowChangeReasonNotifier.receive(RowsChangedReason.update(updatedIndexs));
|
||||
}
|
||||
@@ -181,7 +196,6 @@ class GridRowCache {
|
||||
Future<void> _loadRow(String rowId) async {
|
||||
final payload = RowIdPB.create()
|
||||
..gridId = gridId
|
||||
..blockId = block.id
|
||||
..rowId = rowId;
|
||||
|
||||
final result = await GridEventGetRow(payload).send();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
|
||||
import 'row_cache.dart';
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:app_flowy/core/grid_notification.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
|
||||
typedef UpdateRowNotifiedValue = Either<RowPB, FlowyError>;
|
||||
typedef UpdateFieldNotifiedValue = Either<List<FieldPB>, FlowyError>;
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_sdk/dispatch/dispatch.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/grid_entities.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/group_changeset.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/row_entities.pb.dart';
|
||||
|
||||
class RowFFIService {
|
||||
final String gridId;
|
||||
final String blockId;
|
||||
|
||||
RowFFIService({
|
||||
required this.gridId,
|
||||
required this.blockId,
|
||||
});
|
||||
|
||||
Future<Either<RowPB, FlowyError>> createRow(String rowId) {
|
||||
@@ -26,7 +23,6 @@ class RowFFIService {
|
||||
Future<Either<OptionalRowPB, FlowyError>> getRow(String rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..gridId = gridId
|
||||
..blockId = blockId
|
||||
..rowId = rowId;
|
||||
|
||||
return GridEventGetRow(payload).send();
|
||||
@@ -35,7 +31,6 @@ class RowFFIService {
|
||||
Future<Either<Unit, FlowyError>> deleteRow(String rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..gridId = gridId
|
||||
..blockId = blockId
|
||||
..rowId = rowId;
|
||||
|
||||
return GridEventDeleteRow(payload).send();
|
||||
@@ -44,7 +39,6 @@ class RowFFIService {
|
||||
Future<Either<Unit, FlowyError>> duplicateRow(String rowId) {
|
||||
final payload = RowIdPB.create()
|
||||
..gridId = gridId
|
||||
..blockId = blockId
|
||||
..rowId = rowId;
|
||||
|
||||
return GridEventDuplicateRow(payload).send();
|
||||
|
||||
+22
-18
@@ -1,43 +1,47 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/plugins/grid/application/view/grid_view_listener.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/block_entities.pb.dart';
|
||||
|
||||
import '../field/field_controller.dart';
|
||||
import '../row/row_cache.dart';
|
||||
import 'block_listener.dart';
|
||||
|
||||
/// Read https://appflowy.gitbook.io/docs/essential-documentation/contribute-to-appflowy/architecture/frontend/grid for more information
|
||||
class GridBlockCache {
|
||||
class GridViewCache {
|
||||
final String gridId;
|
||||
final BlockPB block;
|
||||
late GridRowCache _rowCache;
|
||||
late GridBlockListener _listener;
|
||||
final GridViewListener _gridViewListener;
|
||||
|
||||
List<RowInfo> get rows => _rowCache.visibleRows;
|
||||
List<RowInfo> get rowInfos => _rowCache.visibleRows;
|
||||
GridRowCache get rowCache => _rowCache;
|
||||
|
||||
GridBlockCache({
|
||||
GridViewCache({
|
||||
required this.gridId,
|
||||
required this.block,
|
||||
required GridFieldController fieldController,
|
||||
}) {
|
||||
}) : _gridViewListener = GridViewListener(viewId: gridId) {
|
||||
_rowCache = GridRowCache(
|
||||
gridId: gridId,
|
||||
block: block,
|
||||
rows: [],
|
||||
notifier: GridRowFieldNotifierImpl(fieldController),
|
||||
);
|
||||
|
||||
_listener = GridBlockListener(blockId: block.id);
|
||||
_listener.start((result) {
|
||||
result.fold(
|
||||
(changeset) => _rowCache.applyChangesets(changeset),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
_gridViewListener.start(
|
||||
onRowsChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) => _rowCache.applyRowsChanged(changeset),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
onRowsVisibilityChanged: (result) {
|
||||
result.fold(
|
||||
(changeset) => _rowCache.applyRowsVisibility(changeset),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _listener.stop();
|
||||
await _gridViewListener.stop();
|
||||
await _rowCache.dispose();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'package:app_flowy/core/grid_notification.dart';
|
||||
import 'package:dartz/dartz.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/dart_notification.pb.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/view_entities.pb.dart';
|
||||
|
||||
typedef GridRowsVisibilityNotifierValue
|
||||
= Either<GridRowsVisibilityChangesetPB, FlowyError>;
|
||||
|
||||
typedef GridViewRowsNotifierValue = Either<GridViewRowsChangesetPB, FlowyError>;
|
||||
|
||||
class GridViewListener {
|
||||
final String viewId;
|
||||
PublishNotifier<GridViewRowsNotifierValue>? _rowsNotifier = PublishNotifier();
|
||||
PublishNotifier<GridRowsVisibilityNotifierValue>? _rowsVisibilityNotifier =
|
||||
PublishNotifier();
|
||||
|
||||
GridNotificationListener? _listener;
|
||||
GridViewListener({required this.viewId});
|
||||
|
||||
void start({
|
||||
required void Function(GridViewRowsNotifierValue) onRowsChanged,
|
||||
required void Function(GridRowsVisibilityNotifierValue)
|
||||
onRowsVisibilityChanged,
|
||||
}) {
|
||||
if (_listener != null) {
|
||||
_listener?.stop();
|
||||
}
|
||||
|
||||
_listener = GridNotificationListener(
|
||||
objectId: viewId,
|
||||
handler: _handler,
|
||||
);
|
||||
|
||||
_rowsNotifier?.addPublishListener(onRowsChanged);
|
||||
_rowsVisibilityNotifier?.addPublishListener(onRowsVisibilityChanged);
|
||||
}
|
||||
|
||||
void _handler(GridDartNotification ty, Either<Uint8List, FlowyError> result) {
|
||||
switch (ty) {
|
||||
case GridDartNotification.DidUpdateGridViewRowsVisibility:
|
||||
result.fold(
|
||||
(payload) => _rowsVisibilityNotifier?.value =
|
||||
left(GridRowsVisibilityChangesetPB.fromBuffer(payload)),
|
||||
(error) => _rowsVisibilityNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
case GridDartNotification.DidUpdateGridViewRows:
|
||||
result.fold(
|
||||
(payload) => _rowsNotifier?.value =
|
||||
left(GridViewRowsChangesetPB.fromBuffer(payload)),
|
||||
(error) => _rowsNotifier?.value = right(error),
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _listener?.stop();
|
||||
_rowsVisibilityNotifier?.dispose();
|
||||
_rowsVisibilityNotifier = null;
|
||||
|
||||
_rowsNotifier?.dispose();
|
||||
_rowsNotifier = null;
|
||||
}
|
||||
}
|
||||
@@ -338,12 +338,11 @@ class RowCountBadge extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
FlowyText.regular(
|
||||
FlowyText.medium(
|
||||
'${LocaleKeys.grid_row_count.tr()} : ',
|
||||
fontSize: 13,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
FlowyText.regular(rowCount.toString(), fontSize: 13),
|
||||
FlowyText.medium(rowCount.toString()),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -73,7 +72,7 @@ class _PrimaryCellAccessoryState extends State<PrimaryCellAccessory>
|
||||
} else {
|
||||
return Tooltip(
|
||||
message: LocaleKeys.tooltip_openAsPage.tr(),
|
||||
textStyle: TextStyles.caption.textColor(Colors.white),
|
||||
textStyle: AFThemeExtension.of(context).caption.textColor(Colors.white),
|
||||
child: svgWidget(
|
||||
"grid/expander",
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
|
||||
import 'cell_accessory.dart';
|
||||
import 'cell_shortcuts.dart';
|
||||
import 'checkbox_cell.dart';
|
||||
import 'checklist_cell/checklist_cell.dart';
|
||||
import 'date_cell/date_cell.dart';
|
||||
import 'number_cell.dart';
|
||||
import 'select_option_cell/select_option_cell.dart';
|
||||
@@ -55,6 +56,11 @@ class GridCellBuilder {
|
||||
style: style,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Checklist:
|
||||
return GridChecklistCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
key: key,
|
||||
);
|
||||
case FieldType.Number:
|
||||
return GridNumberCell(
|
||||
cellControllerBuilder: cellControllerBuilder,
|
||||
|
||||
@@ -30,7 +30,7 @@ class CellContainer extends StatelessWidget {
|
||||
cellStateNotifier!..onEnter = rowStateNotifier.onEnter,
|
||||
child: Selector<_CellContainerNotifier, bool>(
|
||||
selector: (context, notifier) => notifier.isFocus,
|
||||
builder: (context, isFocus, _) {
|
||||
builder: (privderContext, isFocus, _) {
|
||||
Widget container = Center(child: GridCellShortcuts(child: child));
|
||||
|
||||
if (accessoryBuilder != null) {
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.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 '../cell_builder.dart';
|
||||
import 'checklist_cell_editor.dart';
|
||||
import 'checklist_prograss_bar.dart';
|
||||
|
||||
class GridChecklistCell extends GridCellWidget {
|
||||
final GridCellControllerBuilder cellControllerBuilder;
|
||||
GridChecklistCell({required this.cellControllerBuilder, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
GridChecklistCellState createState() => GridChecklistCellState();
|
||||
}
|
||||
|
||||
class GridChecklistCellState extends State<GridChecklistCell> {
|
||||
late PopoverController _popover;
|
||||
late ChecklistCellBloc _cellBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_popover = PopoverController();
|
||||
final cellController =
|
||||
widget.cellControllerBuilder.build() as GridChecklistCellController;
|
||||
_cellBloc = ChecklistCellBloc(cellController: cellController);
|
||||
_cellBloc.add(const ChecklistCellEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: _cellBloc,
|
||||
child: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_wrapPopover(const ChecklistProgressBar()),
|
||||
InkWell(onTap: () => _popover.show()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPopover(Widget child) {
|
||||
return AppFlowyPopover(
|
||||
controller: _popover,
|
||||
constraints: BoxConstraints.loose(const Size(260, 400)),
|
||||
direction: PopoverDirection.bottomWithLeftAligned,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
popupBuilder: (BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
widget.onCellEditing.value = true;
|
||||
});
|
||||
return GridChecklistCellEditor(
|
||||
cellController: widget.cellControllerBuilder.build()
|
||||
as GridChecklistCellController,
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onCellEditing.value = false,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistProgressBar extends StatelessWidget {
|
||||
const ChecklistProgressBar({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ChecklistCellBloc, ChecklistCellState>(
|
||||
builder: (context, state) {
|
||||
return ChecklistPrograssBar(
|
||||
percent: state.percent,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_editor_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/checklist_cell/checklist_prograss_bar.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/type_option/select_option_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class GridChecklistCellEditor extends StatefulWidget {
|
||||
final GridChecklistCellController cellController;
|
||||
const GridChecklistCellEditor({required this.cellController, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<GridChecklistCellEditor> createState() =>
|
||||
_GridChecklistCellEditorState();
|
||||
}
|
||||
|
||||
class _GridChecklistCellEditorState extends State<GridChecklistCellEditor> {
|
||||
late ChecklistCellEditorBloc bloc;
|
||||
late PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
bloc = ChecklistCellEditorBloc(cellController: widget.cellController);
|
||||
bloc.add(const ChecklistCellEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> slivers = [
|
||||
const SliverChecklistPrograssBar(),
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: ListView.separated(
|
||||
controller: ScrollController(),
|
||||
shrinkWrap: true,
|
||||
itemCount: state.allOptions.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return _ChecklistOptionCell(
|
||||
option: state.allOptions[index],
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return ScrollConfiguration(
|
||||
behavior: const ScrollBehavior().copyWith(scrollbars: false),
|
||||
child: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: slivers,
|
||||
controller: ScrollController(),
|
||||
physics: StyledScrollPhysics(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ChecklistOptionCell extends StatefulWidget {
|
||||
final ChecklistSelectOption option;
|
||||
final PopoverMutex popoverMutex;
|
||||
const _ChecklistOptionCell({
|
||||
required this.option,
|
||||
required this.popoverMutex,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_ChecklistOptionCell> createState() => _ChecklistOptionCellState();
|
||||
}
|
||||
|
||||
class _ChecklistOptionCellState extends State<_ChecklistOptionCell> {
|
||||
late PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final icon = widget.option.isSelected
|
||||
? svgWidget('editor/editor_check')
|
||||
: svgWidget('editor/editor_uncheck');
|
||||
return _wrapPopover(
|
||||
SizedBox(
|
||||
height: GridSize.typeOptionItemHeight,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyButton(
|
||||
text: FlowyText(widget.option.data.name),
|
||||
leftIcon: icon,
|
||||
onTap: () => context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.selectOption(widget.option)),
|
||||
),
|
||||
),
|
||||
_disclosureButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _disclosureButton() {
|
||||
return FlowyIconButton(
|
||||
width: 20,
|
||||
onPressed: () => _popoverController.show(),
|
||||
iconPadding: const EdgeInsets.fromLTRB(2, 2, 2, 2),
|
||||
icon: svgWidget(
|
||||
"editor/details",
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapPopover(Widget child) {
|
||||
return AppFlowyPopover(
|
||||
controller: _popoverController,
|
||||
offset: const Offset(20, 0),
|
||||
asBarrier: true,
|
||||
constraints: BoxConstraints.loose(const Size(200, 300)),
|
||||
mutex: widget.popoverMutex,
|
||||
triggerActions: PopoverTriggerFlags.none,
|
||||
child: child,
|
||||
popupBuilder: (BuildContext popoverContext) {
|
||||
return SelectOptionTypeOptionEditor(
|
||||
option: widget.option.data,
|
||||
onDeleted: () {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.deleteOption(widget.option.data),
|
||||
);
|
||||
|
||||
_popoverController.close();
|
||||
},
|
||||
onUpdated: (updatedOption) {
|
||||
context.read<ChecklistCellEditorBloc>().add(
|
||||
ChecklistCellEditorEvent.updateOption(updatedOption),
|
||||
);
|
||||
},
|
||||
showOptions: false,
|
||||
autoFocus: false,
|
||||
// Use ValueKey to refresh the UI, otherwise, it will remain the old value.
|
||||
key: ValueKey(
|
||||
widget.option.data.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/checklist_cell_editor_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
|
||||
class ChecklistPrograssBar extends StatelessWidget {
|
||||
final double percent;
|
||||
const ChecklistPrograssBar({required this.percent, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LinearPercentIndicator(
|
||||
lineHeight: 10.0,
|
||||
percent: percent,
|
||||
padding: EdgeInsets.zero,
|
||||
progressColor: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor: AFThemeExtension.of(context).tint9,
|
||||
barRadius: const Radius.circular(5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SliverChecklistPrograssBar extends StatelessWidget {
|
||||
const SliverChecklistPrograssBar({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverChecklistPrograssBarDelegate(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverChecklistPrograssBarDelegate
|
||||
extends SliverPersistentHeaderDelegate {
|
||||
_SliverChecklistPrograssBarDelegate();
|
||||
|
||||
double fixHeight = 60;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return BlocBuilder<ChecklistCellEditorBloc, ChecklistCellEditorState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.background,
|
||||
padding: GridSize.typeOptionContentInsets,
|
||||
child: Column(
|
||||
children: [
|
||||
FlowyTextField(
|
||||
autoClearWhenDone: true,
|
||||
hintText: LocaleKeys.grid_checklist_panelTitle.tr(),
|
||||
onChanged: (text) {
|
||||
context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (text) {
|
||||
context
|
||||
.read<ChecklistCellEditorBloc>()
|
||||
.add(ChecklistCellEditorEvent.newOption(text));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 6.0),
|
||||
child: ChecklistPrograssBar(percent: state.percent),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => fixHeight;
|
||||
|
||||
@override
|
||||
double get minExtent => fixHeight;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
|
||||
+1
-2
@@ -6,12 +6,11 @@ import 'package:app_flowy/workspace/presentation/widgets/toggle/toggle_style.dar
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:dartz/dartz.dart' show Either;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
import 'cell_builder.dart';
|
||||
@@ -57,7 +54,7 @@ class _NumberCellState extends GridFocusNodeCellState<GridNumberCell> {
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
onSubmitted: (_) => focusNode.unfocus(),
|
||||
maxLines: 1,
|
||||
style: TextStyles.body1.size(FontSizes.s14),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
|
||||
+24
-28
@@ -1,4 +1,5 @@
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
@@ -87,17 +88,13 @@ class SelectOptionTag extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ChoiceChip(
|
||||
pressElevation: 1,
|
||||
label: FlowyText.medium(
|
||||
name,
|
||||
overflow: TextOverflow.clip,
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: Corners.s6Border,
|
||||
),
|
||||
selectedColor: color,
|
||||
backgroundColor: color,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 6),
|
||||
selected: true,
|
||||
onSelected: (_) => onSelected?.call(),
|
||||
child: FlowyText.medium(name, overflow: TextOverflow.ellipsis),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -115,30 +112,29 @@ class SelectOptionTagCell extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
FlowyHover(
|
||||
child: InkWell(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
SelectOptionTag.fromOption(
|
||||
return FlowyHover(
|
||||
child: InkWell(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5.0),
|
||||
child: SelectOptionTag.fromOption(
|
||||
context: context,
|
||||
option: option,
|
||||
onSelected: () => onSelected(option),
|
||||
),
|
||||
const Spacer(),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () => onSelected(option),
|
||||
),
|
||||
...children,
|
||||
],
|
||||
),
|
||||
],
|
||||
onTap: () => onSelected(option),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+6
-8
@@ -3,7 +3,6 @@ import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:flowy_sdk/log.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
@@ -169,10 +168,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
alignment: AlignmentDirectional.center,
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: _wrapPopover(child),
|
||||
),
|
||||
_wrapPopover(child),
|
||||
InkWell(onTap: () => _popover.show()),
|
||||
],
|
||||
);
|
||||
@@ -197,7 +193,10 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
);
|
||||
},
|
||||
onClose: () => widget.onFocus?.call(false),
|
||||
child: child,
|
||||
child: Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -206,7 +205,6 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
if (widget.selectOptions.isEmpty && widget.cellStyle != null) {
|
||||
child = FlowyText.medium(
|
||||
widget.cellStyle!.placeholder,
|
||||
fontSize: 14,
|
||||
color: Theme.of(context).hintColor,
|
||||
);
|
||||
} else {
|
||||
@@ -223,7 +221,7 @@ class _SelectOptionWrapState extends State<SelectOptionWrap> {
|
||||
).toList();
|
||||
|
||||
child = Wrap(
|
||||
runSpacing: 2,
|
||||
runSpacing: 4,
|
||||
children: children,
|
||||
);
|
||||
}
|
||||
|
||||
+55
-52
@@ -2,14 +2,13 @@ import 'dart:collection';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/select_option_editor_bloc.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -53,21 +52,24 @@ class _SelectOptionCellEditorState extends State<SelectOptionCellEditor> {
|
||||
)..add(const SelectOptionEditorEvent.initial()),
|
||||
child: BlocBuilder<SelectOptionCellEditorBloc, SelectOptionEditorState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [
|
||||
_TextField(popoverMutex: popoverMutex),
|
||||
const TypeOptionSeparator(),
|
||||
const _Title(),
|
||||
_OptionList(popoverMutex: popoverMutex),
|
||||
];
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: CustomScrollView(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: _TextField(popoverMutex: popoverMutex),
|
||||
),
|
||||
const SliverToBoxAdapter(child: TypeOptionSeparator()),
|
||||
const SliverToBoxAdapter(child: VSpace(6)),
|
||||
const SliverToBoxAdapter(child: _Title()),
|
||||
SliverToBoxAdapter(
|
||||
child: _OptionList(popoverMutex: popoverMutex),
|
||||
),
|
||||
],
|
||||
itemCount: children.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return children[index];
|
||||
},
|
||||
separatorBuilder: (BuildContext context, int index) {
|
||||
return VSpace(GridSize.typeOptionSeparatorHeight);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -143,35 +145,32 @@ class _TextField extends StatelessWidget {
|
||||
key: (option) => option.name,
|
||||
value: (option) => option);
|
||||
|
||||
return SizedBox(
|
||||
height: 52,
|
||||
child: SelectOptionTextField(
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
maxLength: 30,
|
||||
tagController: _tagController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (tagName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.selectMultipleOptions(
|
||||
tagNames,
|
||||
remainder,
|
||||
));
|
||||
},
|
||||
),
|
||||
return SelectOptionTextField(
|
||||
options: state.options,
|
||||
selectedOptionMap: optionMap,
|
||||
distanceToText: _editorPanelWidth * 0.7,
|
||||
maxLength: 30,
|
||||
tagController: _tagController,
|
||||
textSeparators: const [','],
|
||||
onClick: () => popoverMutex.close(),
|
||||
newText: (text) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.filterOption(text));
|
||||
},
|
||||
onSubmitted: (tagName) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.trySelectOption(tagName));
|
||||
},
|
||||
onPaste: (tagNames, remainder) {
|
||||
context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.selectMultipleOptions(
|
||||
tagNames,
|
||||
remainder,
|
||||
));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -209,12 +208,17 @@ class _CreateOptionCell extends StatelessWidget {
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(10),
|
||||
SelectOptionTag(
|
||||
name: name,
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
onSelected: () => context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(name)),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: SelectOptionTag(
|
||||
name: name,
|
||||
color: AFThemeExtension.of(context).lightGreyHover,
|
||||
onSelected: () => context
|
||||
.read<SelectOptionCellEditorBloc>()
|
||||
.add(SelectOptionEditorEvent.newOption(name)),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -271,14 +275,13 @@ class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
children: [
|
||||
if (widget.isSelected)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
child: svgWidget("grid/checkmark"),
|
||||
),
|
||||
FlowyIconButton(
|
||||
width: 30,
|
||||
onPressed: () => _popoverController.show(),
|
||||
hoverColor: Colors.transparent,
|
||||
iconPadding: const EdgeInsets.fromLTRB(4, 4, 4, 4),
|
||||
iconPadding: const EdgeInsets.symmetric(horizontal: 6.0),
|
||||
icon: svgWidget(
|
||||
"editor/details",
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
|
||||
+7
-6
@@ -1,7 +1,6 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -49,7 +48,6 @@ class SelectOptionTextField extends StatefulWidget {
|
||||
class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
late FocusNode focusNode;
|
||||
late TextEditingController controller;
|
||||
var textLength = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -64,7 +62,7 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
|
||||
String? _suffixText() {
|
||||
if (widget.maxLength != null) {
|
||||
return '${textLength.toString()}/${widget.maxLength.toString()}';
|
||||
return ' ${controller.text.length}/${widget.maxLength}';
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
@@ -92,7 +90,6 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
focusNode: focusNode,
|
||||
onTap: widget.onClick,
|
||||
onChanged: (text) {
|
||||
textLength = text.length;
|
||||
if (onChanged != null) {
|
||||
onChanged(text);
|
||||
}
|
||||
@@ -112,11 +109,11 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
maxLength: widget.maxLength,
|
||||
maxLengthEnforcement:
|
||||
MaxLengthEnforcement.truncateAfterCompositionEnds,
|
||||
style: TextStyles.body1.size(FontSizes.s14),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1.0,
|
||||
),
|
||||
borderRadius: Corners.s10Border,
|
||||
@@ -124,6 +121,10 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
isDense: true,
|
||||
prefixIcon: _renderTags(context, sc),
|
||||
hintText: LocaleKeys.grid_selectOption_searchOption.tr(),
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodySmall!
|
||||
.textColor(Theme.of(context).hintColor),
|
||||
suffixText: _suffixText(),
|
||||
counterText: "",
|
||||
prefixIconConstraints:
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/prelude.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/startup/startup.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import '../../layout/sizes.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
import 'cell_builder.dart';
|
||||
|
||||
class GridTextCellStyle extends GridCellStyle {
|
||||
@@ -40,7 +37,6 @@ class GridTextCell extends GridCellWidget {
|
||||
class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
late TextCellBloc _cellBloc;
|
||||
late TextEditingController _controller;
|
||||
Timer? _delayOperation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -69,10 +65,8 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: focusNode,
|
||||
onChanged: (value) => focusChanged(),
|
||||
onEditingComplete: () => focusNode.unfocus(),
|
||||
maxLines: null,
|
||||
style: TextStyles.body1.size(FontSizes.s14),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: GridSize.cellContentInsets.top,
|
||||
@@ -90,24 +84,10 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
_delayOperation = null;
|
||||
_cellBloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> focusChanged() async {
|
||||
if (mounted) {
|
||||
_delayOperation?.cancel();
|
||||
_delayOperation = Timer(const Duration(milliseconds: 30), () {
|
||||
if (_cellBloc.isClosed == false &&
|
||||
_controller.text != _cellBloc.state.content) {
|
||||
_cellBloc.add(TextCellEvent.updateText(_controller.text));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String? onCopy() => _cellBloc.state.content;
|
||||
|
||||
@@ -115,4 +95,12 @@ class _GridTextCellState extends GridFocusNodeCellState<GridTextCell> {
|
||||
void onInsert(String value) {
|
||||
_cellBloc.add(TextCellEvent.updateText(value));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> focusChanged() {
|
||||
_cellBloc.add(
|
||||
TextCellEvent.updateText(_controller.text),
|
||||
);
|
||||
return super.focusChanged();
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -1,12 +1,9 @@
|
||||
import 'package:app_flowy/plugins/grid/application/cell/cell_service/cell_service.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/cell/url_cell_editor_bloc.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
|
||||
class URLCellEditor extends StatefulWidget {
|
||||
final GridURLCellController cellController;
|
||||
@@ -45,7 +42,7 @@ class _URLCellEditorState extends State<URLCellEditor> {
|
||||
controller: _controller,
|
||||
onChanged: (value) => focusChanged(),
|
||||
maxLines: null,
|
||||
style: TextStyles.body1.size(FontSizes.s14),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
decoration: const InputDecoration(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
border: InputBorder.none,
|
||||
|
||||
+4
-12
@@ -5,14 +5,11 @@ import 'package:app_flowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/prelude.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../layout/sizes.dart';
|
||||
import '../cell_accessory.dart';
|
||||
@@ -119,15 +116,10 @@ class _GridURLCellState extends GridCellState<GridURLCell> {
|
||||
builder: (context, state) {
|
||||
final richText = Padding(
|
||||
padding: GridSize.cellContentInsets,
|
||||
child: RichText(
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(
|
||||
text: state.content,
|
||||
style: TextStyles.general(
|
||||
fontSize: FontSizes.s14,
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
).underline,
|
||||
),
|
||||
child: FlowyText.medium(
|
||||
state.content,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import 'package:flowy_infra/text_style.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:textstyle_extensions/textstyle_extensions.dart';
|
||||
|
||||
class InputTextField extends StatefulWidget {
|
||||
final void Function(String)? onDone;
|
||||
@@ -47,15 +45,13 @@ class _InputTextFieldState extends State<InputTextField> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final height = widget.maxLength == null ? 36.0 : 56.0;
|
||||
|
||||
return RoundedInputField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
autoFocus: true,
|
||||
height: height,
|
||||
height: 36.0,
|
||||
maxLength: widget.maxLength,
|
||||
style: TextStyles.body1.size(13),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
onChanged: (text) {
|
||||
if (widget.onChanged != null) {
|
||||
widget.onChanged!(text);
|
||||
|
||||
+10
-11
@@ -8,7 +8,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checkbox_filter.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -107,9 +106,9 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
FlowyText(state.filterInfo.field.name),
|
||||
FlowyText(state.filterInfo.fieldInfo.name),
|
||||
const HSpace(4),
|
||||
CheckboxFilterConditionList(
|
||||
CheckboxFilterConditionPBList(
|
||||
filterInfo: state.filterInfo,
|
||||
popoverMutex: popoverMutex,
|
||||
onCondition: (condition) {
|
||||
@@ -137,11 +136,11 @@ class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
|
||||
}
|
||||
}
|
||||
|
||||
class CheckboxFilterConditionList extends StatelessWidget {
|
||||
class CheckboxFilterConditionPBList extends StatelessWidget {
|
||||
final FilterInfo filterInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
final Function(CheckboxFilterCondition) onCondition;
|
||||
const CheckboxFilterConditionList({
|
||||
final Function(CheckboxFilterConditionPB) onCondition;
|
||||
const CheckboxFilterConditionPBList({
|
||||
required this.filterInfo,
|
||||
required this.popoverMutex,
|
||||
required this.onCondition,
|
||||
@@ -155,7 +154,7 @@ class CheckboxFilterConditionList extends StatelessWidget {
|
||||
asBarrier: true,
|
||||
mutex: popoverMutex,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: CheckboxFilterCondition.values
|
||||
actions: CheckboxFilterConditionPB.values
|
||||
.map(
|
||||
(action) => ConditionWrapper(
|
||||
action,
|
||||
@@ -178,7 +177,7 @@ class CheckboxFilterConditionList extends StatelessWidget {
|
||||
}
|
||||
|
||||
class ConditionWrapper extends ActionCell {
|
||||
final CheckboxFilterCondition inner;
|
||||
final CheckboxFilterConditionPB inner;
|
||||
final bool isSelected;
|
||||
|
||||
ConditionWrapper(this.inner, this.isSelected);
|
||||
@@ -196,12 +195,12 @@ class ConditionWrapper extends ActionCell {
|
||||
String get name => inner.filterName;
|
||||
}
|
||||
|
||||
extension TextFilterConditionExtension on CheckboxFilterCondition {
|
||||
extension TextFilterConditionPBExtension on CheckboxFilterConditionPB {
|
||||
String get filterName {
|
||||
switch (this) {
|
||||
case CheckboxFilterCondition.IsChecked:
|
||||
case CheckboxFilterConditionPB.IsChecked:
|
||||
return LocaleKeys.grid_checkboxFilter_isChecked.tr();
|
||||
case CheckboxFilterCondition.IsUnChecked:
|
||||
case CheckboxFilterConditionPB.IsUnChecked:
|
||||
return LocaleKeys.grid_checkboxFilter_isUnchecked.tr();
|
||||
default:
|
||||
return "";
|
||||
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
import 'package:app_flowy/generated/locale_keys.g.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/filter/checklist_filter_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/condition_button.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/disclosure_button.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:app_flowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/checklist_filter.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../choicechip.dart';
|
||||
|
||||
class ChecklistFilterChoicechip extends StatefulWidget {
|
||||
final FilterInfo filterInfo;
|
||||
const ChecklistFilterChoicechip({required this.filterInfo, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<ChecklistFilterChoicechip> createState() =>
|
||||
_ChecklistFilterChoicechipState();
|
||||
}
|
||||
|
||||
class _ChecklistFilterChoicechipState extends State<ChecklistFilterChoicechip> {
|
||||
late ChecklistFilterEditorBloc bloc;
|
||||
late PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
bloc = ChecklistFilterEditorBloc(filterInfo: widget.filterInfo);
|
||||
bloc.add(const ChecklistFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: BlocBuilder<ChecklistFilterEditorBloc, ChecklistFilterEditorState>(
|
||||
builder: (blocContext, state) {
|
||||
return AppFlowyPopover(
|
||||
controller: PopoverController(),
|
||||
constraints: BoxConstraints.loose(const Size(200, 160)),
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
popupBuilder: (BuildContext context) {
|
||||
return ChecklistFilterEditor(
|
||||
bloc: bloc,
|
||||
popoverMutex: popoverMutex,
|
||||
);
|
||||
},
|
||||
child: ChoiceChipButton(
|
||||
filterInfo: widget.filterInfo,
|
||||
filterDesc: state.filterDesc,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistFilterEditor extends StatefulWidget {
|
||||
final ChecklistFilterEditorBloc bloc;
|
||||
final PopoverMutex popoverMutex;
|
||||
const ChecklistFilterEditor(
|
||||
{required this.bloc, required this.popoverMutex, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
ChecklistState createState() => ChecklistState();
|
||||
}
|
||||
|
||||
class ChecklistState extends State<ChecklistFilterEditor> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: widget.bloc,
|
||||
child: BlocBuilder<ChecklistFilterEditorBloc, ChecklistFilterEditorState>(
|
||||
builder: (context, state) {
|
||||
return SizedBox(
|
||||
height: 20,
|
||||
child: Row(
|
||||
children: [
|
||||
FlowyText(state.filterInfo.fieldInfo.name),
|
||||
const HSpace(4),
|
||||
ChecklistFilterConditionPBList(
|
||||
filterInfo: state.filterInfo,
|
||||
),
|
||||
const Spacer(),
|
||||
DisclosureButton(
|
||||
popoverMutex: widget.popoverMutex,
|
||||
onAction: (action) {
|
||||
switch (action) {
|
||||
case FilterDisclosureAction.delete:
|
||||
context
|
||||
.read<ChecklistFilterEditorBloc>()
|
||||
.add(const ChecklistFilterEditorEvent.delete());
|
||||
break;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChecklistFilterConditionPBList extends StatelessWidget {
|
||||
final FilterInfo filterInfo;
|
||||
const ChecklistFilterConditionPBList({
|
||||
required this.filterInfo,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final checklistFilter = filterInfo.checklistFilter()!;
|
||||
return PopoverActionList<ConditionWrapper>(
|
||||
asBarrier: true,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: ChecklistFilterConditionPB.values
|
||||
.map((action) => ConditionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) {
|
||||
return ConditionButton(
|
||||
conditionName: checklistFilter.condition.filterName,
|
||||
onTap: () => controller.show(),
|
||||
);
|
||||
},
|
||||
onSelected: (action, controller) async {
|
||||
context
|
||||
.read<ChecklistFilterEditorBloc>()
|
||||
.add(ChecklistFilterEditorEvent.updateCondition(action.inner));
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ConditionWrapper extends ActionCell {
|
||||
final ChecklistFilterConditionPB inner;
|
||||
|
||||
ConditionWrapper(this.inner);
|
||||
|
||||
@override
|
||||
String get name => inner.filterName;
|
||||
}
|
||||
|
||||
extension ChecklistFilterConditionPBExtension on ChecklistFilterConditionPB {
|
||||
String get filterName {
|
||||
switch (this) {
|
||||
case ChecklistFilterConditionPB.IsComplete:
|
||||
return LocaleKeys.grid_checklistFilter_isComplete.tr();
|
||||
case ChecklistFilterConditionPB.IsIncomplete:
|
||||
return LocaleKeys.grid_checklistFilter_isIncomplted.tr();
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/header/field_type_extension.dart';
|
||||
import 'package:flowy_infra/color_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@@ -37,11 +37,11 @@ class ChoiceChipButton extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
decoration: decoration,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(filterInfo.field.name, fontSize: 12),
|
||||
text: FlowyText(filterInfo.fieldInfo.name),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
radius: const BorderRadius.all(Radius.circular(14)),
|
||||
leftIcon: svgWidget(
|
||||
filterInfo.field.fieldType.iconName(),
|
||||
filterInfo.fieldInfo.fieldType.iconName(),
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
rightIcon: _ChoicechipFilterDesc(filterDesc: filterDesc),
|
||||
|
||||
+14
-14
@@ -12,7 +12,7 @@ import 'package:flutter/material.dart';
|
||||
class SelectOptionFilterConditionList extends StatelessWidget {
|
||||
final FilterInfo filterInfo;
|
||||
final PopoverMutex popoverMutex;
|
||||
final Function(SelectOptionCondition) onCondition;
|
||||
final Function(SelectOptionConditionPB) onCondition;
|
||||
const SelectOptionFilterConditionList({
|
||||
required this.filterInfo,
|
||||
required this.popoverMutex,
|
||||
@@ -27,12 +27,12 @@ class SelectOptionFilterConditionList extends StatelessWidget {
|
||||
asBarrier: true,
|
||||
mutex: popoverMutex,
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
actions: SelectOptionCondition.values
|
||||
actions: SelectOptionConditionPB.values
|
||||
.map(
|
||||
(action) => ConditionWrapper(
|
||||
action,
|
||||
selectOptionFilter.condition == action,
|
||||
filterInfo.field.fieldType,
|
||||
filterInfo.fieldInfo.fieldType,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
@@ -50,7 +50,7 @@ class SelectOptionFilterConditionList extends StatelessWidget {
|
||||
}
|
||||
|
||||
String filterName(SelectOptionFilterPB filter) {
|
||||
if (filterInfo.field.fieldType == FieldType.SingleSelect) {
|
||||
if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
|
||||
return filter.condition.singleSelectFilterName;
|
||||
} else {
|
||||
return filter.condition.multiSelectFilterName;
|
||||
@@ -59,7 +59,7 @@ class SelectOptionFilterConditionList extends StatelessWidget {
|
||||
}
|
||||
|
||||
class ConditionWrapper extends ActionCell {
|
||||
final SelectOptionCondition inner;
|
||||
final SelectOptionConditionPB inner;
|
||||
final bool isSelected;
|
||||
final FieldType fieldType;
|
||||
|
||||
@@ -84,16 +84,16 @@ class ConditionWrapper extends ActionCell {
|
||||
}
|
||||
}
|
||||
|
||||
extension SelectOptionConditionExtension on SelectOptionCondition {
|
||||
extension SelectOptionConditionPBExtension on SelectOptionConditionPB {
|
||||
String get singleSelectFilterName {
|
||||
switch (this) {
|
||||
case SelectOptionCondition.OptionIs:
|
||||
case SelectOptionConditionPB.OptionIs:
|
||||
return LocaleKeys.grid_singleSelectOptionFilter_is.tr();
|
||||
case SelectOptionCondition.OptionIsEmpty:
|
||||
case SelectOptionConditionPB.OptionIsEmpty:
|
||||
return LocaleKeys.grid_singleSelectOptionFilter_isEmpty.tr();
|
||||
case SelectOptionCondition.OptionIsNot:
|
||||
case SelectOptionConditionPB.OptionIsNot:
|
||||
return LocaleKeys.grid_singleSelectOptionFilter_isNot.tr();
|
||||
case SelectOptionCondition.OptionIsNotEmpty:
|
||||
case SelectOptionConditionPB.OptionIsNotEmpty:
|
||||
return LocaleKeys.grid_singleSelectOptionFilter_isNotEmpty.tr();
|
||||
default:
|
||||
return "";
|
||||
@@ -102,13 +102,13 @@ extension SelectOptionConditionExtension on SelectOptionCondition {
|
||||
|
||||
String get multiSelectFilterName {
|
||||
switch (this) {
|
||||
case SelectOptionCondition.OptionIs:
|
||||
case SelectOptionConditionPB.OptionIs:
|
||||
return LocaleKeys.grid_multiSelectOptionFilter_contains.tr();
|
||||
case SelectOptionCondition.OptionIsEmpty:
|
||||
case SelectOptionConditionPB.OptionIsEmpty:
|
||||
return LocaleKeys.grid_multiSelectOptionFilter_isEmpty.tr();
|
||||
case SelectOptionCondition.OptionIsNot:
|
||||
case SelectOptionConditionPB.OptionIsNot:
|
||||
return LocaleKeys.grid_multiSelectOptionFilter_doesNotContain.tr();
|
||||
case SelectOptionCondition.OptionIsNotEmpty:
|
||||
case SelectOptionConditionPB.OptionIsNotEmpty:
|
||||
return LocaleKeys.grid_multiSelectOptionFilter_isNotEmpty.tr();
|
||||
default:
|
||||
return "";
|
||||
|
||||
+27
-10
@@ -1,22 +1,23 @@
|
||||
import 'package:app_flowy/plugins/grid/application/field/field_controller.dart';
|
||||
import 'package:app_flowy/plugins/grid/application/filter/select_option_filter_list_bloc.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/layout/sizes.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/cell/select_option_cell/extension.dart';
|
||||
import 'package:app_flowy/plugins/grid/presentation/widgets/filter/filter_info.dart';
|
||||
import 'package:flowy_infra/image.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/field_entities.pbenum.dart';
|
||||
import 'package:flowy_sdk/protobuf/flowy-grid/select_type_option.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'select_option_loader.dart';
|
||||
|
||||
class SelectOptionFilterList extends StatelessWidget {
|
||||
final String viewId;
|
||||
final FieldInfo fieldInfo;
|
||||
final FilterInfo filterInfo;
|
||||
final List<String> selectedOptionIds;
|
||||
final Function(List<String>) onSelectedOptions;
|
||||
const SelectOptionFilterList({
|
||||
required this.viewId,
|
||||
required this.fieldInfo,
|
||||
required this.filterInfo,
|
||||
required this.selectedOptionIds,
|
||||
required this.onSelectedOptions,
|
||||
Key? key,
|
||||
@@ -25,11 +26,27 @@ class SelectOptionFilterList extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => SelectOptionFilterListBloc(
|
||||
viewId: viewId,
|
||||
fieldPB: fieldInfo.field,
|
||||
selectedOptionIds: selectedOptionIds,
|
||||
)..add(const SelectOptionFilterListEvent.initial()),
|
||||
create: (context) {
|
||||
late SelectOptionFilterListBloc bloc;
|
||||
if (filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
|
||||
bloc = SelectOptionFilterListBloc(
|
||||
viewId: filterInfo.viewId,
|
||||
fieldPB: filterInfo.fieldInfo.field,
|
||||
selectedOptionIds: selectedOptionIds,
|
||||
delegate: SingleSelectOptionFilterDelegateImpl(filterInfo),
|
||||
);
|
||||
} else {
|
||||
bloc = SelectOptionFilterListBloc(
|
||||
viewId: filterInfo.viewId,
|
||||
fieldPB: filterInfo.fieldInfo.field,
|
||||
selectedOptionIds: selectedOptionIds,
|
||||
delegate: MultiSelectOptionFilterDelegateImpl(filterInfo),
|
||||
);
|
||||
}
|
||||
|
||||
bloc.add(const SelectOptionFilterListEvent.initial());
|
||||
return bloc;
|
||||
},
|
||||
child:
|
||||
BlocListener<SelectOptionFilterListBloc, SelectOptionFilterListState>(
|
||||
listenWhen: (previous, current) =>
|
||||
|
||||
Loaded 100 of 565 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user