Compare commits

...
131 Commits
Author SHA1 Message Date
Lucas.Xu 9a387b275f Merge pull request #1595 from LucasXu0/release/0.0.9
chore: release 0.0.9
2022-12-21 11:03:33 +08:00
nathan b6ce7aff9f fix: rename function in test call 2022-12-21 11:03:10 +08:00
Nathan.foooandnathan eee3dcf43a Create test template (#1594)
* refactor: rename functions

* chore: create test templagte

Co-authored-by: nathan <nathan@appflowy.io>
2022-12-21 11:00:08 +08:00
Lucas.Xu 384f8ddcb6 chore: release 0.0.9 2022-12-21 10:35:57 +08:00
Lucas.Xu af716be7aa Merge pull request #1592 from LucasXu0/fix_integration_test
test: fix integration test
2022-12-21 10:09:30 +08:00
Lucas.Xu 86e31253de test: fix integration test 2022-12-21 09:34:14 +08:00
Annie 3f00e78508 chore: update text for folder storage (#1590) 2022-12-20 20:33:41 +08:00
Kelvin 45c73a20c2 test: UnitTest Switching from Multi to text (#1585) 2022-12-20 20:33:17 +08:00
nathan ce86227da3 chore: delete log 2022-12-20 14:45:51 +08:00
Nathan.foooandnathan 4643851b3f refactor: apply cell change set (#1589)
* refactor: update cell changeset trait

* refactor: update cell changeset documentation

Co-authored-by: nathan <nathan@appflowy.io>
2022-12-20 14:40:40 +08:00
Lucas.Xuandnathan 5d7008edd7 feat: Customize the storage folder path (#1538)
* feat: support customize folder path

* feat: add l10n and optimize the logic

* chore: code refactor

* feat: add file read/write permission for macOS

* fix: add toast for restoring path

* feat: fetch apps and show them

* feat: fetch apps and show them

* feat: implement select document logic

* feat: l10n and add select item callback

* feat: add space between tile

* chore: move file exporter to settings

* chore: update UI

* feat: support customizing folder when launching the app

* feat: auto register after customizing folder

* feat: l10n

* feat: l10n

* chore: reinitialize flowy sdk when calling init_sdk

* chore: remove flowysdk const keyword to make sure it can be rebuild

* chore: clear kv values when user logout

* chore: replace current workspace id key in kv.db

* feat: add config.name as a part of seesion_cache_key

* feat: support open folder when launching

* chore: fix some bugs

* chore: dart fix & flutter analyze

* chore: wrap 'sign up with ramdom user' as interface

* feat: dismiss settings view after changing the folder

* fix: read kv value after initializaing with new path

* chore: remove user_id prefix from current workspace key

* fix: move open latest view action to bloc

* test: add test utils for integration tests

* chore: move integration_test to its parent directory

* test: add integration_test ci

* test: switch to B from A, then switch to A again

* chore: fix warings and format code and fix tests

* chore: remove comment out codes

* chore: rename some properties name and optimize the logic

* chore: abstract logic of settings file exporter widget to cubit

* chore: abstract location customizer view from file system view

* chore: abstract settings page index to enum type

* chore: remove the redundant underscore

* test: fix integration test error

* chore: enable integration test for windows and ubuntu

* feat: abstract file picker as service and mock it under integration test

* chore: fix bloc test

Co-authored-by: nathan <nathan@appflowy.io>
2022-12-20 11:14:42 +08:00
Lucas.Xu 079fab6118 Merge pull request #1587 from LucasXu0/fix_#1586
fix: #1586 deleting an image causes the editor to enter a bad state
2022-12-20 09:57:15 +08:00
Lucas.Xu b5f6591daa fix: #1586 deleting an image causes the editor to enter a bad state 2022-12-20 09:37:18 +08:00
Nathan.fooo 9f7f86a49a chore: update test (#1584) 2022-12-20 08:39:46 +08:00
Nathan.foooandnathan 260705280f fix: remove unused steps (#1580)
* chore: remove unused code

* chore: update pin-project version to remove rust lint warnings

* chore: fix potential test failed

Co-authored-by: nathan <nathan@appflowy.io>
2022-12-19 10:47:40 +08:00
Richard Shiue 60d9a6b3ab refactor: appflowy themes (#1567)
* refactor: appflowy themes

* refactor: store ThemeData directly in cubit

* refactor: remove textStyles

* refactor: move AppTheme back into cubit
2022-12-18 16:02:18 +08:00
Nathan.fooo 67e350e797 refactor: provide default type option transform (#1579) 2022-12-17 18:12:47 +08:00
Nathan.fooo 85e489babb Refactor/type option (#1578) 2022-12-17 10:15:13 +08:00
Lucas.Xu d0a914c10f Merge pull request #1573 from juchiast/update-script
build: update install_linux.sh
2022-12-15 20:32:24 +08:00
Duy Do bb312b0a79 build: update install_linux.sh 2022-12-15 11:00:26 +07:00
Lucas.Xu 8cdf6f9ec0 Merge pull request #1557 from rizwan3395/main
fear: support emoji
2022-12-14 11:17:36 +08:00
Nathan.fooo 9666269e27 Merge pull request #1570 from AppFlowy-IO/feat/grid_sort
Feat/grid sort
2022-12-12 21:18:45 +08:00
nathan 31458e817a fix: potential test timeout 2022-12-12 19:49:20 +08:00
nathan 9ba17e004e refactor: type option impl trait 2022-12-12 18:00:09 +08:00
nathan e141e7ee63 chore: add cell cmp trait 2022-12-12 15:29:33 +08:00
Lucas.Xu 3932d0661d feat: update emoji picker UI 2022-12-12 15:10:38 +08:00
Lucas.Xu 98d408af87 fix: flutter analyze problems 2022-12-12 14:51:43 +08:00
Lucas.Xu 15e3d3b486 feat: move emoji_picker to appflowy editor plugin 2022-12-12 11:27:04 +08:00
nathan 1ac1fabb6e chore: add suffix PB to filter structs 2022-12-12 11:25:49 +08:00
nathan 6b4291ccc7 chore: config sort controller 2022-12-12 10:44:34 +08:00
nathan 0cf0b29721 chore: hide the grid blocks from Dart 2022-12-11 13:10:35 +08:00
Nathan.foooandnathan 2a75ad144a fix: the grid cell border color didn't change after switch to new theme (#1564)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-11 09:06:16 +08:00
Nathan.foooandnathan d487820963 fix: update checklist type option data (#1563)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-10 20:46:24 +08:00
Richard Shiue 350c55e71d style: rename theme extension file (#1562) 2022-12-10 19:00:46 +08:00
ENsu fd603fe80f test: fix select transform test name (#1560) 2022-12-10 18:39:37 +08:00
nathan 07ebb0cb95 refactor: refresh rows 2022-12-09 20:04:12 +08:00
Muhammad Rizwan 9dfc8dbb43 Update emoji_select_widget.dart 2022-12-09 15:28:15 +05:00
Haris Arshad 3f27576066 added:emoji
added emoji support
2022-12-09 15:22:43 +05:00
Lucas.Xu 247a19b0c6 Merge pull request #1553 from AppFlowy-IO/update_version_num_0.0.8.1
chore: update changelog
2022-12-09 11:32:04 +08:00
Lucas.Xu 8494e0f0a6 Merge pull request #1554 from LucasXu0/rules
feat: add rules for editorstate.apply
2022-12-09 11:31:51 +08:00
Lucas.Xu e94eb20388 chore: disable rules for test cases 2022-12-09 11:06:28 +08:00
Lucas.Xu 8bbae46a7c feat: add rules for editorstate.apply 2022-12-09 10:36:01 +08:00
nathan 8521cc0897 chore: update changelog 2022-12-09 10:18:38 +08:00
Nathan.foooandnathan 12441f1183 fix: auto size filter menu (#1552)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-09 10:12:45 +08:00
Lucas.Xu 53b7595de8 Merge pull request #1548 from ramanverma2k/fix_1503
Refactor linux workflow & fix 1503
2022-12-09 09:42:59 +08:00
Nathan.foooandnathan 8c225fe547 Feat/restore revision (#1549)
* chore: write snapshot

* chore: add tests

* chore: sync close

* chore: restore from snapshot

* chore: delete invalid revisions after restored from snapshot

* chore: create default view if it fail to deserialize view's revisions when there is no snapshot

* chore: auto generate snapshot

Co-authored-by: nathan <nathan@appflowy.io>
2022-12-09 09:19:47 +08:00
Lucas.Xu a507fb8ec6 Merge pull request #1550 from LucasXu0/fix_windows_copy_paste
fix: Clipboard does not work in Windows #1406
2022-12-08 16:13:01 +08:00
Lucas.Xu 4db5b7a544 chore: typo 2022-12-08 15:50:43 +08:00
Lucas.Xu 1de5b274c3 chore: fix flutter analyze 2022-12-08 15:29:59 +08:00
Lucas.Xu 12b549bad2 fix: Clipboard does not work in Windows #1406 2022-12-08 15:19:21 +08:00
ramanverma2k 6dbb08c2ae fix: move deb control file to linux_installer 2022-12-08 12:04:43 +05:30
Rasyid Miftahul Ihsan 9711d670ca feat: add hover effect when trash icon is selected or hovered. (#1513) 2022-12-08 14:22:27 +08:00
Richard Shiue 442dfe7ef8 feat: switch between light and dark theme based on system settings (#1523)
* feat: allow listening to system for light/dark theme

* chore: implement UI for theme mode setting

* chore: fix translations
2022-12-08 14:21:11 +08:00
Richard Shiue f81d5eb23e fix: location of popover wrt the cell (#1545) 2022-12-08 14:18:57 +08:00
ramanverma2k 32be754eb7 fix: add libkeybinder as a dependency in deb pkg 2022-12-08 10:25:27 +05:30
ramanverma2k 0643e93e94 ci: refactor linux workflow & build an alternative linux package for older distributions. 2022-12-08 10:11:40 +05:30
Lucas.Xu 681d9028e3 Merge pull request #1544 from LucasXu0/fix_1542
fix: [Bug] Text formatting toolbar disappear under layout
2022-12-07 17:09:06 +08:00
Lucas.Xu 49574d3ff0 fix: [Bug] Text formatting toolbar disappear under layout #1542 2022-12-07 10:59:28 +08:00
Lucas.Xu c64b83c2d6 Merge pull request #1541 from LucasXu0/refactor_font_size
chore: refactor font size design
2022-12-06 19:14:14 +08:00
Lucas.Xu 52d65c4c4f Merge pull request #1540 from LucasXu0/missing_code_block_markdown
fix: add missing markdown converter for code block and divider
2022-12-06 18:36:40 +08:00
Lucas.Xu 4da786a439 chore: refactor font size design 2022-12-06 18:36:22 +08:00
Lucas.Xu 114968f2ee fix: markdown decoder and encoder test error 2022-12-06 17:48:00 +08:00
Lucas.Xu 29952bc7fd fix: add missing markdown converter for code block and divider 2022-12-06 17:24:10 +08:00
Nathan.foooandnathan 64ee3140a3 fix: click enter to submit the text content (#1537)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-05 17:17:33 +08:00
Nathan.foooandnathan a2f9ca2f28 fix: options don't refresh after moving the card (#1536)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-05 15:23:59 +08:00
Nathan.foooandnathan 72dc0b8f67 fix: filter orders (#1535)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-05 15:23:48 +08:00
Nathan.foooandnathan 721781f0ae chore: refresh the grid rows after typing text in the text filter (#1533)
Co-authored-by: nathan <nathan@appflowy.io>
2022-12-05 09:55:37 +08:00
Nathan.foooandnathan 0d879a6091 Fix/select option filter (#1530)
* fix: multi select filter bugs

* fix: single select bugs

Co-authored-by: nathan <nathan@appflowy.io>
2022-12-05 09:54:47 +08:00
Nathan.fooo 806a924fff Merge pull request #1528 from AppFlowy-IO/feat/folder_node
Feat/folder node
2022-12-05 09:53:59 +08:00
nathan 9488f42749 chore: macro node 2022-12-04 19:52:24 +08:00
ENsu a2161216d8 feat: can transform options from single & multi select (#1493)
* feat: can transform options from single & multi select

* test: add single-multi switch test
2022-12-04 16:32:37 +08:00
nathan aca9b81c79 chore: implement folder 2022-12-04 09:54:25 +08:00
nathan 81dd353d12 chore: add Node macro 2022-12-03 21:12:45 +08:00
nathan eb1659fd3e chore: add node ast parser 2022-12-03 12:03:52 +08:00
nathan c89277d507 feat: create folder node & add tests 2022-12-02 22:31:03 +08:00
nathan fa01dbb572 chore: rename 2022-12-02 10:44:46 +08:00
Lucas.Xu 63613689cd Merge pull request #1522 from LucasXu0/checklist_typo
chore: checklist.panelTitle typo
2022-12-02 09:43:36 +08:00
Lucas.Xu 89f89e8822 Merge pull request #1519 from LucasXu0/plugin
Separate AppFlowy Editor Plugins
2022-12-02 09:43:20 +08:00
Lucas.Xu a801fa627c chore: checklist.panelTitle typo 2022-12-01 19:42:49 +08:00
Lucas.Xu c6c164d347 feat: integrate divider, code block, and math equation into appflowy 2022-12-01 19:26:00 +08:00
Nathan.fooo be4222a62f Merge pull request #1518 from richardshiue/deprecate-textstyle-getters
chore: remove unused getters for TextStyles
2022-12-01 19:05:49 +08:00
Lucas.Xu 4fa2d6dc2e feat: overwrite paste logic in code block 2022-12-01 18:54:48 +08:00
Lucas.Xu e476337a6a feat: move code block plugin to appflowy editor plugins directory 2022-12-01 18:35:27 +08:00
nathan 44f9254c0a chore: add codegen crate 2022-12-01 15:17:55 +08:00
Lucas.Xu 89becbfe71 feat: add hint text for math equation plugin 2022-12-01 15:08:16 +08:00
Richard Shiue cf42d8cd43 chore: remove unused getters for TextStyles 2022-12-01 14:45:59 +08:00
Lucas.Xu 2b27fe85aa feat: move math equation plugin to appflowy editor plugins directory 2022-12-01 14:44:06 +08:00
nathan 30e7323cf7 chore: delete flowy-tool 2022-12-01 14:42:09 +08:00
Nathan.fooo 39a97499de Merge pull request #1516 from AppFlowy-IO/refactor/backend_event_system 2022-12-01 11:49:48 +08:00
Lucas.Xu 157f929ff9 feat: move divider plugin to appflowy editor plugins directory 2022-12-01 11:14:35 +08:00
nathan bf36ef7fd9 chore: update documentation 2022-12-01 10:59:22 +08:00
Lucas.Xu 7ba638268b feat: init appflowy editor plugins 2022-12-01 09:47:28 +08:00
nathan 96c427e869 refactor: rename flowy-dispatch structs 2022-12-01 09:26:16 +08:00
Nathan.fooo fff4af53e2 Merge pull request #1510 from LucasXu0/fix_1488
fix: [Bug] Inconsistent font weights #1488
2022-11-30 20:31:39 +08:00
Nathan.fooo 6910004003 Merge pull request #1512 from richardshiue/fix-flowytextfield-bugs
fix: flowy text field bugs
2022-11-30 20:30:42 +08:00
Nathan.fooo 7cb964c7a2 Merge pull request #1511 from AppFlowy-IO/feat/checklist_filter
chore: support checklist filter
2022-11-30 20:24:15 +08:00
Richard Shiue eaa1cb5a67 fix: flowy text field bugs 2022-11-30 18:28:51 +08:00
nathan 553cfb3f5e chore: support checklist filter 2022-11-30 16:43:57 +08:00
Lucas.Xu 086195d2cf fix: inconsistent font weight in Chinese #1488 2022-11-30 16:36:53 +08:00
Nathan.fooo 64d9b76e3a Merge pull request #1509 from AppFlowy-IO/feat/remove_debug_info
chore: disable split-debuginfo
2022-11-30 15:59:03 +08:00
Nathan.fooo 5ce8087c91 Merge pull request #1508 from AppFlowy-IO/update_version_num_0.0.8
Update version num 0.0.8
2022-11-30 15:37:14 +08:00
nathan 6117f84373 chore: disable split-debuginfo 2022-11-30 15:36:39 +08:00
Nathan.fooo c8b90974fb Feat: support new column type, Checklist.
Feat/support checklist type
2022-11-30 15:32:19 +08:00
nathan f1a25ab4b7 chore: update version 2022-11-30 15:26:57 +08:00
nathan 2d11d3a68d chore: update version 2022-11-30 15:19:09 +08:00
nathan d89a89131e chore: remove textstyle ref 2022-11-30 15:16:01 +08:00
nathan a800e01f0b chore: config checklist board UI 2022-11-30 15:15:05 +08:00
nathan 29e07089ca fix: checklist cell did get notified after the cell content change 2022-11-30 15:15:03 +08:00
nathan 3cdd6665b3 chore: config checklist cell 2022-11-30 15:14:27 +08:00
nathan 1ba299815e feat: implement checklist UI 2022-11-30 15:14:27 +08:00
nathan b4671c1d99 feat: support checklist 2022-11-30 15:14:27 +08:00
Richard ShiueandNathan.fooo aa85970b0f chore: port more const textstyles to theme provider styles (#1477)
* chore: port more const textstyles to theme provider styles

* style: unnecessary or use shared text fontSizes

* fix: build error from merging main

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
2022-11-30 15:11:32 +08:00
Richard Shiue 3b3b61e67c chore: select option cell & header editor UI + move to theme.of(context).texttheme (#1483)
* chore: improvements for suffix text in textfields

* chore: port more const textstyles to theme provider styles

* chore: select option editor UI improvements
2022-11-30 15:02:41 +08:00
Richard Shiue f02e77fcd8 fix: onPrimary and onPrimary colors (#1499) 2022-11-30 15:00:14 +08:00
Lucas.Xu 5adf3139ba Merge pull request #1507 from LucasXu0/remove_svg_function
Remove svg function
2022-11-30 13:44:41 +08:00
Lucas.Xu c127fd28b8 chore: fix flutter analyze 2022-11-30 13:23:03 +08:00
Lucas.Xu d7783a6440 chore: remove duplicated svg function 2022-11-30 13:22:16 +08:00
Lucas.Xu fba69767a7 Merge pull request #1495 from LucasXu0/customize_font_size
Customize Font Size In AppFlowy #1479
2022-11-29 13:45:33 +08:00
Nathan.foooandnathan acaaabee2a chore: add transform field tests (#1504)
Co-authored-by: nathan <nathan@appflowy.io>
2022-11-29 13:34:10 +08:00
Lucas.Xu 925ef48a83 fix: build macos install ci error (#1502) 2022-11-29 09:38:06 +08:00
Nathan.foooandnathan 80d1cbabe0 feat: support multi-select option filter (#1501)
Co-authored-by: nathan <nathan@appflowy.io>
2022-11-28 22:49:13 +08:00
Lucas.Xu 43031b50eb feat: update bold text style 2022-11-28 21:31:33 +08:00
Lucas.Xu 52ba0c0092 fix: the cursor shows sometimes when tapping the checkbox 2022-11-28 18:44:48 +08:00
Lucas.Xu 0e55cd07a4 chore: fix typo 2022-11-28 18:32:32 +08:00
Lucas.Xu 14ac2db06d Merge remote-tracking branch 'origin/main' into customize_font_size 2022-11-28 18:11:06 +08:00
Lucas.Xu a162b02476 feat: convert provider to cubit #1479 2022-11-28 18:02:05 +08:00
Lucas.Xu 04e14c6bba fix: transparent color should be treated as a highlight color 2022-11-28 16:22:10 +08:00
Lucas.Xu d75645c4bc fix: remove the unnecessary text style update 2022-11-28 16:17:46 +08:00
Lucas.Xu 6d6e61956a feat: Customize Font Size In AppFlowy #1479 2022-11-28 15:34:55 +08:00
Lucas.Xu 37b119172b feat: Customize Font Size In AppFlowy #1479 2022-11-28 13:26:43 +08:00
Lucas.Xu 0ba26e0a84 feat: Customize Font Size In AppFlowy #1479 2022-11-28 10:37:37 +08:00
565 changed files with 17653 additions and 7673 deletions

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
+128
View File
@@ -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
+88 -31
View File
@@ -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 \
+23 -30
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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"
}
}
}
+43 -3
View File
@@ -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';
+10 -6
View File
@@ -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);
}
@@ -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)),
@@ -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 {
@@ -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);
@@ -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()
@@ -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(
@@ -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();
@@ -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) {
@@ -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,
);
},
);
}
}
@@ -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,
),
);
},
);
}
}
@@ -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';
@@ -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,
@@ -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),
),
);
}
}
@@ -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,
);
}
@@ -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,
@@ -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,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,
@@ -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);
@@ -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 "";
@@ -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 "";
}
}
}
@@ -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),
@@ -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 "";
@@ -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